Skip to content

Commit 56eb47c

Browse files
committed
Ignore kernel-assigned LL addrs when selecting "bip6"
Commit facb232 aligned the way the default bridge's IPv6 subnet and gateway addresses are selected with IPv4. Part of that involved looking at addresses already on the bridge, along with daemon config options. But, for IPv6, the kernel will assign a link-local address to the bridge. Make sure that address is ignored when selecting "bip6" when it's not explicitly specified. This is made slightly complicated because we allow fixed-cidr-v6 to be a link-local subnet (either the standard "fe80::/64", or any other non-overlapping LL subnet in "fe80::/10"). Following this change, if fixed-cidr-v6 is (or is included by) "fe80::/64", the bridge's kernel-assigned LL address may be used as the network's gateway address - even though it may also get an IPAM-assigned LL address. Signed-off-by: Rob Murray <[email protected]>
1 parent 8fee8a7 commit 56eb47c

2 files changed

Lines changed: 108 additions & 4 deletions

File tree

daemon/daemon_unix.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"path/filepath"
1313
"runtime"
1414
"runtime/debug"
15+
"slices"
1516
"strconv"
1617
"strings"
1718
"sync"
@@ -1190,6 +1191,15 @@ func selectBIP(
11901191
if err != nil {
11911192
return nil, nil, errors.Wrap(err, "list bridge addresses failed")
11921193
}
1194+
// For IPv6, ignore the kernel-assigned link-local address. Remove all
1195+
// link-local addresses, unless fixed-cidr-v6 has the standard link-local
1196+
// prefix. (If fixed-cidr-v6 is the standard LL prefix, the kernel-assigned
1197+
// address is likely to be used instead of an IPAM assigned address.)
1198+
if family == netlink.FAMILY_V6 && (fCidrIP == nil || !isStandardLL(fCidrIP)) {
1199+
bridgeNws = slices.DeleteFunc(bridgeNws, func(nlAddr netlink.Addr) bool {
1200+
return isStandardLL(nlAddr.IP)
1201+
})
1202+
}
11931203
if len(bridgeNws) > 0 {
11941204
// Pick any address from the bridge as a starting point.
11951205
nw := bridgeNws[0].IPNet
@@ -1238,6 +1248,16 @@ func selectBIP(
12381248
return bIP, bIPNet, nil
12391249
}
12401250

1251+
// isStandardLL returns true if ip is in fe80::/64 (the link local prefix is fe80::/10,
1252+
// but only fe80::/64 is normally used - however, it's possible to ask IPAM for a
1253+
// link-local subnet that's outside that range).
1254+
func isStandardLL(ip net.IP) bool {
1255+
if ip == nil {
1256+
return false
1257+
}
1258+
return ip.Mask(net.CIDRMask(64, 128)).Equal(net.ParseIP("fe80::"))
1259+
}
1260+
12411261
// Remove default bridge interface if present (--bridge=none use case)
12421262
func removeDefaultBridgeInterface() {
12431263
if lnk, err := nlwrap.LinkByName(bridge.DefaultBridgeName); err == nil {

integration/daemon/daemon_linux_test.go

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"net"
66
"net/netip"
7+
"slices"
78
"testing"
89

910
"github.com/docker/docker/api/types/network"
@@ -14,6 +15,7 @@ import (
1415
"github.com/vishvananda/netlink"
1516
"gotest.tools/v3/assert"
1617
is "gotest.tools/v3/assert/cmp"
18+
"gotest.tools/v3/poll"
1719
"gotest.tools/v3/skip"
1820
)
1921

@@ -111,6 +113,29 @@ func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) {
111113
{Subnet: "fdd1:8161:2d2c::/56", IPRange: "fdd1:8161:2d2c::/64", Gateway: "fdd1:8161:2d2c::8888"},
112114
},
113115
},
116+
{
117+
name: "link-local fixed-cidr-v6",
118+
daemonArgs: []string{
119+
"--fixed-cidr", "192.168.176.0/24",
120+
"--fixed-cidr-v6", "fe80::/64",
121+
},
122+
expIPAMConfig: []network.IPAMConfig{
123+
{Subnet: "192.168.176.0/24", IPRange: "192.168.176.0/24"},
124+
{Subnet: "fe80::/64", IPRange: "fe80::/64", Gateway: llGwPlaceholder},
125+
},
126+
},
127+
{
128+
name: "nonstandard link-local fixed-cidr-v6",
129+
initialBridgeAddrs: []string{"192.168.176.88/20", "fe80:1234::88/56"},
130+
daemonArgs: []string{
131+
"--fixed-cidr", "192.168.176.0/24",
132+
"--fixed-cidr-v6", "fe80:1234::/64",
133+
},
134+
expIPAMConfig: []network.IPAMConfig{
135+
{Subnet: "192.168.176.0/20", IPRange: "192.168.176.0/24", Gateway: "192.168.176.88"},
136+
{Subnet: "fe80:1234::/56", IPRange: "fe80:1234::/64", Gateway: "fe80:1234::88"},
137+
},
138+
},
114139
{
115140
name: "fixed-cidr within old bridge subnet with new bip",
116141
initialBridgeAddrs: []string{"192.168.176.88/20", "fdd1:8161:2d2c::/56"},
@@ -233,6 +258,30 @@ func TestDaemonDefaultBridgeIPAM_UserBr(t *testing.T) {
233258
{Subnet: "fdd1:8161:2d2c:10::/60", IPRange: "fdd1:8161:2d2c:11::/64", Gateway: "fdd1:8161:2d2c:10::8888"},
234259
},
235260
},
261+
{
262+
name: "link-local fixed-cidr-v6",
263+
initialBridgeAddrs: []string{"192.168.176.88/20"},
264+
daemonArgs: []string{
265+
"--fixed-cidr", "192.168.176.0/24",
266+
"--fixed-cidr-v6", "fe80::/64",
267+
},
268+
expIPAMConfig: []network.IPAMConfig{
269+
{Subnet: "192.168.176.0/20", IPRange: "192.168.176.0/24", Gateway: "192.168.176.88"},
270+
{Subnet: "fe80::/64", IPRange: "fe80::/64", Gateway: llGwPlaceholder},
271+
},
272+
},
273+
{
274+
name: "nonstandard link-local fixed-cidr-v6",
275+
initialBridgeAddrs: []string{"192.168.176.88/20", "fe80:1234::88/56"},
276+
daemonArgs: []string{
277+
"--fixed-cidr", "192.168.176.0/24",
278+
"--fixed-cidr-v6", "fe80:1234::/64",
279+
},
280+
expIPAMConfig: []network.IPAMConfig{
281+
{Subnet: "192.168.176.0/20", IPRange: "192.168.176.0/24", Gateway: "192.168.176.88"},
282+
{Subnet: "fe80:1234::/56", IPRange: "fe80:1234::/64", Gateway: "fe80:1234::88"},
283+
},
284+
},
236285
{
237286
name: "fixed-cidr bigger than bridge subnet",
238287
initialBridgeAddrs: []string{"192.168.176.88/24"},
@@ -310,6 +359,11 @@ func TestDaemonDefaultBridgeIPAM_UserBr(t *testing.T) {
310359
}
311360
}
312361

362+
// llGwPlaceholder can be used as a value for "Gateway" in expected IPAM config,
363+
// before comparison with actual results it'll be replaced by the kernel assigned
364+
// link local IPv6 address for the bridge.
365+
const llGwPlaceholder = "ll-gateway-placeholder"
366+
313367
type defaultBridgeIPAMTestCase struct {
314368
name string
315369
bridgeName string
@@ -333,7 +387,7 @@ func testDefaultBridgeIPAM(ctx context.Context, t *testing.T, tc defaultBridgeIP
333387
defer cleanup()
334388

335389
host.Do(t, func() {
336-
createBridge(t, tc.bridgeName, tc.initialBridgeAddrs)
390+
llAddr := createBridge(t, tc.bridgeName, tc.initialBridgeAddrs)
337391

338392
var dArgs []string
339393
if !tc.ipv4Only {
@@ -365,7 +419,13 @@ func testDefaultBridgeIPAM(ctx context.Context, t *testing.T, tc defaultBridgeIP
365419

366420
insp, err := c.NetworkInspect(ctx, network.NetworkBridge, network.InspectOptions{})
367421
assert.NilError(t, err)
368-
assert.Check(t, is.DeepEqual(insp.IPAM.Config, tc.expIPAMConfig))
422+
expIPAMConfig := slices.Clone(tc.expIPAMConfig)
423+
for i := range expIPAMConfig {
424+
if expIPAMConfig[i].Gateway == llGwPlaceholder {
425+
expIPAMConfig[i].Gateway = llAddr.String()
426+
}
427+
}
428+
assert.Check(t, is.DeepEqual(insp.IPAM.Config, expIPAMConfig))
369429
})
370430
})
371431
}
@@ -386,7 +446,10 @@ func newHostInL3Seg(t *testing.T, name, ip4, ip6 string) (networking.Host, func(
386446
return l3.Hosts[hostname], func() { l3.Destroy(t) }
387447
}
388448

389-
func createBridge(t *testing.T, ifName string, addrs []string) {
449+
// createBridge creates a bridge device named ifName, brings it up, waits for
450+
// the kernel to assign a link-local IPv6 address, assigns addrs, and returns
451+
// the kernel-assigned LL address.
452+
func createBridge(t *testing.T, ifName string, addrs []string) net.IP {
390453
t.Helper()
391454

392455
// Get a netlink handle in this netns.
@@ -399,14 +462,35 @@ func createBridge(t *testing.T, ifName string, addrs []string) {
399462
Name: ifName,
400463
},
401464
}
402-
403465
err = nlh.LinkAdd(link)
404466
assert.NilError(t, err)
467+
468+
// Bring the interface up, and wait for the kernel to assign its link-local
469+
// address (to cause maximum confusion - the LL address shouldn't be selected
470+
// as "bip6").
471+
brLink, err := nlh.LinkByName(ifName)
472+
assert.NilError(t, err)
473+
err = nlh.LinkSetUp(brLink)
474+
assert.NilError(t, err)
475+
var llAddr net.IP
476+
poll.WaitOn(t, func(t poll.LogT) poll.Result {
477+
addrs, err := nlh.AddrList(brLink, netlink.FAMILY_V6)
478+
if err != nil {
479+
return poll.Error(err)
480+
}
481+
if len(addrs) == 0 {
482+
return poll.Continue("no IPv6 addresses")
483+
}
484+
llAddr = addrs[0].IP
485+
return poll.Success()
486+
})
487+
405488
for _, addr := range addrs {
406489
ip, ipNet, err := net.ParseCIDR(addr)
407490
assert.NilError(t, err)
408491
ipNet.IP = ip
409492
err = nlh.AddrAdd(link, &netlink.Addr{IPNet: ipNet})
410493
assert.NilError(t, err)
411494
}
495+
return llAddr
412496
}

0 commit comments

Comments
 (0)