Skip to content

Commit 38ed40e

Browse files
committed
feat(network): add support for static IPv6 assignment
This commit introduces end-to-end support for assigning static IPv6 addresses to containers, both via the command line and through nerdctl compose. Key changes: - A new `--ip6` flag is added to `nerdctl run` to allow direct assignment of a static IPv6 address. - The CNI bridge configuration for IPv6 networks is now updated to declare the `"ips"` capability. This is required by libcni to process static IP capability arguments. - To prevent regressions in downstream CNI plugins (like dnsname and firewall), the `"dns"` and `"portMappings"` capabilities are also explicitly declared for IPv6 networks. This avoids issues where implicit capabilities were being dropped. - The compose parser now recognizes the `ipv6_address` field within a service's network configuration and translates it to the `--ip6` flag during container creation. - Adds comprehensive integration tests for static IPv4, IPv6, and dual-stack IP assignment, as well as unit tests for compose file parsing, to validate the new functionality and prevent regressions. Fixes #4597 Signed-off-by: Frits <[email protected]>
1 parent 2165e30 commit 38ed40e

File tree

5 files changed

+128
-5
lines changed

5 files changed

+128
-5
lines changed

cmd/nerdctl/network/network_create_linux_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,83 @@ func TestNetworkCreate(t *testing.T) {
156156
}
157157
},
158158
},
159+
{
160+
Description: "with static IPv4 address",
161+
Setup: func(data test.Data, helpers test.Helpers) {
162+
networkName := data.Identifier()
163+
staticIP := "172.19.0.100"
164+
data.Labels().Set("networkName", networkName)
165+
data.Labels().Set("staticIP", staticIP)
166+
helpers.Ensure("network", "create", networkName, "--driver", "bridge", "--subnet", "172.19.0.0/24")
167+
},
168+
Cleanup: func(data test.Data, helpers test.Helpers) {
169+
helpers.Anyhow("network", "rm", data.Labels().Get("networkName"))
170+
},
171+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
172+
return helpers.Command("run", "--rm", "--net", data.Labels().Get("networkName"), "--ip", data.Labels().Get("staticIP"), testutil.CommonImage, "ip", "addr", "show", "eth0")
173+
},
174+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
175+
return &test.Expected{
176+
ExitCode: 0,
177+
Output: func(stdout string, t tig.T) {
178+
assert.Assert(t, strings.Contains(stdout, fmt.Sprintf("inet %s/24", data.Labels().Get("staticIP"))))
179+
},
180+
}
181+
},
182+
},
183+
{
184+
Description: "with static IPv6 address",
185+
Require: nerdtest.OnlyIPv6,
186+
Setup: func(data test.Data, helpers test.Helpers) {
187+
networkName := data.Identifier()
188+
staticIPv6 := "2001:db8:1::100"
189+
data.Labels().Set("networkName", networkName)
190+
data.Labels().Set("staticIPv6", staticIPv6)
191+
helpers.Ensure("network", "create", networkName, "--driver", "bridge", "--ipv6", "--subnet", "2001:db8:1::/64")
192+
},
193+
Cleanup: func(data test.Data, helpers test.Helpers) {
194+
helpers.Anyhow("network", "rm", data.Labels().Get("networkName"))
195+
},
196+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
197+
return helpers.Command("run", "--rm", "--net", data.Labels().Get("networkName"), "--ip", data.Labels().Get("staticIPv6"), testutil.CommonImage, "ip", "addr", "show", "eth0")
198+
},
199+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
200+
return &test.Expected{
201+
ExitCode: 0,
202+
Output: func(stdout string, t tig.T) {
203+
assert.Assert(t, strings.Contains(stdout, fmt.Sprintf("inet6 %s/64", data.Labels().Get("staticIPv6"))))
204+
},
205+
}
206+
},
207+
},
208+
{
209+
Description: "with dual-stack static IP addresses",
210+
Require: nerdtest.OnlyIPv6,
211+
Setup: func(data test.Data, helpers test.Helpers) {
212+
networkName := data.Identifier()
213+
staticIPv4 := "172.20.0.100"
214+
staticIPv6 := "2001:db8:2::100"
215+
data.Labels().Set("networkName", networkName)
216+
data.Labels().Set("staticIPv4", staticIPv4)
217+
data.Labels().Set("staticIPv6", staticIPv6)
218+
helpers.Ensure("network", "create", networkName, "--driver", "bridge", "--subnet", "172.20.0.0/24", "--ipv6", "--subnet", "2001:db8:2::/64")
219+
},
220+
Cleanup: func(data test.Data, helpers test.Helpers) {
221+
helpers.Anyhow("network", "rm", data.Labels().Get("networkName"))
222+
},
223+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
224+
return helpers.Command("run", "--rm", "--net", data.Labels().Get("networkName"), "--ip", data.Labels().Get("staticIPv4"), "--ip", data.Labels().Get("staticIPv6"), testutil.CommonImage, "ip", "addr", "show", "eth0")
225+
},
226+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
227+
return &test.Expected{
228+
ExitCode: 0,
229+
Output: func(stdout string, t tig.T) {
230+
assert.Assert(t, strings.Contains(stdout, fmt.Sprintf("inet %s/24", data.Labels().Get("staticIPv4"))))
231+
assert.Assert(t, strings.Contains(stdout, fmt.Sprintf("inet6 %s/64", data.Labels().Get("staticIPv6"))))
232+
},
233+
}
234+
},
235+
},
159236
}
160237

161238
testCase.Run(t)

pkg/composer/serviceparser/serviceparser.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,9 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e
597597
if value != nil && value.Ipv4Address != "" {
598598
c.RunArgs = append(c.RunArgs, "--ip="+value.Ipv4Address)
599599
}
600+
if value != nil && value.Ipv6Address != "" {
601+
c.RunArgs = append(c.RunArgs, "--ip6="+value.Ipv6Address)
602+
}
600603
if value != nil && value.MacAddress != "" {
601604
c.RunArgs = append(c.RunArgs, "--mac-address="+value.MacAddress)
602605
}

pkg/composer/serviceparser/serviceparser_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,44 @@ services:
482482

483483
}
484484

485+
func TestParseDualStackAddress(t *testing.T) {
486+
t.Parallel()
487+
const dockerComposeYAML = `
488+
services:
489+
foo:
490+
image: nginx:alpine
491+
networks:
492+
default:
493+
ipv4_address: "172.30.0.100"
494+
ipv6_address: "2001:db8:abc:123::42"
495+
networks:
496+
default:
497+
driver: bridge
498+
ipam:
499+
driver: default
500+
config:
501+
- subnet: "172.30.0.0/24"
502+
- subnet: "2001:db8:abc:123::/64"
503+
`
504+
comp := testutil.NewComposeDir(t, dockerComposeYAML)
505+
defer comp.CleanUp()
506+
507+
project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
508+
assert.NilError(t, err)
509+
510+
fooSvc, err := project.GetService("foo")
511+
assert.NilError(t, err)
512+
513+
foo, err := Parse(project, fooSvc)
514+
assert.NilError(t, err)
515+
516+
t.Logf("foo: %+v", foo)
517+
for _, c := range foo.Containers {
518+
assert.Assert(t, in(c.RunArgs, "--ip=172.30.0.100"))
519+
assert.Assert(t, in(c.RunArgs, "--ip6=2001:db8:abc:123::42"))
520+
}
521+
}
522+
485523
func TestParseConfigs(t *testing.T) {
486524
t.Parallel()
487525
if runtime.GOOS == "windows" {

pkg/netutil/cni_plugin_unix.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,12 @@ func (*tuningConfig) GetPluginType() string {
136136

137137
// https://github.com/containernetworking/plugins/blob/v1.0.1/plugins/ipam/host-local/backend/allocator/config.go#L47-L56
138138
type hostLocalIPAMConfig struct {
139-
Type string `json:"type"`
140-
Routes []IPAMRoute `json:"routes,omitempty"`
141-
ResolveConf string `json:"resolveConf,omitempty"`
142-
DataDir string `json:"dataDir,omitempty"`
143-
Ranges [][]IPAMRange `json:"ranges,omitempty"`
139+
Type string `json:"type"`
140+
Routes []IPAMRoute `json:"routes,omitempty"`
141+
ResolveConf string `json:"resolveConf,omitempty"`
142+
DataDir string `json:"dataDir,omitempty"`
143+
Ranges [][]IPAMRange `json:"ranges,omitempty"`
144+
Capabilities map[string]bool `json:"capabilities,omitempty"`
144145
}
145146

146147
func newHostLocalIPAMConfig() *hostLocalIPAMConfig {

pkg/netutil/netutil_unix.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]
138138
bridge.HairpinMode = true
139139
if ipv6 {
140140
bridge.Capabilities["ips"] = true
141+
// Explicitly declare capabilities that are implicitly lost when
142+
// the bridge.Capabilities map becomes non-empty.
143+
bridge.Capabilities["dns"] = true
144+
bridge.Capabilities["portMappings"] = true
141145
}
142146

143147
// Determine the appropriate firewall ingress policy based on icc setting

0 commit comments

Comments
 (0)