Skip to content

Commit 81d103a

Browse files
committed
[ipam/eni] wait for ENI interface before CNI ADD route setup
Signed-off-by: Jared Ledvina <jared.ledvina@datadoghq.com>
1 parent fd380fd commit 81d103a

6 files changed

Lines changed: 329 additions & 32 deletions

File tree

daemon/infraendpoints/infra_ip_allocation.go

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import (
2121
"github.com/cilium/statedb"
2222
"github.com/vishvananda/netlink"
2323
"go4.org/netipx"
24-
"golang.org/x/sys/unix"
25-
"k8s.io/apimachinery/pkg/util/wait"
2624

2725
"github.com/cilium/cilium/pkg/common"
2826
linuxrouting "github.com/cilium/cilium/pkg/datapath/linux/routing"
@@ -33,6 +31,7 @@ import (
3331
"github.com/cilium/cilium/pkg/ipam"
3432
ipamOption "github.com/cilium/cilium/pkg/ipam/option"
3533
"github.com/cilium/cilium/pkg/logging/logfields"
34+
"github.com/cilium/cilium/pkg/mac"
3635
"github.com/cilium/cilium/pkg/mtu"
3736
"github.com/cilium/cilium/pkg/node"
3837
"github.com/cilium/cilium/pkg/option"
@@ -220,33 +219,21 @@ func (r *infraIPAllocator) reallocateOldRouterIPs(fromK8s, fromFS net.IP) (resul
220219
return result
221220
}
222221

222+
// waitForENI blocks until the ENI's netlink interface for macAddr is
223+
// observed on the node, guarding against the case where the operator has
224+
// reported the ENI as attached (and handed out IPs from it) before the
225+
// interface is actually visible via netlink. This delegates to the shared
226+
// linuxrouting.WaitForENIInterface helper, which is also used by the CNI ADD
227+
// datapath setup path and (below) the ingress IP path; see
228+
// https://github.com/cilium/cilium/pull/41954,
229+
// https://github.com/cilium/cilium/pull/47295, and
230+
// https://github.com/cilium/cilium/issues/45414.
223231
func (r *infraIPAllocator) waitForENI(ctx context.Context, macAddr string) error {
224-
bo := wait.Backoff{
225-
Duration: 250 * time.Millisecond,
226-
Factor: 2,
227-
Jitter: 0.2,
228-
Steps: 5,
229-
}
230-
231-
findENIByMAC := func(ctx context.Context) (bool, error) {
232-
links, err := safenetlink.LinkList()
233-
if err != nil {
234-
return false, fmt.Errorf("unable to list interfaces: %w", err)
235-
}
236-
237-
for _, l := range links {
238-
// filter out slave devices
239-
if l.Attrs().RawFlags&unix.IFF_SLAVE != 0 {
240-
continue
241-
}
242-
if l.Attrs().HardwareAddr.String() == macAddr {
243-
return true, nil
244-
}
245-
}
246-
return false, nil
232+
parsedMAC, err := net.ParseMAC(macAddr)
233+
if err != nil {
234+
return fmt.Errorf("invalid MAC address %q: %w", macAddr, err)
247235
}
248-
249-
return wait.ExponentialBackoffWithContext(ctx, bo, findENIByMAC)
236+
return linuxrouting.WaitForENIInterface(ctx, mac.MAC(parsedMAC))
250237
}
251238

252239
func (r *infraIPAllocator) reallocateRouterIPs(ctx context.Context, family node.AddressingFamily, fromK8s, fromFS net.IP) (routerIP net.IP, err error) {
@@ -299,6 +286,7 @@ func (r *infraIPAllocator) reallocateRouterIPs(ctx context.Context, family node.
299286
if err := r.waitForENI(ctx, result.PrimaryMAC); err != nil {
300287
r.logger.Warn("unable to find ENI netlink interface, this will likely lead to an error in configuring the router routes and rules",
301288
logfields.MACAddr, result.PrimaryMAC,
289+
logfields.Error, err,
302290
)
303291
}
304292
}
@@ -425,7 +413,7 @@ func (r *infraIPAllocator) allocateHealthIPs(oldV4HealthIP netip.Addr, oldV6Heal
425413
return nil
426414
}
427415

428-
func (r *infraIPAllocator) allocateIngressIPs(oldV4IngressIP net.IP, oldV6IngressIP net.IP) error {
416+
func (r *infraIPAllocator) allocateIngressIPs(ctx context.Context, oldV4IngressIP net.IP, oldV6IngressIP net.IP) error {
429417
if !r.daemonConfig.EnableEnvoyConfig {
430418
return nil
431419
}
@@ -475,6 +463,20 @@ func (r *infraIPAllocator) allocateIngressIPs(oldV4IngressIP net.IP, oldV6Ingres
475463
if ingressRouting, err := r.parseRoutingInfo(result); err != nil {
476464
r.logger.Warn("Unable to allocate ingress information for ENI", logfields.Error, err)
477465
} else {
466+
// wait for the ENI to be up and running before configuring
467+
// routes and rules, mirroring the guard used for the router
468+
// interface above. See
469+
// https://github.com/cilium/cilium/pull/41954 and
470+
// https://github.com/cilium/cilium/pull/47295.
471+
if r.daemonConfig.IPAM == ipamOption.IPAMENI {
472+
if err := r.waitForENI(ctx, result.PrimaryMAC); err != nil {
473+
r.logger.Warn("unable to find ENI netlink interface, this will likely lead to an error in configuring the ingress routes and rules",
474+
logfields.MACAddr, result.PrimaryMAC,
475+
logfields.Error, err,
476+
)
477+
}
478+
}
479+
478480
if err := ingressRouting.Configure(
479481
result.IP,
480482
r.mtuManager.GetDeviceMTU(),
@@ -553,7 +555,7 @@ func (r *infraIPAllocator) AllocateIPs(ctx context.Context) error {
553555
return fmt.Errorf("failed to allocate service loopback IPs: %w", err)
554556
}
555557

556-
if err := r.allocateIngressIPs(localNode.IPv4IngressIP, localNode.IPv6IngressIP); err != nil {
558+
if err := r.allocateIngressIPs(ctx, localNode.IPv4IngressIP, localNode.IPv6IngressIP); err != nil {
557559
return fmt.Errorf("failed to allocate ingress IPs: %w", err)
558560
}
559561

daemon/infraendpoints/infra_ip_allocation_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package infraendpoints
55

66
import (
7+
"context"
78
"fmt"
89
"net"
910
"net/netip"
@@ -323,3 +324,17 @@ func Test_getCiliumHostIPsFromFile(t *testing.T) {
323324
})
324325
}
325326
}
327+
328+
// TestWaitForENIInvalidMAC verifies that waitForENI fails fast with a clear
329+
// error when handed a malformed MAC address, rather than delegating to the
330+
// netlink poll and exhausting the backoff. This exercises the net.ParseMAC
331+
// guard added when the helper was refactored onto linuxrouting.WaitForENIInterface.
332+
func TestWaitForENIInvalidMAC(t *testing.T) {
333+
r := &infraIPAllocator{
334+
logger: hivetest.Logger(t),
335+
}
336+
337+
err := r.waitForENI(context.Background(), "not-a-valid-mac")
338+
require.Error(t, err)
339+
require.ErrorContains(t, err, "invalid MAC address")
340+
}

pkg/datapath/linux/routing/routing.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package linuxrouting
55

66
import (
7+
"context"
78
"errors"
89
"fmt"
910
"log/slog"
@@ -13,6 +14,7 @@ import (
1314
"github.com/vishvananda/netlink"
1415
"go4.org/netipx"
1516
"golang.org/x/sys/unix"
17+
"k8s.io/apimachinery/pkg/util/wait"
1618

1719
"github.com/cilium/statedb"
1820

@@ -24,8 +26,73 @@ import (
2426
"github.com/cilium/cilium/pkg/logging/logfields"
2527
"github.com/cilium/cilium/pkg/mac"
2628
"github.com/cilium/cilium/pkg/option"
29+
"github.com/cilium/cilium/pkg/time"
2730
)
2831

32+
// WaitForENIInterfaceBackoff bounds how long WaitForENIInterface will poll
33+
// for an ENI's netlink interface to appear before giving up.
34+
//
35+
// ENI attachment is asynchronous: the operator can report an ENI as
36+
// attached (and cilium-agent can hand out an IP from it) before the
37+
// corresponding netlink device is actually visible on the node. Any code
38+
// path that resolves the ENI's ifindex by MAC address immediately after
39+
// that can lose the race and fail with "interface with MAC ... not found".
40+
//
41+
// This was first observed and guarded against for the router interface in
42+
// https://github.com/cilium/cilium/pull/41954, which used a 5-step backoff
43+
// (sleeps of 250ms, 500ms, 1s, 2s -- ~3.75s before jitter, up to ~4.5s with
44+
// the 20% jitter). That window has since proven too short in some
45+
// environments -- the guard still fires as a CI flake, see
46+
// https://github.com/cilium/cilium/issues/45414 -- so the shared helper below
47+
// extends it to 7 steps (7 condition checks, so 6 sleeps of 250ms, 500ms,
48+
// 1s, 2s, 4s, 8s -- ~15.75s before jitter, up to ~18.9s with the 20% jitter)
49+
// to give the ENI more time to show up. If it still never appears,
50+
// WaitForENIInterface returns an error; its callers (in
51+
// daemon/infraendpoints and plugins/cilium-cni) then log a warning and fall
52+
// through to their Configure call, which is what ultimately fails the
53+
// operation (e.g. CNI ADD) with "interface with MAC ... not found".
54+
var WaitForENIInterfaceBackoff = wait.Backoff{
55+
Duration: 250 * time.Millisecond,
56+
Factor: 2,
57+
Jitter: 0.2,
58+
Steps: 7,
59+
}
60+
61+
// WaitForENIInterface blocks until a (non-slave) netlink interface with the
62+
// given MAC address is observed on the node, or until ctx is cancelled or
63+
// WaitForENIInterfaceBackoff is exhausted.
64+
//
65+
// This is shared by every code path that needs to resolve an ENI's ifindex
66+
// by MAC address shortly after the ENI is reported as attached: the router
67+
// interface, the ingress IP interface, and the per-pod CNI ADD datapath
68+
// setup performed by retrieveIfIndexFromMAC below.
69+
//
70+
// See https://github.com/cilium/cilium/pull/41954 (original router-only
71+
// guard, merged) and https://github.com/cilium/cilium/pull/47295 (equivalent
72+
// guard for the ingress path, proposed upstream but not yet merged).
73+
func WaitForENIInterface(ctx context.Context, macAddr mac.MAC) error {
74+
findENIByMAC := func(ctx context.Context) (bool, error) {
75+
links, err := safenetlink.LinkList()
76+
if err != nil {
77+
return false, fmt.Errorf("unable to list interfaces: %w", err)
78+
}
79+
80+
for _, l := range links {
81+
// Linux slave devices have the same MAC address as their master
82+
// device, but we want the master device.
83+
if l.Attrs().RawFlags&unix.IFF_SLAVE != 0 {
84+
continue
85+
}
86+
if l.Attrs().HardwareAddr.String() == macAddr.String() {
87+
return true, nil
88+
}
89+
}
90+
return false, nil
91+
}
92+
93+
return wait.ExponentialBackoffWithContext(ctx, WaitForENIInterfaceBackoff, findENIByMAC)
94+
}
95+
2996
// useCompatEgressPriority determines whether to use the new or old style egress rule.
3097
// Old style rules are only used in Azure IPAM mode.
3198
func (info *RoutingInfo) useCompatEgressPriority() bool {

0 commit comments

Comments
 (0)