Skip to content

Commit d88daf3

Browse files
committed
Support more than one IP per interface and IPv6 for results returned by CNI
1 parent dc8f6de commit d88daf3

File tree

6 files changed

+86
-87
lines changed

6 files changed

+86
-87
lines changed

cni/vmconf/vmconf.go

+28-33
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@
1212
// permissions and limitations under the License.
1313

1414
/*
15-
Package vmconf defines an interface for converting particular CNI invocation
16-
results to networking configuration usable by a VM. It expects the CNI result
17-
to have the following properties:
18-
* The results should contain an interface for a tap device, which will be used
19-
as the VM's tap device.
20-
* The results should contain an interface with the same name as the tap device
21-
but with sandbox ID set to the containerID provided during CNI invocation.
22-
This should be a "pseudo-interface", not one that has actually been created.
23-
It represents the configuration that should be applied to the VM internally.
24-
The CNI "containerID" is, in this case, used more as a "vmID" to represent
25-
the VM's internal network interface.
26-
* If the CNI results specify an IP associated with this interface, that IP
27-
should be used to statically configure the VM's internal network interface.
15+
Package vmconf defines an interface for converting particular CNI invocation
16+
results to networking configuration usable by a VM. It expects the CNI result
17+
to have the following properties:
18+
- The results should contain an interface for a tap device, which will be used
19+
as the VM's tap device.
20+
- The results should contain an interface with the same name as the tap device
21+
but with sandbox ID set to the containerID provided during CNI invocation.
22+
This should be a "pseudo-interface", not one that has actually been created.
23+
It represents the configuration that should be applied to the VM internally.
24+
The CNI "containerID" is, in this case, used more as a "vmID" to represent
25+
the VM's internal network interface.
26+
- If the CNI results specify an IP associated with this interface, that IP
27+
should be used to statically configure the VM's internal network interface.
2828
*/
2929
package vmconf
3030

@@ -62,7 +62,7 @@ type StaticNetworkConf struct {
6262
VMMTU int
6363
// VMIPConfig is the ip configuration that callers should configure their VM's internal
6464
// primary interface to use.
65-
VMIPConfig *current.IPConfig
65+
VMIPConfig []*current.IPConfig
6666
// VMRoutes are the routes that callers should configure their VM's internal route table
6767
// to have
6868
VMRoutes []*types.Route
@@ -88,32 +88,32 @@ type StaticNetworkConf struct {
8888
//
8989
// Due to the limitation of "ip=", not all configuration specified in StaticNetworkConf can be
9090
// applied automatically. In particular:
91-
// * The MacAddr and MTU cannot be applied
92-
// * The only routes created will match what's specified in VMIPConfig; VMRoutes will be ignored.
93-
// * Only up to two namesevers can be supplied. If VMNameservers is has more than 2 entries, only
94-
// the first two in the slice will be applied in the VM.
95-
// * VMDomain, VMSearchDomains and VMResolverOptions will be ignored
96-
// * Nameserver settings are also only set in /proc/net/pnp. Most applications will thus require
97-
// /etc/resolv.conf to be a symlink to /proc/net/pnp in order to resolve names as expected.
91+
// - The MacAddr and MTU cannot be applied
92+
// - The only routes created will match what's specified in VMIPConfig; VMRoutes will be ignored.
93+
// - Only up to two namesevers can be supplied. If VMNameservers is has more than 2 entries, only
94+
// the first two in the slice will be applied in the VM.
95+
// - VMDomain, VMSearchDomains and VMResolverOptions will be ignored
96+
// - Nameserver settings are also only set in /proc/net/pnp. Most applications will thus require
97+
// /etc/resolv.conf to be a symlink to /proc/net/pnp in order to resolve names as expected.
9898
func (c StaticNetworkConf) IPBootParam() string {
9999
// See "ip=" section of kernel linked above for details on each field listed below.
100100

101101
// client-ip is really just the ip that will be assigned to the primary interface
102-
clientIP := c.VMIPConfig.Address.IP.String()
102+
clientIP := c.VMIPConfig[0].Address.IP.String()
103103

104104
// don't set nfs server IP
105105
const serverIP = ""
106106

107107
// default gateway for the network; used to generate a corresponding route table entry
108-
defaultGateway := c.VMIPConfig.Gateway.String()
108+
defaultGateway := c.VMIPConfig[0].Gateway.String()
109109

110110
// subnet mask used to generate a corresponding route table entry for the primary interface
111111
// (must be provided in dotted decimal notation)
112112
subnetMask := fmt.Sprintf("%d.%d.%d.%d",
113-
c.VMIPConfig.Address.Mask[0],
114-
c.VMIPConfig.Address.Mask[1],
115-
c.VMIPConfig.Address.Mask[2],
116-
c.VMIPConfig.Address.Mask[3],
113+
c.VMIPConfig[0].Address.Mask[0],
114+
c.VMIPConfig[0].Address.Mask[1],
115+
c.VMIPConfig[0].Address.Mask[2],
116+
c.VMIPConfig[0].Address.Mask[3],
117117
)
118118

119119
// the "hostname" field actually just configures a hostname value for DHCP requests, thus no need to set it
@@ -168,11 +168,6 @@ func StaticNetworkConfFrom(result types.Result, containerID string) (*StaticNetw
168168

169169
// find the IP associated with the VM iface
170170
vmIPs := internal.InterfaceIPs(currentResult, vmIface.Name, vmIface.Sandbox)
171-
if len(vmIPs) != 1 {
172-
return nil, fmt.Errorf("expected to find 1 IP for vm interface %q, but instead found %+v",
173-
vmIface.Name, vmIPs)
174-
}
175-
vmIP := vmIPs[0]
176171

177172
netNS, err := ns.GetNS(tapIface.Sandbox)
178173
if err != nil {
@@ -189,7 +184,7 @@ func StaticNetworkConfFrom(result types.Result, containerID string) (*StaticNetw
189184
NetNSPath: tapIface.Sandbox,
190185
VMMacAddr: vmIface.Mac,
191186
VMMTU: tapMTU,
192-
VMIPConfig: vmIP,
187+
VMIPConfig: vmIPs,
193188
VMRoutes: currentResult.Routes,
194189
VMNameservers: currentResult.DNS.Nameservers,
195190
VMDomain: currentResult.DNS.Domain,

cni/vmconf/vmconf_test.go

+7-5
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,14 @@ func TestIPBootParams(t *testing.T) {
6868
VMMacAddr: "00:11:22:33:44:55",
6969
VMIfName: "eth0",
7070
VMMTU: 1337,
71-
VMIPConfig: &current.IPConfig{
72-
Address: net.IPNet{
73-
IP: net.IPv4(10, 0, 0, 2),
74-
Mask: net.IPv4Mask(255, 255, 255, 0),
71+
VMIPConfig: []*current.IPConfig{
72+
&current.IPConfig{
73+
Address: net.IPNet{
74+
IP: net.IPv4(10, 0, 0, 2),
75+
Mask: net.IPv4Mask(255, 255, 255, 0),
76+
},
77+
Gateway: net.IPv4(10, 0, 0, 1),
7578
},
76-
Gateway: net.IPv4(10, 0, 0, 1),
7779
},
7880
VMRoutes: []*types.Route{{
7981
Dst: net.IPNet{

machine.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -503,8 +503,11 @@ func (m *Machine) setupKernelArgs(ctx context.Context) error {
503503
// If any network interfaces have a static IP configured, we need to set the "ip=" boot param.
504504
// Validation that we are not overriding an existing "ip=" setting happens in the network validation
505505
if staticIPInterface := m.Cfg.NetworkInterfaces.staticIPInterface(); staticIPInterface != nil {
506-
ipBootParam := staticIPInterface.StaticConfiguration.IPConfiguration.ipBootParam()
507-
kernelArgs["ip"] = &ipBootParam
506+
// Only generate the ip= boot param if there is a single ip on the interface
507+
if len(staticIPInterface.StaticConfiguration.IPConfiguration) == 1 {
508+
ipBootParam := staticIPInterface.StaticConfiguration.IPConfiguration[0].ipBootParam()
509+
kernelArgs["ip"] = &ipBootParam
510+
}
508511
}
509512

510513
m.Cfg.KernelArgs = kernelArgs.String()
@@ -649,7 +652,7 @@ func (m *Machine) startVMM(ctx context.Context) error {
649652
return nil
650653
}
651654

652-
//StopVMM stops the current VMM.
655+
// StopVMM stops the current VMM.
653656
func (m *Machine) StopVMM() error {
654657
return m.stopVMM()
655658
}

machine_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1998,7 +1998,7 @@ func connectToVM(m *Machine, sshKeyPath string) (*ssh.Client, error) {
19981998
return nil, errors.New("No network interfaces")
19991999
}
20002000

2001-
ip := m.Cfg.NetworkInterfaces.staticIPInterface().StaticConfiguration.IPConfiguration.IPAddr.IP
2001+
ip := m.Cfg.NetworkInterfaces.staticIPInterface().StaticConfiguration.IPConfiguration[0].IPAddr.IP
20022002

20032003
return ssh.Dial("tcp", fmt.Sprintf("%s:22", ip), config)
20042004
}
@@ -2193,7 +2193,7 @@ func TestLoadSnapshot(t *testing.T) {
21932193
require.NoError(t, err)
21942194
defer client.Close()
21952195

2196-
ipToRestore = m.Cfg.NetworkInterfaces.staticIPInterface().StaticConfiguration.IPConfiguration.IPAddr.IP.String()
2196+
ipToRestore = m.Cfg.NetworkInterfaces.staticIPInterface().StaticConfiguration.IPConfiguration[0].IPAddr.IP.String()
21972197

21982198
session, err := client.NewSession()
21992199
require.NoError(t, err)

network.go

+17-15
Original file line numberDiff line numberDiff line change
@@ -141,19 +141,18 @@ func (networkInterfaces NetworkInterfaces) setupNetwork(
141141
MacAddress: vmNetConf.VMMacAddr,
142142
}
143143

144-
if vmNetConf.VMIPConfig != nil {
144+
for _, vmIPCfg := range vmNetConf.VMIPConfig {
145145
if len(vmNetConf.VMNameservers) > 2 {
146146
logger.Warnf("more than 2 nameservers provided from CNI result, only the first 2 %+v will be applied",
147147
vmNetConf.VMNameservers[:2])
148148
vmNetConf.VMNameservers = vmNetConf.VMNameservers[:2]
149149
}
150-
151-
cniNetworkInterface.StaticConfiguration.IPConfiguration = &IPConfiguration{
152-
IPAddr: vmNetConf.VMIPConfig.Address,
153-
Gateway: vmNetConf.VMIPConfig.Gateway,
150+
cniNetworkInterface.StaticConfiguration.IPConfiguration = append(cniNetworkInterface.StaticConfiguration.IPConfiguration, &IPConfiguration{
151+
IPAddr: vmIPCfg.Address,
152+
Gateway: vmIPCfg.Gateway,
154153
Nameservers: vmNetConf.VMNameservers,
155154
IfName: cniNetworkInterface.CNIConfiguration.VMIfName,
156-
}
155+
})
157156
}
158157
}
159158

@@ -484,16 +483,16 @@ type StaticNetworkConfiguration struct {
484483

485484
// IPConfiguration (optional) allows a static IP, gateway and up to 2 DNS nameservers
486485
// to be automatically configured within the VM upon startup.
487-
IPConfiguration *IPConfiguration
486+
IPConfiguration []*IPConfiguration
488487
}
489488

490489
func (staticConf StaticNetworkConfiguration) validate() error {
491490
if staticConf.HostDevName == "" {
492491
return fmt.Errorf("HostDevName must be provided if StaticNetworkConfiguration is provided: %+v", staticConf)
493492
}
494493

495-
if staticConf.IPConfiguration != nil {
496-
err := staticConf.IPConfiguration.validate()
494+
for _, ipCfg := range staticConf.IPConfiguration {
495+
err := ipCfg.validate()
497496
if err != nil {
498497
return err
499498
}
@@ -522,10 +521,10 @@ type IPConfiguration struct {
522521
}
523522

524523
func (ipConf IPConfiguration) validate() error {
525-
// Make sure only ipv4 is being provided (for now).
524+
// Make sure it is a valid ip.
526525
for _, ip := range []net.IP{ipConf.IPAddr.IP, ipConf.Gateway} {
527-
if ip.To4() == nil {
528-
return fmt.Errorf("invalid ip, only ipv4 addresses are supported: %+v", ip)
526+
if ip.To4() == nil && ip.To16() == nil {
527+
return fmt.Errorf("invalid ip: %+v", ip)
529528
}
530529
}
531530

@@ -538,11 +537,14 @@ func (ipConf IPConfiguration) validate() error {
538537

539538
func (conf IPConfiguration) ipBootParam() string {
540539
// the vmconf package already has a function for doing this, just re-use it
540+
541541
vmConf := vmconf.StaticNetworkConf{
542542
VMNameservers: conf.Nameservers,
543-
VMIPConfig: &current.IPConfig{
544-
Address: conf.IPAddr,
545-
Gateway: conf.Gateway,
543+
VMIPConfig: []*current.IPConfig{
544+
{
545+
Address: conf.IPAddr,
546+
Gateway: conf.Gateway,
547+
},
546548
},
547549
VMIfName: conf.IfName,
548550
}

network_test.go

+26-29
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,27 @@ var (
4444
kernelArgsWithIP = parseKernelArgs("foo=bar this=phony ip=whatevz")
4545

4646
// These RFC 5737 IPs are reserved for documentation, they are not usable
47-
validIPConfiguration = &IPConfiguration{
48-
IPAddr: net.IPNet{
49-
IP: net.IPv4(198, 51, 100, 2),
50-
Mask: net.IPv4Mask(255, 255, 255, 0),
47+
validIPConfiguration = []*IPConfiguration{
48+
&IPConfiguration{
49+
IPAddr: net.IPNet{
50+
IP: net.IPv4(198, 51, 100, 2),
51+
Mask: net.IPv4Mask(255, 255, 255, 0),
52+
},
53+
Gateway: net.IPv4(198, 51, 100, 1),
54+
Nameservers: []string{"192.0.2.1", "192.0.2.2"},
5155
},
52-
Gateway: net.IPv4(198, 51, 100, 1),
53-
Nameservers: []string{"192.0.2.1", "192.0.2.2"},
5456
}
5557

5658
// IPv6 is currently invalid
5759
// These RFC 3849 IPs are reserved for documentation, they are not usable
58-
invalidIPConfiguration = &IPConfiguration{
59-
IPAddr: net.IPNet{
60-
IP: net.ParseIP("2001:db8:a0b:12f0::2"),
61-
Mask: net.CIDRMask(24, 128),
60+
invalidIPConfiguration = []*IPConfiguration{
61+
&IPConfiguration{
62+
IPAddr: net.IPNet{
63+
IP: net.ParseIP("2001:db8:a0b:12f0::2"),
64+
Mask: net.CIDRMask(24, 128),
65+
},
66+
Gateway: net.ParseIP("2001:db8:a0b:12f0::1"),
6267
},
63-
Gateway: net.ParseIP("2001:db8:a0b:12f0::1"),
6468
}
6569

6670
validStaticNetworkInterface = NetworkInterface{
@@ -99,30 +103,23 @@ func TestNetworkStaticValidationFails_TooManyNameservers(t *testing.T) {
99103
staticNetworkConfig := StaticNetworkConfiguration{
100104
MacAddress: mockMacAddrString,
101105
HostDevName: tapName,
102-
IPConfiguration: &IPConfiguration{
103-
IPAddr: net.IPNet{
104-
IP: net.IPv4(198, 51, 100, 2),
105-
Mask: net.IPv4Mask(255, 255, 255, 0),
106+
IPConfiguration: []*IPConfiguration{
107+
&IPConfiguration{
108+
IPAddr: net.IPNet{
109+
IP: net.IPv4(198, 51, 100, 2),
110+
Mask: net.IPv4Mask(255, 255, 255, 0),
111+
},
112+
Gateway: net.IPv4(198, 51, 100, 1),
113+
Nameservers: []string{"192.0.2.1", "192.0.2.2", "192.0.2.3"},
106114
},
107-
Gateway: net.IPv4(198, 51, 100, 1),
108-
Nameservers: []string{"192.0.2.1", "192.0.2.2", "192.0.2.3"},
109115
},
110116
}
111117

112118
err := staticNetworkConfig.validate()
113119
assert.Error(t, err, "network config with more than two nameservers did not result in validation error")
114120
}
115121

116-
func TestNetworkStaticValidationFails_IPConfiguration(t *testing.T) {
117-
staticNetworkConfig := StaticNetworkConfiguration{
118-
MacAddress: mockMacAddrString,
119-
HostDevName: tapName,
120-
IPConfiguration: invalidIPConfiguration,
121-
}
122-
123-
err := staticNetworkConfig.validate()
124-
assert.Error(t, err, "invalid network config hostdevname did not result in validation error")
125-
}
122+
// TestNetworkStaticValidationFails_IPConfiguration removed as IPv6 support was added in this fork
126123

127124
func TestNetworkCNIValidation(t *testing.T) {
128125
err := validCNIInterface.CNIConfiguration.validate()
@@ -448,12 +445,12 @@ func startCNIMachine(t *testing.T, ctx context.Context, m *Machine) string {
448445
assert.NotEmpty(t, staticConfig.HostDevName,
449446
"static config should have host dev name set")
450447

451-
ipConfig := staticConfig.IPConfiguration
448+
ipConfig := staticConfig.IPConfiguration[0]
452449
require.NotNil(t, ipConfig,
453450
"cni configuration should have updated network interface ip configuration")
454451

455452
require.Equal(t, m.Cfg.NetworkInterfaces[0].CNIConfiguration.VMIfName,
456-
staticConfig.IPConfiguration.IfName,
453+
staticConfig.IPConfiguration[0].IfName,
457454
"interface name should be propagated to static conf")
458455

459456
return ipConfig.IPAddr.IP.String()

0 commit comments

Comments
 (0)