Skip to content

Commit 0b42963

Browse files
itzgCopilot
andauthored
Fix Docker IPv6 route discovery for IPAM networks (#575)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent eb1cfd9 commit 0b42963

3 files changed

Lines changed: 178 additions & 6 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
services:
2+
mc:
3+
image: itzg/minecraft-server
4+
environment:
5+
EULA: "TRUE"
6+
TYPE: PAPER
7+
VERSION: 1.20.1
8+
PREFER_IPV6: true
9+
labels:
10+
mc-router.host: "localhost"
11+
networks:
12+
- only-ipv6
13+
mc-router:
14+
image: itzg/mc-router
15+
environment:
16+
IN_DOCKER: true
17+
networks:
18+
- only-ipv6
19+
ports:
20+
- "25565:25565"
21+
volumes:
22+
- /var/run/docker.sock:/var/run/docker.sock:ro
23+
networks:
24+
only-ipv6:
25+
driver: bridge
26+
enable_ipv6: true
27+
# https://docs.docker.com/reference/compose-file/networks/#enable_ipv4
28+
enable_ipv4: false

server/docker.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/docker/docker/api/types/container"
1414
"github.com/docker/docker/api/types/events"
1515
"github.com/docker/docker/api/types/filters"
16+
"github.com/docker/docker/api/types/network"
1617
"github.com/docker/docker/client"
1718
"github.com/sirupsen/logrus"
1819
)
@@ -267,7 +268,7 @@ func (w *dockerWatcherImpl) containersForID(ctx context.Context, containerID str
267268
}
268269
endpoint := ""
269270
if !data.notRunning {
270-
endpoint = fmt.Sprintf("%s:%d", data.ip, data.port)
271+
endpoint = dockerBackendEndpoint(data.ip, data.port)
271272
}
272273
var result []*routableContainer
273274
for _, host := range data.hosts {
@@ -483,7 +484,7 @@ func (w *dockerWatcherImpl) listContainers(ctx context.Context) ([]*routableCont
483484

484485
endpoint := ""
485486
if !data.notRunning {
486-
endpoint = fmt.Sprintf("%s:%d", data.ip, data.port)
487+
endpoint = dockerBackendEndpoint(data.ip, data.port)
487488
}
488489
logrus.WithField("backendEndpoint", endpoint).
489490
WithField("containerID", container.ID).
@@ -529,6 +530,20 @@ type parsedDockerContainerData struct {
529530
notRunning bool
530531
}
531532

533+
func dockerEndpointIP(endpoint *network.EndpointSettings) string {
534+
if endpoint == nil {
535+
return ""
536+
}
537+
if endpoint.IPAddress != "" {
538+
return endpoint.IPAddress
539+
}
540+
return endpoint.GlobalIPv6Address
541+
}
542+
543+
func dockerBackendEndpoint(ip string, port uint64) string {
544+
return net.JoinHostPort(ip, strconv.FormatUint(port, 10))
545+
}
546+
532547
func (w *dockerWatcherImpl) parseContainerData(container *container.InspectResponse) (data parsedDockerContainerData, ok bool) {
533548
data.autoScaleUp = w.config.autoScaleUp
534549
data.autoScaleDown = w.config.autoScaleDown
@@ -629,17 +644,17 @@ func (w *dockerWatcherImpl) parseContainerData(container *container.InspectRespo
629644
// specified network
630645
for name, endpoint := range container.NetworkSettings.Networks {
631646
if name == endpoint.NetworkID {
632-
data.ip = endpoint.IPAddress
647+
data.ip = dockerEndpointIP(endpoint)
633648
}
634649

635650
if name == *data.network {
636-
data.ip = endpoint.IPAddress
651+
data.ip = dockerEndpointIP(endpoint)
637652
break
638653
}
639654

640655
for _, alias := range endpoint.Aliases {
641656
if alias == name {
642-
data.ip = endpoint.IPAddress
657+
data.ip = dockerEndpointIP(endpoint)
643658
break
644659
}
645660
}
@@ -655,7 +670,7 @@ func (w *dockerWatcherImpl) parseContainerData(container *container.InspectRespo
655670
}
656671

657672
for _, endpoint := range container.NetworkSettings.Networks {
658-
data.ip = endpoint.IPAddress
673+
data.ip = dockerEndpointIP(endpoint)
659674
break
660675
}
661676
}

server/docker_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package server
2+
3+
import (
4+
"testing"
5+
6+
"github.com/docker/docker/api/types/container"
7+
"github.com/docker/docker/api/types/network"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestDockerEndpointIP(t *testing.T) {
13+
t.Run("prefers IPv4 when both exist", func(t *testing.T) {
14+
endpoint := &network.EndpointSettings{
15+
IPAddress: "10.0.0.10",
16+
GlobalIPv6Address: "fd00::10",
17+
}
18+
assert.Equal(t, "10.0.0.10", dockerEndpointIP(endpoint))
19+
})
20+
21+
t.Run("falls back to IPv6 when IPv4 absent", func(t *testing.T) {
22+
endpoint := &network.EndpointSettings{
23+
GlobalIPv6Address: "fd00::20",
24+
}
25+
assert.Equal(t, "fd00::20", dockerEndpointIP(endpoint))
26+
})
27+
28+
t.Run("returns empty when endpoint missing", func(t *testing.T) {
29+
assert.Empty(t, dockerEndpointIP(nil))
30+
})
31+
}
32+
33+
func TestDockerBackendEndpoint(t *testing.T) {
34+
assert.Equal(t, "10.0.0.10:25565", dockerBackendEndpoint("10.0.0.10", 25565))
35+
assert.Equal(t, "[fd00::20]:25565", dockerBackendEndpoint("fd00::20", 25565))
36+
}
37+
38+
func TestDockerWatcherParseContainerData_IPSelection(t *testing.T) {
39+
makeInspect := func(labels map[string]string, networks map[string]*network.EndpointSettings) *container.InspectResponse {
40+
return &container.InspectResponse{
41+
Config: &container.Config{
42+
Labels: labels,
43+
},
44+
NetworkSettings: &container.NetworkSettings{
45+
Networks: networks,
46+
},
47+
ContainerJSONBase: &container.ContainerJSONBase{
48+
ID: "abc123",
49+
Name: "example",
50+
State: &container.State{
51+
Running: true,
52+
},
53+
},
54+
}
55+
}
56+
57+
w := &dockerWatcherImpl{}
58+
59+
t.Run("uses ipv4 for single-network container", func(t *testing.T) {
60+
inspect := makeInspect(
61+
map[string]string{
62+
DockerRouterLabelHost: "mc.example.com",
63+
},
64+
map[string]*network.EndpointSettings{
65+
"v6net": {
66+
IPAddress: "172.20.0.2",
67+
GlobalIPv6Address: "fd00::2",
68+
},
69+
},
70+
)
71+
72+
data, ok := w.parseContainerData(inspect)
73+
require.True(t, ok)
74+
assert.Equal(t, "172.20.0.2", data.ip)
75+
assert.Equal(t, uint64(25565), data.port)
76+
})
77+
78+
t.Run("uses ipv6 for single-network container when ipv4 absent", func(t *testing.T) {
79+
inspect := makeInspect(
80+
map[string]string{
81+
DockerRouterLabelHost: "mc.example.com",
82+
},
83+
map[string]*network.EndpointSettings{
84+
"v6net": {
85+
GlobalIPv6Address: "fd00::2",
86+
},
87+
},
88+
)
89+
90+
data, ok := w.parseContainerData(inspect)
91+
require.True(t, ok)
92+
assert.Equal(t, "fd00::2", data.ip)
93+
})
94+
95+
t.Run("uses ipv6 for selected network when network label is set", func(t *testing.T) {
96+
inspect := makeInspect(
97+
map[string]string{
98+
DockerRouterLabelHost: "mc.example.com",
99+
DockerRouterLabelNetwork: "v6net",
100+
},
101+
map[string]*network.EndpointSettings{
102+
"other": {
103+
IPAddress: "172.20.0.5",
104+
},
105+
"v6net": {
106+
GlobalIPv6Address: "fd00::6",
107+
},
108+
},
109+
)
110+
111+
data, ok := w.parseContainerData(inspect)
112+
require.True(t, ok)
113+
assert.Equal(t, "fd00::6", data.ip)
114+
})
115+
116+
t.Run("rejects container with no routable ip while running", func(t *testing.T) {
117+
inspect := makeInspect(
118+
map[string]string{
119+
DockerRouterLabelHost: "mc.example.com",
120+
},
121+
map[string]*network.EndpointSettings{
122+
"v6net": {},
123+
},
124+
)
125+
126+
_, ok := w.parseContainerData(inspect)
127+
assert.False(t, ok)
128+
})
129+
}

0 commit comments

Comments
 (0)