@@ -2,9 +2,11 @@ package networking
22
33import (
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+ }
0 commit comments