github.com/moby/docker@v26.1.3+incompatible/integration/network/macvlan/macvlan_test.go (about) 1 //go:build !windows 2 3 package macvlan // import "github.com/docker/docker/integration/network/macvlan" 4 5 import ( 6 "context" 7 "strings" 8 "testing" 9 10 "github.com/docker/docker/api/types" 11 containertypes "github.com/docker/docker/api/types/container" 12 "github.com/docker/docker/client" 13 "github.com/docker/docker/integration/internal/container" 14 net "github.com/docker/docker/integration/internal/network" 15 n "github.com/docker/docker/integration/network" 16 "github.com/docker/docker/testutil" 17 "github.com/docker/docker/testutil/daemon" 18 "gotest.tools/v3/assert" 19 is "gotest.tools/v3/assert/cmp" 20 "gotest.tools/v3/skip" 21 ) 22 23 func TestDockerNetworkMacvlanPersistance(t *testing.T) { 24 // verify the driver automatically provisions the 802.1q link (dm-dummy0.60) 25 skip.If(t, testEnv.IsRemoteDaemon) 26 skip.If(t, testEnv.IsRootless, "rootless mode has different view of network") 27 28 ctx := testutil.StartSpan(baseContext, t) 29 30 d := daemon.New(t) 31 d.StartWithBusybox(ctx, t) 32 defer d.Stop(t) 33 34 master := "dm-dummy0" 35 n.CreateMasterDummy(ctx, t, master) 36 defer n.DeleteInterface(ctx, t, master) 37 38 c := d.NewClientT(t) 39 40 netName := "dm-persist" 41 net.CreateNoError(ctx, t, c, netName, 42 net.WithMacvlan("dm-dummy0.60"), 43 ) 44 assert.Check(t, n.IsNetworkAvailable(ctx, c, netName)) 45 d.Restart(t) 46 assert.Check(t, n.IsNetworkAvailable(ctx, c, netName)) 47 } 48 49 func TestDockerNetworkMacvlan(t *testing.T) { 50 skip.If(t, testEnv.IsRemoteDaemon) 51 skip.If(t, testEnv.IsRootless, "rootless mode has different view of network") 52 53 ctx := testutil.StartSpan(baseContext, t) 54 55 for _, tc := range []struct { 56 name string 57 test func(*testing.T, context.Context, client.APIClient) 58 }{ 59 { 60 name: "Subinterface", 61 test: testMacvlanSubinterface, 62 }, { 63 name: "OverlapParent", 64 test: testMacvlanOverlapParent, 65 }, { 66 name: "NilParent", 67 test: testMacvlanNilParent, 68 }, { 69 name: "InternalMode", 70 test: testMacvlanInternalMode, 71 }, { 72 name: "MultiSubnetWithParent", 73 test: testMacvlanMultiSubnetWithParent, 74 }, { 75 name: "MultiSubnetNoParent", 76 test: testMacvlanMultiSubnetNoParent, 77 }, { 78 name: "Addressing", 79 test: testMacvlanAddressing, 80 }, { 81 name: "NoIPv6", 82 test: testMacvlanNoIPv6, 83 }, 84 } { 85 tc := tc 86 t.Run(tc.name, func(t *testing.T) { 87 testutil.StartSpan(ctx, t) 88 89 d := daemon.New(t) 90 t.Cleanup(func() { d.Stop(t) }) 91 d.StartWithBusybox(ctx, t) 92 c := d.NewClientT(t) 93 94 tc.test(t, ctx, c) 95 }) 96 97 // FIXME(vdemeester) clean network 98 } 99 } 100 101 func testMacvlanOverlapParent(t *testing.T, ctx context.Context, client client.APIClient) { 102 // verify the same parent interface cannot be used if already in use by an existing network 103 master := "dm-dummy0" 104 n.CreateMasterDummy(ctx, t, master) 105 defer n.DeleteInterface(ctx, t, master) 106 107 netName := "dm-subinterface" 108 parentName := "dm-dummy0.40" 109 net.CreateNoError(ctx, t, client, netName, 110 net.WithMacvlan(parentName), 111 ) 112 assert.Check(t, n.IsNetworkAvailable(ctx, client, netName)) 113 114 _, err := net.Create(ctx, client, "dm-parent-net-overlap", 115 net.WithMacvlan(parentName), 116 ) 117 assert.Check(t, err != nil) 118 119 // delete the network while preserving the parent link 120 err = client.NetworkRemove(ctx, netName) 121 assert.NilError(t, err) 122 123 assert.Check(t, n.IsNetworkNotAvailable(ctx, client, netName)) 124 // verify the network delete did not delete the predefined link 125 n.LinkExists(ctx, t, master) 126 } 127 128 func testMacvlanSubinterface(t *testing.T, ctx context.Context, client client.APIClient) { 129 // verify the same parent interface cannot be used if already in use by an existing network 130 master := "dm-dummy0" 131 parentName := "dm-dummy0.20" 132 n.CreateMasterDummy(ctx, t, master) 133 defer n.DeleteInterface(ctx, t, master) 134 n.CreateVlanInterface(ctx, t, master, parentName, "20") 135 136 netName := "dm-subinterface" 137 net.CreateNoError(ctx, t, client, netName, 138 net.WithMacvlan(parentName), 139 ) 140 assert.Check(t, n.IsNetworkAvailable(ctx, client, netName)) 141 142 // delete the network while preserving the parent link 143 err := client.NetworkRemove(ctx, netName) 144 assert.NilError(t, err) 145 146 assert.Check(t, n.IsNetworkNotAvailable(ctx, client, netName)) 147 // verify the network delete did not delete the predefined link 148 n.LinkExists(ctx, t, parentName) 149 } 150 151 func testMacvlanNilParent(t *testing.T, ctx context.Context, client client.APIClient) { 152 // macvlan bridge mode - dummy parent interface is provisioned dynamically 153 netName := "dm-nil-parent" 154 net.CreateNoError(ctx, t, client, netName, 155 net.WithMacvlan(""), 156 ) 157 assert.Check(t, n.IsNetworkAvailable(ctx, client, netName)) 158 159 id1 := container.Run(ctx, t, client, container.WithNetworkMode(netName)) 160 id2 := container.Run(ctx, t, client, container.WithNetworkMode(netName)) 161 162 _, err := container.Exec(ctx, client, id2, []string{"ping", "-c", "1", id1}) 163 assert.Check(t, err == nil) 164 } 165 166 func testMacvlanInternalMode(t *testing.T, ctx context.Context, client client.APIClient) { 167 // macvlan bridge mode - dummy parent interface is provisioned dynamically 168 netName := "dm-internal" 169 net.CreateNoError(ctx, t, client, netName, 170 net.WithMacvlan(""), 171 net.WithInternal(), 172 ) 173 assert.Check(t, n.IsNetworkAvailable(ctx, client, netName)) 174 175 id1 := container.Run(ctx, t, client, container.WithNetworkMode(netName)) 176 id2 := container.Run(ctx, t, client, container.WithNetworkMode(netName)) 177 178 result, _ := container.Exec(ctx, client, id1, []string{"ping", "-c", "1", "8.8.8.8"}) 179 assert.Check(t, strings.Contains(result.Combined(), "Network is unreachable")) 180 181 _, err := container.Exec(ctx, client, id2, []string{"ping", "-c", "1", id1}) 182 assert.Check(t, err == nil) 183 } 184 185 func testMacvlanMultiSubnetWithParent(t *testing.T, ctx context.Context, client client.APIClient) { 186 const parentIfName = "dm-dummy0" 187 n.CreateMasterDummy(ctx, t, parentIfName) 188 defer n.DeleteInterface(ctx, t, parentIfName) 189 testMacvlanMultiSubnet(t, ctx, client, parentIfName) 190 } 191 192 func testMacvlanMultiSubnetNoParent(t *testing.T, ctx context.Context, client client.APIClient) { 193 testMacvlanMultiSubnet(t, ctx, client, "") 194 } 195 196 func testMacvlanMultiSubnet(t *testing.T, ctx context.Context, client client.APIClient, parent string) { 197 netName := "dualstackbridge" 198 net.CreateNoError(ctx, t, client, netName, 199 net.WithMacvlan(parent), 200 net.WithIPv6(), 201 net.WithIPAM("172.28.100.0/24", ""), 202 net.WithIPAM("172.28.102.0/24", "172.28.102.254"), 203 net.WithIPAM("2001:db8:abc2::/64", ""), 204 net.WithIPAM("2001:db8:abc4::/64", "2001:db8:abc4::254"), 205 ) 206 207 assert.Check(t, n.IsNetworkAvailable(ctx, client, netName)) 208 209 // start dual stack containers and verify the user specified --ip and --ip6 addresses on subnets 172.28.100.0/24 and 2001:db8:abc2::/64 210 id1 := container.Run(ctx, t, client, 211 container.WithNetworkMode("dualstackbridge"), 212 container.WithIPv4("dualstackbridge", "172.28.100.20"), 213 container.WithIPv6("dualstackbridge", "2001:db8:abc2::20"), 214 ) 215 id2 := container.Run(ctx, t, client, 216 container.WithNetworkMode("dualstackbridge"), 217 container.WithIPv4("dualstackbridge", "172.28.100.21"), 218 container.WithIPv6("dualstackbridge", "2001:db8:abc2::21"), 219 ) 220 c1, err := client.ContainerInspect(ctx, id1) 221 assert.NilError(t, err) 222 if parent == "" { 223 // Inspect the v4 gateway to ensure no default GW was assigned 224 assert.Check(t, is.Equal(c1.NetworkSettings.Networks["dualstackbridge"].Gateway, "")) 225 // Inspect the v6 gateway to ensure no default GW was assigned 226 assert.Check(t, is.Equal(c1.NetworkSettings.Networks["dualstackbridge"].IPv6Gateway, "")) 227 } else { 228 // Inspect the v4 gateway to ensure the proper default GW was assigned 229 assert.Check(t, is.Equal(c1.NetworkSettings.Networks["dualstackbridge"].Gateway, "172.28.100.1")) 230 // Inspect the v6 gateway to ensure the proper default GW was assigned 231 assert.Check(t, is.Equal(c1.NetworkSettings.Networks["dualstackbridge"].IPv6Gateway, "2001:db8:abc2::1")) 232 } 233 234 // verify ipv4 connectivity to the explicit --ip address second to first 235 _, err = container.Exec(ctx, client, id2, []string{"ping", "-c", "1", c1.NetworkSettings.Networks["dualstackbridge"].IPAddress}) 236 assert.NilError(t, err) 237 // verify ipv6 connectivity to the explicit --ip6 address second to first 238 _, err = container.Exec(ctx, client, id2, []string{"ping6", "-c", "1", c1.NetworkSettings.Networks["dualstackbridge"].GlobalIPv6Address}) 239 assert.NilError(t, err) 240 241 // start dual stack containers and verify the user specified --ip and --ip6 addresses on subnets 172.28.102.0/24 and 2001:db8:abc4::/64 242 id3 := container.Run(ctx, t, client, 243 container.WithNetworkMode("dualstackbridge"), 244 container.WithIPv4("dualstackbridge", "172.28.102.20"), 245 container.WithIPv6("dualstackbridge", "2001:db8:abc4::20"), 246 ) 247 id4 := container.Run(ctx, t, client, 248 container.WithNetworkMode("dualstackbridge"), 249 container.WithIPv4("dualstackbridge", "172.28.102.21"), 250 container.WithIPv6("dualstackbridge", "2001:db8:abc4::21"), 251 ) 252 c3, err := client.ContainerInspect(ctx, id3) 253 assert.NilError(t, err) 254 if parent == "" { 255 // Inspect the v4 gateway to ensure no default GW was assigned 256 assert.Check(t, is.Equal(c3.NetworkSettings.Networks["dualstackbridge"].Gateway, "")) 257 // Inspect the v6 gateway to ensure no default GW was assigned 258 assert.Check(t, is.Equal(c3.NetworkSettings.Networks["dualstackbridge"].IPv6Gateway, "")) 259 } else { 260 // Inspect the v4 gateway to ensure the proper explicitly assigned default GW was assigned 261 assert.Check(t, is.Equal(c3.NetworkSettings.Networks["dualstackbridge"].Gateway, "172.28.102.254")) 262 // Inspect the v6 gateway to ensure the proper explicitly assigned default GW was assigned 263 assert.Check(t, is.Equal(c3.NetworkSettings.Networks["dualstackbridge"].IPv6Gateway, "2001:db8:abc4::254")) 264 } 265 266 // verify ipv4 connectivity to the explicit --ip address from third to fourth 267 _, err = container.Exec(ctx, client, id4, []string{"ping", "-c", "1", c3.NetworkSettings.Networks["dualstackbridge"].IPAddress}) 268 assert.NilError(t, err) 269 // verify ipv6 connectivity to the explicit --ip6 address from third to fourth 270 _, err = container.Exec(ctx, client, id4, []string{"ping6", "-c", "1", c3.NetworkSettings.Networks["dualstackbridge"].GlobalIPv6Address}) 271 assert.NilError(t, err) 272 } 273 274 func testMacvlanAddressing(t *testing.T, ctx context.Context, client client.APIClient) { 275 const parentIfName = "dm-dummy0" 276 n.CreateMasterDummy(ctx, t, parentIfName) 277 defer n.DeleteInterface(ctx, t, parentIfName) 278 279 // Ensure the default gateways, next-hops and default dev devices are properly set 280 netName := "dualstackbridge" 281 net.CreateNoError(ctx, t, client, netName, 282 net.WithMacvlan(parentIfName), 283 net.WithIPv6(), 284 net.WithOption("macvlan_mode", "bridge"), 285 net.WithIPAM("172.28.130.0/24", ""), 286 net.WithIPAM("2001:db8:abca::/64", "2001:db8:abca::254"), 287 ) 288 assert.Check(t, n.IsNetworkAvailable(ctx, client, netName)) 289 290 id1 := container.Run(ctx, t, client, 291 container.WithNetworkMode("dualstackbridge"), 292 ) 293 294 // Validate macvlan bridge mode defaults gateway sets the default IPAM next-hop inferred from the subnet 295 result, err := container.Exec(ctx, client, id1, []string{"ip", "route"}) 296 assert.NilError(t, err) 297 assert.Check(t, strings.Contains(result.Combined(), "default via 172.28.130.1 dev eth0")) 298 // Validate macvlan bridge mode sets the v6 gateway to the user specified default gateway/next-hop 299 result, err = container.Exec(ctx, client, id1, []string{"ip", "-6", "route"}) 300 assert.NilError(t, err) 301 assert.Check(t, strings.Contains(result.Combined(), "default via 2001:db8:abca::254 dev eth0")) 302 } 303 304 // Check that a macvlan interface with '--ipv6=false' doesn't get kernel-assigned 305 // IPv6 addresses, but the loopback interface does still have an IPv6 address ('::1'). 306 func testMacvlanNoIPv6(t *testing.T, ctx context.Context, client client.APIClient) { 307 const netName = "macvlannet" 308 309 net.CreateNoError(ctx, t, client, netName, 310 net.WithMacvlan(""), 311 net.WithOption("macvlan_mode", "bridge"), 312 ) 313 assert.Check(t, n.IsNetworkAvailable(ctx, client, netName)) 314 315 id := container.Run(ctx, t, client, container.WithNetworkMode(netName)) 316 317 loRes := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "lo"}) 318 assert.Check(t, is.Contains(loRes.Combined(), " inet ")) 319 assert.Check(t, is.Contains(loRes.Combined(), " inet6 ")) 320 321 eth0Res := container.ExecT(ctx, t, client, id, []string{"ip", "a", "show", "dev", "eth0"}) 322 assert.Check(t, is.Contains(eth0Res.Combined(), " inet ")) 323 assert.Check(t, !strings.Contains(eth0Res.Combined(), " inet6 "), 324 "result.Combined(): %s", eth0Res.Combined()) 325 326 sysctlRes := container.ExecT(ctx, t, client, id, []string{"sysctl", "-n", "net.ipv6.conf.eth0.disable_ipv6"}) 327 assert.Check(t, is.Equal(strings.TrimSpace(sysctlRes.Combined()), "1")) 328 } 329 330 // TestMACVlanDNS checks whether DNS is forwarded, with/without a parent 331 // interface, and with '--internal'. Note that there's no attempt here to give 332 // the macvlan network external connectivity - when this test supplies a parent 333 // interface, it's a dummy. External DNS lookups only work because the daemon is 334 // configured to see a host resolver on a loopback interface, so the external DNS 335 // lookup happens in the host's namespace. The test is checking that an 336 // automatically configured dummy interface causes the network to behave as if it 337 // was '--internal'. 338 func TestMACVlanDNS(t *testing.T) { 339 skip.If(t, testEnv.IsRootless, "rootless mode has different view of network") 340 341 ctx := testutil.StartSpan(baseContext, t) 342 343 net.StartDaftDNS(t, "127.0.0.1") 344 345 tmpFileName := net.WriteTempResolvConf(t, "127.0.0.1") 346 d := daemon.New(t, daemon.WithEnvVars("DOCKER_TEST_RESOLV_CONF_PATH="+tmpFileName)) 347 d.StartWithBusybox(ctx, t) 348 t.Cleanup(func() { d.Stop(t) }) 349 c := d.NewClientT(t) 350 351 const parentIfName = "dm-dummy0" 352 n.CreateMasterDummy(ctx, t, parentIfName) 353 defer n.DeleteInterface(ctx, t, parentIfName) 354 355 const netName = "macvlan-dns-net" 356 357 testcases := []struct { 358 name string 359 parent string 360 internal bool 361 expDNS bool 362 }{ 363 { 364 name: "with parent", 365 parent: parentIfName, 366 // External DNS should be used (even though the network has no external connectivity). 367 expDNS: true, 368 }, 369 { 370 name: "no parent", 371 // External DNS should not be used, equivalent to '--internal'. 372 }, 373 { 374 name: "with parent, internal", 375 parent: parentIfName, 376 internal: true, 377 expDNS: false, 378 }, 379 } 380 381 for _, tc := range testcases { 382 t.Run(tc.name, func(t *testing.T) { 383 ctx := testutil.StartSpan(ctx, t) 384 createOpts := []func(*types.NetworkCreate){ 385 net.WithMacvlan(tc.parent), 386 } 387 if tc.internal { 388 createOpts = append(createOpts, net.WithInternal()) 389 } 390 net.CreateNoError(ctx, t, c, netName, createOpts...) 391 defer c.NetworkRemove(ctx, netName) 392 393 ctrId := container.Run(ctx, t, c, container.WithNetworkMode(netName)) 394 defer c.ContainerRemove(ctx, ctrId, containertypes.RemoveOptions{Force: true}) 395 res, err := container.Exec(ctx, c, ctrId, []string{"nslookup", "test.example"}) 396 assert.NilError(t, err) 397 if tc.expDNS { 398 assert.Check(t, is.Equal(res.ExitCode, 0)) 399 assert.Check(t, is.Contains(res.Stdout(), net.DNSRespAddr)) 400 } else { 401 assert.Check(t, is.Equal(res.ExitCode, 1)) 402 assert.Check(t, is.Contains(res.Stdout(), "SERVFAIL")) 403 } 404 }) 405 } 406 }