Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 6d1c268

Browse filesBrowse files
committed
Release IPv6 address if unused due to sysctl setting
When running: docker network create --ipv6 b46 docker run --rm -ti \ --network name=b46,driver-opt=com.docker.network.endpoint.sysctls=net.ipv6.conf.IFNAME.disable_ipv6=1 \ busybox IPv6 is enabled in the container and the network, so an IPv6 address will be allocated for the endpoint. But, when the sysctl is applied, the IPv6 address will be removed from the interface ... so, no unsolicited neighbour advertisement should be (or can be) sent and, the endpoint should not be treated as dual-stack when selecting a gateway endpoint and, if it is selected as the gateway endpoint, setting up an IPv6 route via the network will fail. So, if the IPv6 address disappears after sysctls have been applied, release the address and remove it from the endpoint's config. TODO ... - hosts entries - legacy links have old address (which may get recycled, so possibly harmful to have it left around in iptables and hosts). - maybe bail out of Endpoint.sbJoin if osSbox==nil before setting up addresses for hosts, DNS etc - and only do that stuff during SetKey, after successfully configuring the interface with an IPv6 address? Signed-off-by: Rob Murray <rob.murray@docker.com>
1 parent 1021211 commit 6d1c268
Copy full SHA for 6d1c268

11 files changed

+228-18Lines changed: 228 additions & 18 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎daemon/container_operations.go‎

Copy file name to clipboardExpand all lines: daemon/container_operations.go
+18-4Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -736,11 +736,18 @@ func (daemon *Daemon) connectToNetwork(ctx context.Context, cfg *config.Config,
736736

737737
delete(ctr.NetworkSettings.Networks, n.ID())
738738

739-
if err := daemon.updateEndpointNetworkSettings(cfg, ctr, n, ep); err != nil {
740-
return err
741-
}
742-
743739
if nwName == network.DefaultNetwork {
740+
// Legacy links must be prepared before the Endpoint.Join, because the network
741+
// driver needs info about them - and, the daemon's network settings need to be
742+
// filled-in to do that prep. So, do both here.
743+
//
744+
// However, this means if the Endpoint.Join drops the endpoint's IPv6 address
745+
// (because there's a sysctl setting or some equivalent disabling IPv6 on the
746+
// interface), host entries for IPv6 addresses of the linked container will be
747+
// left behind in the container's /etc/hosts file.
748+
if err := daemon.updateEndpointNetworkSettings(cfg, ctr, n, ep); err != nil {
749+
return err
750+
}
744751
if err := daemon.addLegacyLinks(ctx, cfg, ctr, endpointConfig, sb); err != nil {
745752
return err
746753
}
@@ -751,10 +758,17 @@ func (daemon *Daemon) connectToNetwork(ctx context.Context, cfg *config.Config,
751758
return err
752759
}
753760

761+
// Connect the container to the network. Note that this will release the IPv6
762+
// address assigned to the Endpoint, if IPv6 is disabled on the interface
763+
// (probably by an endpoint specific sysctl setting).
754764
if err := ep.Join(ctx, sb, joinOptions...); err != nil {
755765
return err
756766
}
757767

768+
if err := daemon.updateEndpointNetworkSettings(cfg, ctr, n, ep); err != nil {
769+
return err
770+
}
771+
758772
if !ctr.Managed {
759773
// add container name/alias to DNS
760774
if err := daemon.ActivateContainerServiceBinding(ctr.Name); err != nil {
Collapse file

‎daemon/network.go‎

Copy file name to clipboardExpand all lines: daemon/network.go
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,10 @@ func buildEndpointInfo(networkSettings *network.Settings, n *libnetwork.Network,
11021102
onesv6, _ := iface.AddressIPv6().Mask.Size()
11031103
networkSettings.Networks[nwName].GlobalIPv6Address = iface.AddressIPv6().IP.String()
11041104
networkSettings.Networks[nwName].GlobalIPv6PrefixLen = onesv6
1105+
} else {
1106+
// If IPv6 was disabled on the interface, and its address was removed, remove it here too.
1107+
networkSettings.Networks[nwName].GlobalIPv6Address = ""
1108+
networkSettings.Networks[nwName].GlobalIPv6PrefixLen = 0
11051109
}
11061110

11071111
return nil
Collapse file

‎integration/networking/bridge_linux_test.go‎

Copy file name to clipboardExpand all lines: integration/networking/bridge_linux_test.go
+81-4Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/google/go-cmp/cmp/cmpopts"
3232
"gotest.tools/v3/assert"
3333
is "gotest.tools/v3/assert/cmp"
34+
"gotest.tools/v3/icmd"
3435
"gotest.tools/v3/skip"
3536
)
3637

@@ -777,6 +778,79 @@ func TestDisableIPv6Addrs(t *testing.T) {
777778
}
778779
}
779780

781+
// TestDisableIPv6OnInterface checks that it's possible to disable IPv6 on an
782+
// endpoint in an IPv6 network using a sysctl.
783+
func TestDisableIPv6OnInterface(t *testing.T) {
784+
ctx := setupTest(t)
785+
d := daemon.New(t)
786+
d.StartWithBusybox(ctx, t, "--ipv6")
787+
defer d.Stop(t)
788+
789+
c := d.NewClientT(t)
790+
defer c.Close()
791+
792+
tests := []struct {
793+
name string
794+
netName string
795+
}{
796+
{
797+
name: "default bridge",
798+
netName: "bridge",
799+
},
800+
{
801+
name: "user defined bridge",
802+
netName: "testnet",
803+
},
804+
}
805+
806+
for _, tc := range tests {
807+
t.Run(tc.netName, func(t *testing.T) {
808+
if tc.netName != "bridge" {
809+
network.CreateNoError(ctx, t, c, tc.netName, network.WithIPv6())
810+
defer network.RemoveNoError(ctx, t, c, tc.netName)
811+
}
812+
813+
const ctrName = "ctr"
814+
ctrId := container.Run(ctx, t, c,
815+
container.WithName(ctrName),
816+
container.WithNetworkMode(tc.netName),
817+
container.WithExposedPorts("80/tcp"),
818+
container.WithPortMap(nat.PortMap{"80/tcp": {{HostPort: "8080"}}}),
819+
container.WithEndpointSettings(tc.netName, &networktypes.EndpointSettings{
820+
DriverOpts: map[string]string{
821+
netlabel.EndpointSysctls: "net.ipv6.conf.IFNAME.disable_ipv6=1",
822+
},
823+
}),
824+
)
825+
defer c.ContainerRemove(ctx, ctrId, containertypes.RemoveOptions{Force: true})
826+
827+
// The interface should not have any IPv6 addresses.
828+
execRes := container.ExecT(ctx, t, c, ctrId, []string{"ip", "a", "show", "eth0"})
829+
assert.Check(t, !strings.Contains(execRes.Stdout(), "inet6"),
830+
"Unexpected IPv6 address in: %s", execRes.Stdout())
831+
832+
// Inspect should not show an IPv6 container address.
833+
inspRes2 := container.Inspect(ctx, t, c, ctrId)
834+
assert.Check(t, is.Equal("", inspRes2.NetworkSettings.Networks[tc.netName].GlobalIPv6Address))
835+
assert.Check(t, is.Equal(0, inspRes2.NetworkSettings.Networks[tc.netName].GlobalIPv6PrefixLen))
836+
837+
// Port mappings should be IPv4-only.
838+
checkProxies(ctx, t, c, d.Pid(), []expProxyCfg{
839+
{"tcp", "0.0.0.0", "8080", ctrName, tc.netName, true, "80"},
840+
{"tcp", "::", "8080", ctrName, tc.netName, true, "80"},
841+
})
842+
843+
// There should not be an IPv6 DNS or /etc/hosts entry.
844+
runRes := container.RunAttach(ctx, t, c,
845+
container.WithNetworkMode(tc.netName),
846+
container.WithCmd("ping", "-6", ctrName),
847+
)
848+
assert.Check(t, is.Equal(runRes.ExitCode, 1))
849+
assert.Check(t, is.Contains(runRes.Stderr.String(), "bad address"))
850+
})
851+
}
852+
}
853+
780854
// Check that a container in a network with IPv4 disabled doesn't get
781855
// IPv4 addresses.
782856
func TestDisableIPv4(t *testing.T) {
@@ -1234,9 +1308,12 @@ func checkProxies(ctx context.Context, t *testing.T, c *client.Client, daemonPid
12341308
}
12351309

12361310
gotProxies := make([]string, len(exp))
1237-
res, err := exec.Command("ps", "-f", "--ppid", strconv.Itoa(daemonPid)).CombinedOutput()
1238-
assert.NilError(t, err)
1239-
for _, line := range strings.Split(string(res), "\n") {
1311+
res := icmd.RunCommand("ps", "-f", "--ppid", strconv.Itoa(daemonPid))
1312+
if res.Error != nil {
1313+
t.Error(res)
1314+
return
1315+
}
1316+
for _, line := range strings.Split(res.Stdout(), "\n") {
12401317
_, args, ok := strings.Cut(line, "docker-proxy")
12411318
if !ok {
12421319
continue
@@ -1254,7 +1331,7 @@ func checkProxies(ctx context.Context, t *testing.T, c *client.Client, daemonPid
12541331
gotProxies = append(gotProxies, makeExpStr(proto, hostIP, hostPort, ctrIP, ctrPort))
12551332
}
12561333

1257-
assert.DeepEqual(t, gotProxies, wantProxies)
1334+
assert.Check(t, is.DeepEqual(gotProxies, wantProxies))
12581335
}
12591336

12601337
// Check that a gratuitous ARP / neighbour advertisement is sent for a new
Collapse file

‎libnetwork/drivers/bridge/bridge_linux.go‎

Copy file name to clipboardExpand all lines: libnetwork/drivers/bridge/bridge_linux.go
+13Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ type connectivityConfiguration struct {
111111
PortBindings []types.PortBinding
112112
ExposedPorts []types.TransportPort
113113
NoProxy6To4 bool
114+
DropIPv6 bool
114115
}
115116

116117
type bridgeEndpoint struct {
@@ -1452,6 +1453,10 @@ func (d *driver) ProgramExternalConnectivity(ctx context.Context, nid, eid strin
14521453
return err
14531454
}
14541455

1456+
if endpoint.extConnConfig.DropIPv6 {
1457+
endpoint.addrv6 = nil
1458+
}
1459+
14551460
// Program any required port mapping and store them in the endpoint
14561461
if endpoint.extConnConfig != nil && endpoint.extConnConfig.PortBindings != nil {
14571462
endpoint.portMapping, err = network.addPortMappings(
@@ -1670,5 +1675,13 @@ func parseConnectivityOptions(cOptions map[string]interface{}) (*connectivityCon
16701675
}
16711676
}
16721677

1678+
if opt, ok := cOptions[netlabel.NoIPv6]; ok {
1679+
if dropIPv6, ok := opt.(bool); ok {
1680+
cc.DropIPv6 = dropIPv6
1681+
} else {
1682+
return nil, types.InvalidParameterErrorf("invalid "+netlabel.NoIPv6+" in connectivity configuration: %v", opt)
1683+
}
1684+
}
1685+
16731686
return cc, nil
16741687
}
Collapse file

‎libnetwork/endpoint.go‎

Copy file name to clipboardExpand all lines: libnetwork/endpoint.go
+63-5Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -559,14 +559,14 @@ func (ep *Endpoint) sbJoin(ctx context.Context, sb *Sandbox, options ...Endpoint
559559
}
560560
}
561561

562+
// Current endpoint(s) providing external connectivity for the sandbox
563+
gwepBefore4, gwepBefore6 := sb.getGatewayEndpoint()
564+
562565
sb.addHostsEntries(ctx, ep.getEtcHostsAddrs())
563566
if err := sb.updateDNS(n.enableIPv6); err != nil {
564567
return err
565568
}
566569

567-
// Current endpoint(s) providing external connectivity for the sandbox
568-
gwepBefore4, gwepBefore6 := sb.getGatewayEndpoint()
569-
570570
sb.addEndpoint(ep)
571571
defer func() {
572572
if retErr != nil {
@@ -699,6 +699,9 @@ func (ep *Endpoint) sbJoin(ctx context.Context, sb *Sandbox, options ...Endpoint
699699
log.G(ctx).Debugf("Programming external connectivity on endpoint")
700700
labels := sb.Labels()
701701
labels[netlabel.NoProxy6To4] = noProxy6To4After
702+
if ep.iface.addrv6 == nil {
703+
labels[netlabel.NoIPv6] = true
704+
}
702705
if err := d.ProgramExternalConnectivity(ctx, n.ID(), ep.ID(), labels); err != nil {
703706
return errdefs.System(fmt.Errorf(
704707
"driver failed programming external connectivity on endpoint %s (%s): %v",
@@ -1045,7 +1048,7 @@ func (ep *Endpoint) Delete(ctx context.Context, force bool) error {
10451048
return err
10461049
}
10471050

1048-
ep.releaseAddress()
1051+
ep.releaseIPAddresses()
10491052

10501053
if err := n.getEpCnt().DecEndpointCnt(); err != nil {
10511054
log.G(ctx).Warnf("failed to decrement endpoint count for ep %s: %v", ep.ID(), err)
@@ -1334,7 +1337,7 @@ func (ep *Endpoint) assignAddressVersion(ipVer int, ipam ipamapi.Ipam) error {
13341337
return fmt.Errorf("no available IPv%d addresses on this network's address pools: %s (%s)", ipVer, n.Name(), n.ID())
13351338
}
13361339

1337-
func (ep *Endpoint) releaseAddress() {
1340+
func (ep *Endpoint) releaseIPAddresses() {
13381341
n := ep.getNetwork()
13391342
if n.hasSpecialDriver() {
13401343
return
@@ -1361,6 +1364,61 @@ func (ep *Endpoint) releaseAddress() {
13611364
}
13621365
}
13631366

1367+
func (ep *Endpoint) releaseIPv6Address(ctx context.Context) (*endpointJoinInfo, error) {
1368+
// TODO(robmry) - locking needs sorting out, some of this isn't necessary and *joinInfo
1369+
// is accessed without the lock being held. But, for now, stay consistent with other uses.
1370+
ep.mu.Lock()
1371+
joinInfo := ep.joinInfo
1372+
addrv6 := ep.iface.addrv6
1373+
v6PoolId := ep.iface.v6PoolID
1374+
network := ep.network
1375+
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(log.Fields{
1376+
"net": network.Name(),
1377+
"ep": ep.name,
1378+
"ip": ep.iface.addrv6,
1379+
}))
1380+
ep.mu.Unlock()
1381+
1382+
if addrv6 == nil || network.hasSpecialDriver() {
1383+
return joinInfo, nil
1384+
}
1385+
1386+
log.G(ctx).Debug("Releasing IPv6 address for endpoint")
1387+
1388+
ipam, _, err := network.getController().getIPAMDriver(network.ipamType)
1389+
if err != nil {
1390+
log.G(ctx).WithError(err).Warn("Failed to retrieve ipam driver to release IPv6 address")
1391+
return nil, err
1392+
}
1393+
1394+
if err := ipam.ReleaseAddress(v6PoolId, addrv6.IP); err != nil {
1395+
log.G(ctx).WithError(err).Warn("Failed to release IPv6 address")
1396+
return nil, err
1397+
}
1398+
1399+
ep.mu.Lock()
1400+
ep.iface.addrv6 = nil
1401+
if ep.joinInfo != nil {
1402+
ep.joinInfo.gw6 = nil
1403+
}
1404+
ep.mu.Unlock()
1405+
1406+
if err := ep.network.getController().updateToStore(ctx, ep); err != nil {
1407+
return nil, err
1408+
}
1409+
1410+
serviceID := ep.svcID
1411+
if serviceID == "" {
1412+
serviceID = ep.ID()
1413+
}
1414+
for i, dnsName := range ep.getDNSNames() {
1415+
ipMapUpdate := i == 0 // ipMapUpdate indicates whether PTR records should be updated.
1416+
network.deleteSvcRecords(ep.ID(), dnsName, serviceID, nil, addrv6.IP, ipMapUpdate, "releaseIPv6Address")
1417+
}
1418+
1419+
return joinInfo, nil
1420+
}
1421+
13641422
func (c *Controller) cleanupLocalEndpoints() error {
13651423
// Get used endpoints
13661424
eps := make(map[string]interface{})
Collapse file

‎libnetwork/netlabel/labels.go‎

Copy file name to clipboardExpand all lines: libnetwork/netlabel/labels.go
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,11 @@ const (
7070
// container, when the default binding address is 0.0.0.0. This label
7171
// is intended for internal use, it may be removed in a future release.
7272
NoProxy6To4 = DriverPrivatePrefix + ".no_proxy_6to4"
73+
74+
// NoIPv6 can be used as a ProgramExternalConnectivity option to tell the
75+
// driver that, even though CreateEndpoint was supplied an IPv6 address, when the
76+
// endpoint was configured it lost its IPv6 address (probably because of a sysctl
77+
// setting). This label is intended for internal use, it may be removed in a
78+
// future release.
79+
NoIPv6 = DriverPrivatePrefix + ".no_ipv6"
7380
)
Collapse file

‎libnetwork/network.go‎

Copy file name to clipboardExpand all lines: libnetwork/network.go
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1233,7 +1233,7 @@ func (n *Network) createEndpoint(ctx context.Context, name string, options ...En
12331233
}
12341234
defer func() {
12351235
if err != nil {
1236-
ep.releaseAddress()
1236+
ep.releaseIPAddresses()
12371237
}
12381238
}()
12391239

Collapse file

‎libnetwork/osl/interface_linux.go‎

Copy file name to clipboardExpand all lines: libnetwork/osl/interface_linux.go
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ func moveLink(ctx context.Context, nlhHost nlwrap.Handle, iface netlink.Link, i
225225
// interface according to the specified settings. The caller is expected
226226
// to only provide a prefix for DstName. The AddInterface api will auto-generate
227227
// an appropriate suffix for the DstName to disambiguate.
228+
// If an IPv6 address is configured, but unused because of sysctl settings applied
229+
// after address assignment, it will be removed from the Interface.
228230
func (n *Namespace) AddInterface(ctx context.Context, srcName, dstPrefix string, options ...IfaceOption) error {
229231
ctx, span := otel.Tracer("").Start(ctx, "libnetwork.osl.AddInterface", trace.WithAttributes(
230232
attribute.String("srcName", srcName),
@@ -641,6 +643,23 @@ func (n *Namespace) configureInterface(ctx context.Context, nlh nlwrap.Handle, i
641643
return err
642644
}
643645

646+
// If an IPv6 address was configured, and now it's gone away, it's because of a sysctl
647+
// setting. Remove the address from the Interface so that there's no attempt to send
648+
// Neighbour Advertisements for it, and the caller knows to release the address.
649+
if i.addressIPv6 != nil {
650+
v6addrs, err := nlh.AddrList(iface, netlink.FAMILY_V6)
651+
if err != nil {
652+
return fmt.Errorf("failed to check IPv6 addresses: %v", err)
653+
}
654+
if len(v6addrs) == 0 {
655+
log.G(ctx).WithFields(log.Fields{
656+
"ip": i.addressIPv6.String(),
657+
"ifname": i.dstName,
658+
}).Debug("IPv6 address not present after applying sysctls")
659+
i.addressIPv6 = nil
660+
}
661+
}
662+
644663
return nil
645664
}
646665

Collapse file

‎libnetwork/osl/namespace_linux.go‎

Copy file name to clipboardExpand all lines: libnetwork/osl/namespace_linux.go
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ func (n *Namespace) Interfaces() []*Interface {
251251
return ifaces
252252
}
253253

254-
func (n *Namespace) ifaceBySrcName(srcName string) *Interface {
254+
// GetInterface returns a pointer to the Interface with a matching srcName, else nil.
255+
func (n *Namespace) GetInterface(srcName string) *Interface {
255256
n.mu.Lock()
256257
defer n.mu.Unlock()
257258
for _, iface := range n.iFaces {
@@ -518,7 +519,6 @@ func (n *Namespace) IPv6LoEnabled() bool {
518519
func (n *Namespace) RefreshIPv6LoEnabled() {
519520
n.mu.Lock()
520521
defer n.mu.Unlock()
521-
522522
// If anything goes wrong, assume no-IPv6.
523523
n.ipv6LoEnabledCached = false
524524
iface, err := n.nlHandle.LinkByName("lo")
Collapse file

‎libnetwork/osl/route_linux.go‎

Copy file name to clipboardExpand all lines: libnetwork/osl/route_linux.go
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ func (n *Namespace) SetDefaultRouteIPv6(srcName string) error {
241241
}
242242

243243
func (n *Namespace) setDefaultRoute(srcName string, routeMatcher func(*net.IPNet) bool) error {
244-
iface := n.ifaceBySrcName(srcName)
244+
iface := n.GetInterface(srcName)
245245
if iface == nil {
246246
return fmt.Errorf("no interface")
247247
}
@@ -309,7 +309,7 @@ func (n *Namespace) unsetDefaultRoute(srcName string, routeMatcher func(*net.IPN
309309
return nil
310310
}
311311

312-
iface := n.ifaceBySrcName(srcName)
312+
iface := n.GetInterface(srcName)
313313
if iface == nil {
314314
return nil
315315
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.