macvlan_test.go 15 KB

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