Browse code

Allow macvlan endpoint to start with parent down

When a macvlan's parent interface is down it's not possible
to send NA messages. So, ignore the error.

Signed-off-by: Rob Murray <rob.murray@docker.com>

Rob Murray authored on 2025/03/11 23:32:46
Showing 2 changed files
... ...
@@ -19,6 +19,7 @@ import (
19 19
 	"github.com/docker/docker/testutil/daemon"
20 20
 	"gotest.tools/v3/assert"
21 21
 	is "gotest.tools/v3/assert/cmp"
22
+	"gotest.tools/v3/icmd"
22 23
 	"gotest.tools/v3/skip"
23 24
 )
24 25
 
... ...
@@ -670,3 +671,28 @@ func TestEndpointWithCustomIfname(t *testing.T) {
670 670
 	assert.NilError(t, err)
671 671
 	assert.Assert(t, strings.Contains(out.Stdout, ": foobar@if"), "expected ': foobar@if' in 'ip link show':\n%s", out.Stdout)
672 672
 }
673
+
674
+// TestParentDown checks that when a macvlan's parent is down, a container can still
675
+// be attached.
676
+// Regression test for https://github.com/moby/moby/issues/49593
677
+func TestParentDown(t *testing.T) {
678
+	skip.If(t, testEnv.IsRemoteDaemon)
679
+	skip.If(t, testEnv.IsRootless, "rootless mode has different view of network")
680
+
681
+	ctx := setupTest(t)
682
+	apiClient := testEnv.APIClient()
683
+
684
+	const tap = "dummytap0"
685
+	res := icmd.RunCommand("ip", "tuntap", "add", "mode", "tap", tap)
686
+	res.Assert(t, icmd.Success)
687
+	defer icmd.RunCommand("ip", "link", "del", tap)
688
+
689
+	const netName = "testnet-macvlan"
690
+	net.CreateNoError(ctx, t, apiClient, netName,
691
+		net.WithMacvlan(tap),
692
+		net.WithIPv6(),
693
+	)
694
+
695
+	ctrID := container.Run(ctx, t, apiClient, container.WithNetworkMode(netName))
696
+	defer container.Remove(ctx, t, apiClient, ctrID, containertypes.RemoveOptions{Force: true})
697
+}
... ...
@@ -300,8 +300,8 @@ func (n *Namespace) AddInterface(ctx context.Context, srcName, dstPrefix, dstNam
300 300
 		if err := waitForBridgePort(ctx, ns.NlHandle(), iface); err != nil {
301 301
 			return fmt.Errorf("check bridge port state: %w", err)
302 302
 		}
303
-		waitForMcastRoute(ctx, iface.Attrs().Index, i, n.nlHandle)
304
-		if err := n.advertiseAddrs(ctx, iface.Attrs().Index, i, n.nlHandle); err != nil {
303
+		mcastRouteOk := waitForMcastRoute(ctx, iface.Attrs().Index, i, n.nlHandle)
304
+		if err := n.advertiseAddrs(ctx, iface.Attrs().Index, i, n.nlHandle, mcastRouteOk); err != nil {
305 305
 			return fmt.Errorf("failed to advertise addresses: %w", err)
306 306
 		}
307 307
 	}
... ...
@@ -606,9 +606,9 @@ func stpEnabled(ctx context.Context, name string) bool {
606 606
 // That error has not been seen since addition of the check that the veth's parent bridge port
607 607
 // is forwarding, so that may have been the issue. But, in case it's a timing problem that's
608 608
 // only less-likely because of delay caused by that check, make sure the route exists.
609
-func waitForMcastRoute(ctx context.Context, ifIndex int, i *Interface, nlh nlwrap.Handle) {
609
+func waitForMcastRoute(ctx context.Context, ifIndex int, i *Interface, nlh nlwrap.Handle) bool {
610 610
 	if i.addressIPv6 == nil || i.advertiseAddrNMsgs == 0 {
611
-		return
611
+		return true
612 612
 	}
613 613
 	ctx, span := otel.Tracer("").Start(ctx, "libnetwork.osl.waitForMcastRoute")
614 614
 	defer span.End()
... ...
@@ -624,16 +624,17 @@ func waitForMcastRoute(ctx context.Context, ifIndex int, i *Interface, nlh nlwra
624 624
 			// FIXME(robmry) - if EMSGSIZE is returned (why?), it seems to be persistent.
625 625
 			//  So, skip the delay and continue to the NA send as it seems to succeed.
626 626
 			log.G(ctx).Info("Skipping check for route to send NA, EMSGSIZE")
627
-			return
627
+			return true
628 628
 		}
629 629
 		if err != nil || len(routes) == 0 {
630 630
 			log.G(ctx).WithFields(log.Fields{"error": err, "nroutes": len(routes)}).Info("Waiting for route to send NA")
631 631
 			time.Sleep(pollInterval)
632 632
 			continue
633 633
 		}
634
-		return
634
+		return true
635 635
 	}
636 636
 	log.G(ctx).WithField("", maxWait).Warn("No route for neighbour advertisement")
637
+	return false
637 638
 }
638 639
 
639 640
 // advertiseAddrs triggers send unsolicited ARP and Neighbour Advertisement
... ...
@@ -646,16 +647,17 @@ func waitForMcastRoute(ctx context.Context, ifIndex int, i *Interface, nlh nlwra
646 646
 // until the ARP/Neighbour cache entries expire.
647 647
 //
648 648
 // Note that the kernel's arp_notify sysctl setting is not respected.
649
-func (n *Namespace) advertiseAddrs(ctx context.Context, ifIndex int, i *Interface, nlh nlwrap.Handle) error {
649
+func (n *Namespace) advertiseAddrs(ctx context.Context, ifIndex int, i *Interface, nlh nlwrap.Handle, mcastRouteOk bool) error {
650 650
 	mac := i.MacAddress()
651 651
 	address4 := i.Address()
652 652
 	address6 := i.AddressIPv6()
653 653
 	ctx = log.WithLogger(ctx, log.G(ctx).WithFields(log.Fields{
654
-		"iface": i.dstName,
655
-		"ifi":   ifIndex,
656
-		"mac":   mac.String(),
657
-		"ip4":   address4,
658
-		"ip6":   address6,
654
+		"iface":        i.dstName,
655
+		"ifi":          ifIndex,
656
+		"mac":          mac.String(),
657
+		"ip4":          address4,
658
+		"ip6":          address6,
659
+		"mcastRouteOk": mcastRouteOk,
659 660
 	}))
660 661
 
661 662
 	if address4 == nil && address6 == nil {
... ...
@@ -712,7 +714,11 @@ func (n *Namespace) advertiseAddrs(ctx context.Context, ifIndex int, i *Interfac
712 712
 		if naSender != nil {
713 713
 			if err := naSender.Send(); err != nil {
714 714
 				log.G(ctx).WithError(err).Warn("Failed to send unsolicited NA")
715
-				errs = append(errs, err)
715
+				// If there was no multicast route and the network is unreachable, ignore the
716
+				// error - this happens when a macvlan's parent interface is down.
717
+				if mcastRouteOk || !errors.Is(err, unix.ENETUNREACH) {
718
+					errs = append(errs, err)
719
+				}
716 720
 			}
717 721
 		}
718 722
 		return errors.Join(errs...)