Skip to content

Commit 1832774

Browse files
committed
Allow separate IPv4/IPv6 gateway endpoints.
A dual-stack endpoint still has priority when selecting a gateway Endpoint for a Sandbox. But, now there are IPv6-only networks, it is possible to have a Sandbox with only IPv4-only and IPv6-only endpoints. This change means they are both gateway endpoints. Tell the network driver it mustn't proxy host-IPv6 to endpoint-IPv4 when there's an IPv6 gateway endpoint (which may belong to a different net driver). Update that when networks are connected/disconnected. Signed-off-by: Rob Murray <[email protected]>
1 parent 869f799 commit 1832774

7 files changed

Lines changed: 391 additions & 96 deletions

File tree

integration/networking/bridge_linux_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package networking
22

33
import (
44
"context"
5+
"flag"
56
"fmt"
67
"os/exec"
78
"regexp"
9+
"strconv"
810
"strings"
911
"testing"
1012
"time"
@@ -14,10 +16,12 @@ import (
1416
"github.com/docker/docker/client"
1517
"github.com/docker/docker/integration/internal/container"
1618
"github.com/docker/docker/integration/internal/network"
19+
n "github.com/docker/docker/integration/network"
1720
"github.com/docker/docker/libnetwork/drivers/bridge"
1821
"github.com/docker/docker/libnetwork/netlabel"
1922
"github.com/docker/docker/testutil"
2023
"github.com/docker/docker/testutil/daemon"
24+
"github.com/docker/go-connections/nat"
2125
"github.com/google/go-cmp/cmp/cmpopts"
2226
"gotest.tools/v3/assert"
2327
is "gotest.tools/v3/assert/cmp"
@@ -951,3 +955,148 @@ func TestContainerDisabledIPv6(t *testing.T) {
951955
assert.Check(t, is.Equal(res.Stdout(), ""))
952956
assert.Check(t, is.Contains(res.Stderr(), "bad address"))
953957
}
958+
959+
type expProxyCfg struct {
960+
proto string
961+
hostIP string
962+
hostPort string
963+
ctrName string
964+
ctrNetName string
965+
ctrIPv4 bool
966+
ctrPort string
967+
}
968+
969+
func TestGatewaySelection(t *testing.T) {
970+
skip.If(t, testEnv.IsRootless, "proxies run in child namespace")
971+
972+
ctx := setupTest(t)
973+
d := daemon.New(t, daemon.WithExperimental())
974+
d.StartWithBusybox(ctx, t)
975+
defer d.Stop(t)
976+
c := d.NewClientT(t)
977+
defer c.Close()
978+
979+
const netName4 = "net4"
980+
network.CreateNoError(ctx, t, c, netName4)
981+
defer network.RemoveNoError(ctx, t, c, netName4)
982+
983+
const netName6 = "net6"
984+
netId6 := network.CreateNoError(ctx, t, c, netName6, network.WithIPv6(), network.WithIPv4(false))
985+
defer network.RemoveNoError(ctx, t, c, netName6)
986+
987+
const netName46 = "net46"
988+
netId46 := network.CreateNoError(ctx, t, c, netName46, network.WithIPv6())
989+
defer network.RemoveNoError(ctx, t, c, netName46)
990+
991+
master := "dm-dummy0"
992+
n.CreateMasterDummy(ctx, t, master)
993+
defer n.DeleteInterface(ctx, t, master)
994+
const netNameIpvlan6 = "ipvlan6"
995+
netIdIpvlan6 := network.CreateNoError(ctx, t, c, netNameIpvlan6,
996+
network.WithIPvlan("dm-dummy0", "l2"),
997+
network.WithIPv4(false),
998+
network.WithIPv6(),
999+
)
1000+
defer network.RemoveNoError(ctx, t, c, netNameIpvlan6)
1001+
1002+
const ctrName = "ctr"
1003+
ctrId := container.Run(ctx, t, c,
1004+
container.WithName(ctrName),
1005+
container.WithNetworkMode(netName4),
1006+
container.WithExposedPorts("80"),
1007+
container.WithPortMap(nat.PortMap{"80": {{HostPort: "8080"}}}),
1008+
container.WithCmd("httpd", "-f"),
1009+
)
1010+
defer c.ContainerRemove(ctx, ctrId, containertypes.RemoveOptions{Force: true})
1011+
1012+
// The container only has an IPv4 endpoint, it should be the gateway, and
1013+
// the host-IPv6 should be proxied to container-IPv4.
1014+
checkProxies(ctx, t, c, d.Pid(), []expProxyCfg{
1015+
{"tcp", "0.0.0.0", "8080", ctrName, netName4, true, "80"},
1016+
{"tcp", "::", "8080", ctrName, netName4, true, "80"},
1017+
})
1018+
1019+
// Connect the IPv6-only network. The IPv6 endpoint should become the
1020+
// gateway for IPv6, the IPv4 endpoint should be reconfigured as the
1021+
// gateway for IPv4 only.
1022+
err := c.NetworkConnect(ctx, netId6, ctrId, nil)
1023+
assert.NilError(t, err)
1024+
checkProxies(ctx, t, c, d.Pid(), []expProxyCfg{
1025+
{"tcp", "0.0.0.0", "8080", ctrName, netName4, true, "80"},
1026+
{"tcp", "::", "8080", ctrName, netName6, false, "80"},
1027+
})
1028+
1029+
// Disconnect the IPv6-only network, the IPv4 should get back the mapping
1030+
// from host-IPv6.
1031+
err = c.NetworkDisconnect(ctx, netId6, ctrId, false)
1032+
assert.NilError(t, err)
1033+
checkProxies(ctx, t, c, d.Pid(), []expProxyCfg{
1034+
{"tcp", "0.0.0.0", "8080", ctrName, netName4, true, "80"},
1035+
{"tcp", "::", "8080", ctrName, netName4, true, "80"},
1036+
})
1037+
1038+
// Connect the dual-stack network, it should become the gateway for v6 and v4.
1039+
err = c.NetworkConnect(ctx, netId46, ctrId, nil)
1040+
assert.NilError(t, err)
1041+
checkProxies(ctx, t, c, d.Pid(), []expProxyCfg{
1042+
{"tcp", "0.0.0.0", "8080", ctrName, netName46, true, "80"},
1043+
{"tcp", "::", "8080", ctrName, netName46, false, "80"},
1044+
})
1045+
1046+
// Go back to the IPv4-only gateway, with proxy from host IPv6.
1047+
err = c.NetworkDisconnect(ctx, netId46, ctrId, false)
1048+
assert.NilError(t, err)
1049+
checkProxies(ctx, t, c, d.Pid(), []expProxyCfg{
1050+
{"tcp", "0.0.0.0", "8080", ctrName, netName4, true, "80"},
1051+
{"tcp", "::", "8080", ctrName, netName4, true, "80"},
1052+
})
1053+
1054+
// Connect the IPv6-only ipvlan network, its new Endpoint should become the IPv6
1055+
// gateway, so the IPv4-only bridge is expected to drop its mapping from host IPv6.
1056+
err = c.NetworkConnect(ctx, netIdIpvlan6, ctrId, nil)
1057+
assert.NilError(t, err)
1058+
checkProxies(ctx, t, c, d.Pid(), []expProxyCfg{
1059+
{"tcp", "0.0.0.0", "8080", ctrName, netName4, true, "80"},
1060+
})
1061+
}
1062+
1063+
func checkProxies(ctx context.Context, t *testing.T, c *client.Client, daemonPid int, exp []expProxyCfg) {
1064+
t.Helper()
1065+
makeExpStr := func(proto, hostIP, hostPort, ctrIP, ctrPort string) string {
1066+
return fmt.Sprintf("%s:%s/%s <-> %s:%s", hostIP, hostPort, proto, ctrIP, ctrPort)
1067+
}
1068+
1069+
wantProxies := make([]string, len(exp))
1070+
for _, e := range exp {
1071+
inspect := container.Inspect(ctx, t, c, e.ctrName)
1072+
nw := inspect.NetworkSettings.Networks[e.ctrNetName]
1073+
ctrIP := nw.GlobalIPv6Address
1074+
if e.ctrIPv4 {
1075+
ctrIP = nw.IPAddress
1076+
}
1077+
wantProxies = append(wantProxies, makeExpStr(e.proto, e.hostIP, e.hostPort, ctrIP, e.ctrPort))
1078+
}
1079+
1080+
gotProxies := make([]string, len(exp))
1081+
res, err := exec.Command("ps", "-f", "--ppid", strconv.Itoa(daemonPid)).CombinedOutput()
1082+
assert.NilError(t, err)
1083+
for _, line := range strings.Split(string(res), "\n") {
1084+
_, args, ok := strings.Cut(line, "docker-proxy")
1085+
if !ok {
1086+
continue
1087+
}
1088+
var proto, hostIP, hostPort, ctrIP, ctrPort string
1089+
var useListenFd bool
1090+
fs := flag.NewFlagSet("docker-proxy", flag.ContinueOnError)
1091+
fs.StringVar(&proto, "proto", "", "Protocol")
1092+
fs.StringVar(&hostIP, "host-ip", "", "Host IP")
1093+
fs.StringVar(&hostPort, "host-port", "", "Host Port")
1094+
fs.StringVar(&ctrIP, "container-ip", "", "Container IP")
1095+
fs.StringVar(&ctrPort, "container-port", "", "Container Port")
1096+
fs.BoolVar(&useListenFd, "use-listen-fd", false, "Use listen fd")
1097+
fs.Parse(strings.Split(strings.TrimSpace(args), " "))
1098+
gotProxies = append(gotProxies, makeExpStr(proto, hostIP, hostPort, ctrIP, ctrPort))
1099+
}
1100+
1101+
assert.DeepEqual(t, gotProxies, wantProxies)
1102+
}

libnetwork/default_gateway.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,31 @@ func (c *Controller) defaultGwNetwork() (*Network, error) {
175175
return n, err
176176
}
177177

178-
// Returns the endpoint which is providing external connectivity to the sandbox
179-
func (sb *Sandbox) getGatewayEndpoint() *Endpoint {
180-
for _, ep := range sb.Endpoints() {
178+
// getGatewayEndpoint returns the endpoints providing external connectivity to
179+
// the sandbox. If the gateway is dual-stack, ep4 and ep6 will point at the same
180+
// endpoint. If there is no IPv4/IPv6 connectivity, nil pointers will be returned.
181+
func (sb *Sandbox) getGatewayEndpoint() (ep4, ep6 *Endpoint) {
182+
return selectGatewayEndpoint(sb.Endpoints())
183+
}
184+
185+
// selectGatewayEndpoint is like getGatewayEndpoint, but selects only from
186+
// endpoints.
187+
func selectGatewayEndpoint(endpoints []*Endpoint) (ep4, ep6 *Endpoint) {
188+
for _, ep := range endpoints {
181189
if ep.getNetwork().Type() == "null" || ep.getNetwork().Type() == "host" {
182190
continue
183191
}
184-
if len(ep.Gateway()) != 0 {
185-
return ep
192+
gw4 := len(ep.Gateway()) != 0
193+
gw6 := len(ep.GatewayIPv6()) != 0
194+
if gw4 && gw6 {
195+
return ep, ep
196+
}
197+
if gw4 && ep4 == nil {
198+
ep4 = ep
199+
}
200+
if gw6 && ep6 == nil {
201+
ep6 = ep
186202
}
187203
}
188-
return nil
204+
return ep4, ep6
189205
}

0 commit comments

Comments
 (0)