From 841c4c8057bcf5317d6565875595a3f0c046e3fa Mon Sep 17 00:00:00 2001 From: Rob Murray Date: Tue, 9 Apr 2024 15:30:12 +0100 Subject: [PATCH] Disable IPv6 for endpoints in '--ipv6=false' networks. No IPAM IPv6 address is given to an interface in a network with '--ipv6=false', but the kernel would assign a link-local address and, in a macvlan/ipvlan network, the interface may get a SLAAC-assigned address. So, disable IPv6 on the interface to avoid that. Signed-off-by: Rob Murray --- integration/network/ipvlan/ipvlan_test.go | 26 +++++++++++++- integration/network/macvlan/macvlan_test.go | 29 +++++++++++++++ integration/networking/bridge_test.go | 39 +++++++++++++++++++-- libnetwork/osl/interface_linux.go | 21 +++++++---- 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/integration/network/ipvlan/ipvlan_test.go b/integration/network/ipvlan/ipvlan_test.go index e4d4aad04b..14c5ea8702 100644 --- a/integration/network/ipvlan/ipvlan_test.go +++ b/integration/network/ipvlan/ipvlan_test.go @@ -94,6 +94,9 @@ func TestDockerNetworkIpvlan(t *testing.T) { }, { name: "L3Addressing", test: testIpvlanL3Addressing, + }, { + name: "NoIPv6", + test: testIpvlanNoIPv6, }, } { @@ -441,6 +444,28 @@ func testIpvlanL3Addressing(t *testing.T, ctx context.Context, client dclient.AP assert.Check(t, is.Contains(result.Combined(), "default dev eth0")) } +// Check that an ipvlan interface with '--ipv6=false' doesn't get kernel-assigned +// IPv6 addresses, but the loopback interface does still have an IPv6 address ('::1'). +func testIpvlanNoIPv6(t *testing.T, ctx context.Context, client dclient.APIClient) { + const netName = "ipvlannet" + net.CreateNoError(ctx, t, client, netName, net.WithIPvlan("", "l3")) + assert.Check(t, n.IsNetworkAvailable(ctx, client, netName)) + + id := container.Run(ctx, t, client, container.WithNetworkMode(netName)) + + loRes := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "lo"}) + assert.Check(t, is.Contains(loRes.Combined(), " inet ")) + assert.Check(t, is.Contains(loRes.Combined(), " inet6 ")) + + eth0Res := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "eth0"}) + assert.Check(t, is.Contains(eth0Res.Combined(), " inet ")) + assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "), + "result.Combined(): %s", eth0Res.Combined()) + + sysctlRes := container.ExecT(ctx, t, client, id, []string{"sysctl", "-n", "net.ipv6.conf.eth0.disable_ipv6"}) + assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), "1")) +} + // TestIPVlanDNS checks whether DNS is forwarded, for combinations of l2/l3 mode, // with/without a parent interface, and with '--internal'. Note that, there's no // attempt here to give the ipvlan network external connectivity - when this test @@ -452,7 +477,6 @@ func testIpvlanL3Addressing(t *testing.T, ctx context.Context, client dclient.AP // https://github.com/moby/moby/issues/47662 func TestIPVlanDNS(t *testing.T) { skip.If(t, testEnv.IsRootless, "rootless mode has different view of network") - ctx := testutil.StartSpan(baseContext, t) net.StartDaftDNS(t, "127.0.0.1") diff --git a/integration/network/macvlan/macvlan_test.go b/integration/network/macvlan/macvlan_test.go index ee510124f4..f07119d5be 100644 --- a/integration/network/macvlan/macvlan_test.go +++ b/integration/network/macvlan/macvlan_test.go @@ -77,6 +77,9 @@ func TestDockerNetworkMacvlan(t *testing.T) { }, { name: "Addressing", test: testMacvlanAddressing, + }, { + name: "NoIPv6", + test: testMacvlanNoIPv6, }, } { tc := tc @@ -298,6 +301,32 @@ func testMacvlanAddressing(t *testing.T, ctx context.Context, client client.APIC assert.Check(t, strings.Contains(result.Combined(), "default via 2001:db8:abca::254 dev eth0")) } +// Check that a macvlan interface with '--ipv6=false' doesn't get kernel-assigned +// IPv6 addresses, but the loopback interface does still have an IPv6 address ('::1'). +func testMacvlanNoIPv6(t *testing.T, ctx context.Context, client client.APIClient) { + const netName = "macvlannet" + + net.CreateNoError(ctx, t, client, netName, + net.WithMacvlan(""), + net.WithOption("macvlan_mode", "bridge"), + ) + assert.Check(t, n.IsNetworkAvailable(ctx, client, netName)) + + id := container.Run(ctx, t, client, container.WithNetworkMode(netName)) + + loRes := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "lo"}) + assert.Check(t, is.Contains(loRes.Combined(), " inet ")) + assert.Check(t, is.Contains(loRes.Combined(), " inet6 ")) + + eth0Res := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "eth0"}) + assert.Check(t, is.Contains(eth0Res.Combined(), " inet ")) + assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "), + "result.Combined(): %s", eth0Res.Combined()) + + sysctlRes := container.ExecT(ctx, t, client, id, []string{"sysctl", "-n", "net.ipv6.conf.eth0.disable_ipv6"}) + assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), "1")) +} + // TestMACVlanDNS checks whether DNS is forwarded, with/without a parent // interface, and with '--internal'. Note that there's no attempt here to give // the macvlan network external connectivity - when this test supplies a parent diff --git a/integration/networking/bridge_test.go b/integration/networking/bridge_test.go index b5eb0fdce0..d0726af541 100644 --- a/integration/networking/bridge_test.go +++ b/integration/networking/bridge_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "regexp" + "strings" "testing" "time" @@ -611,8 +612,8 @@ func TestInternalNwConnectivity(t *testing.T) { assert.Check(t, is.Contains(res.Stderr(), "Network is unreachable")) } -// Check that the container's interface has no IPv6 address when IPv6 is -// disabled in a container via sysctl. +// Check that the container's interfaces have no IPv6 address when IPv6 is +// disabled in a container via sysctl (including 'lo'). func TestDisableIPv6Addrs(t *testing.T) { skip.If(t, testEnv.DaemonInfo.OSType == "windows") @@ -676,6 +677,40 @@ func TestDisableIPv6Addrs(t *testing.T) { } } +// Check that an interface to an '--ipv6=false' network has no IPv6 +// address - either IPAM assigned, or kernel-assigned LL, but the loopback +// interface does still have an IPv6 address ('::1'). +func TestNonIPv6Network(t *testing.T) { + skip.If(t, testEnv.DaemonInfo.OSType == "windows") + + ctx := setupTest(t) + d := daemon.New(t) + d.StartWithBusybox(ctx, t) + defer d.Stop(t) + + c := d.NewClientT(t) + defer c.Close() + + const netName = "testnet" + network.CreateNoError(ctx, t, c, netName) + defer network.RemoveNoError(ctx, t, c, netName) + + id := container.Run(ctx, t, c, container.WithNetworkMode(netName)) + defer c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true}) + + loRes := container.ExecT(ctx, t, c, id, []string{"ip", "a", "show", "dev", "lo"}) + assert.Check(t, is.Contains(loRes.Combined(), " inet ")) + assert.Check(t, is.Contains(loRes.Combined(), " inet6 ")) + + eth0Res := container.ExecT(ctx, t, c, id, []string{"ip", "a", "show", "dev", "eth0"}) + assert.Check(t, is.Contains(eth0Res.Combined(), " inet ")) + assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "), + "result.Combined(): %s", eth0Res.Combined()) + + sysctlRes := container.ExecT(ctx, t, c, id, []string{"sysctl", "-n", "net.ipv6.conf.eth0.disable_ipv6"}) + assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), "1")) +} + // Test that it's possible to set a sysctl on an interface in the container. // Regression test for https://github.com/moby/moby/issues/47619 func TestSetInterfaceSysctl(t *testing.T) { diff --git a/libnetwork/osl/interface_linux.go b/libnetwork/osl/interface_linux.go index e87efbaa39..3491aac70c 100644 --- a/libnetwork/osl/interface_linux.go +++ b/libnetwork/osl/interface_linux.go @@ -363,17 +363,24 @@ func setInterfaceIP(nlh *netlink.Handle, iface netlink.Link, i *Interface) error } func setInterfaceIPv6(nlh *netlink.Handle, iface netlink.Link, i *Interface) error { - if i.AddressIPv6() == nil { + addr := i.AddressIPv6() + // IPv6 must be enabled on the interface if and only if the network is + // IPv6-enabled. For an interface on an IPv4-only network, if IPv6 isn't + // disabled, the interface will be put into IPv6 multicast groups making + // it unexpectedly susceptible to NDP cache poisoning, route injection, etc. + // (At present, there will always be a pre-configured IPv6 address if the + // network is IPv6-enabled.) + if err := setIPv6(i.ns.path, i.DstName(), addr != nil); err != nil { + return fmt.Errorf("failed to configure ipv6: %v", err) + } + if addr == nil { return nil } - if err := checkRouteConflict(nlh, i.AddressIPv6(), netlink.FAMILY_V6); err != nil { + if err := checkRouteConflict(nlh, addr, netlink.FAMILY_V6); err != nil { return err } - if err := setIPv6(i.ns.path, i.DstName(), true); err != nil { - return fmt.Errorf("failed to enable ipv6: %v", err) - } - ipAddr := &netlink.Addr{IPNet: i.AddressIPv6(), Label: "", Flags: syscall.IFA_F_NODAD} - return nlh.AddrAdd(iface, ipAddr) + nlAddr := &netlink.Addr{IPNet: addr, Label: "", Flags: syscall.IFA_F_NODAD} + return nlh.AddrAdd(iface, nlAddr) } func setInterfaceLinkLocalIPs(nlh *netlink.Handle, iface netlink.Link, i *Interface) error {