summaryrefslogtreecommitdiff
path: root/libpod/network/cni/cni_conversion.go
blob: 93d871767d6f4ea55c509e2357d8f3b173ac4b06 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
// +build linux

package cni

import (
	"encoding/json"
	"io/ioutil"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/containernetworking/cni/libcni"
	"github.com/containers/podman/v3/libpod/network/types"
	"github.com/containers/podman/v3/libpod/network/util"
	pkgutil "github.com/containers/podman/v3/pkg/util"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

func createNetworkFromCNIConfigList(conf *libcni.NetworkConfigList, confPath string) (*types.Network, error) {
	network := types.Network{
		Name:        conf.Name,
		ID:          getNetworkIDFromName(conf.Name),
		Labels:      map[string]string{},
		Options:     map[string]string{},
		IPAMOptions: map[string]string{},
	}

	cniJSON := make(map[string]interface{})
	err := json.Unmarshal(conf.Bytes, &cniJSON)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to unmarshal network config %s", conf.Name)
	}
	if args, ok := cniJSON["args"]; ok {
		if key, ok := args.(map[string]interface{}); ok {
			// read network labels and options from the conf file
			network.Labels = getNetworkArgsFromConfList(key, podmanLabelKey)
			network.Options = getNetworkArgsFromConfList(key, podmanOptionsKey)
		}
	}

	f, err := os.Stat(confPath)
	if err != nil {
		return nil, err
	}
	stat := f.Sys().(*syscall.Stat_t)
	network.Created = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))

	firstPlugin := conf.Plugins[0]
	network.Driver = firstPlugin.Network.Type

	switch firstPlugin.Network.Type {
	case types.BridgeNetworkDriver:
		var bridge hostLocalBridge
		err := json.Unmarshal(firstPlugin.Bytes, &bridge)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to unmarshal the bridge plugin config in %s", confPath)
		}
		network.NetworkInterface = bridge.BrName

		// if isGateway is false we have an internal network
		if !bridge.IsGW {
			network.Internal = true
		}

		// set network options
		if bridge.MTU != 0 {
			network.Options["mtu"] = strconv.Itoa(bridge.MTU)
		}
		if bridge.Vlan != 0 {
			network.Options["vlan"] = strconv.Itoa(bridge.Vlan)
		}

		err = convertIPAMConfToNetwork(&network, bridge.IPAM, confPath)
		if err != nil {
			return nil, err
		}

	case types.MacVLANNetworkDriver, types.IPVLANNetworkDriver:
		var vlan VLANConfig
		err := json.Unmarshal(firstPlugin.Bytes, &vlan)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to unmarshal the macvlan plugin config in %s", confPath)
		}
		network.NetworkInterface = vlan.Master

		// set network options
		if vlan.MTU != 0 {
			network.Options["mtu"] = strconv.Itoa(vlan.MTU)
		}

		if vlan.Mode != "" {
			network.Options["mode"] = vlan.Mode
		}

		err = convertIPAMConfToNetwork(&network, vlan.IPAM, confPath)
		if err != nil {
			return nil, err
		}

	default:
		// A warning would be good but users would get this warning everytime so keep this at info level.
		logrus.Infof("Unsupported CNI config type %s in %s, this network can still be used but inspect or list cannot show all information",
			firstPlugin.Network.Type, confPath)
	}

	// check if the dnsname plugin is configured
	network.DNSEnabled = findPluginByName(conf.Plugins, "dnsname")

	return &network, nil
}

func findPluginByName(plugins []*libcni.NetworkConfig, name string) bool {
	for _, plugin := range plugins {
		if plugin.Network.Type == name {
			return true
		}
	}
	return false
}

// convertIPAMConfToNetwork converts A cni IPAMConfig to libpod network subnets.
// It returns an array of subnets and an extra bool if dhcp is configured.
func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath string) error {
	if ipam.PluginType == types.DHCPIPAMDriver {
		network.IPAMOptions["driver"] = types.DHCPIPAMDriver
		return nil
	}

	if ipam.PluginType != types.HostLocalIPAMDriver {
		return errors.Errorf("unsupported ipam plugin %s in %s", ipam.PluginType, confPath)
	}

	network.IPAMOptions["driver"] = types.HostLocalIPAMDriver
	for _, r := range ipam.Ranges {
		for _, ipam := range r {
			s := types.Subnet{}

			// Do not use types.ParseCIDR() because we want the ip to be
			// the network address and not a random ip in the sub.
			_, sub, err := net.ParseCIDR(ipam.Subnet)
			if err != nil {
				return err
			}
			s.Subnet = types.IPNet{IPNet: *sub}

			// gateway
			var gateway net.IP
			if ipam.Gateway != "" {
				gateway = net.ParseIP(ipam.Gateway)
				if gateway == nil {
					return errors.Errorf("failed to parse gateway ip %s", ipam.Gateway)
				}
				// convert to 4 byte if ipv4
				ipv4 := gateway.To4()
				if ipv4 != nil {
					gateway = ipv4
				}
			} else if !network.Internal {
				// only add a gateway address if the network is not internal
				gateway, err = util.FirstIPInSubnet(sub)
				if err != nil {
					return errors.Errorf("failed to get first ip in subnet %s", sub.String())
				}
			}
			s.Gateway = gateway

			var rangeStart net.IP
			var rangeEnd net.IP
			if ipam.RangeStart != "" {
				rangeStart = net.ParseIP(ipam.RangeStart)
				if rangeStart == nil {
					return errors.Errorf("failed to parse range start ip %s", ipam.RangeStart)
				}
			}
			if ipam.RangeEnd != "" {
				rangeEnd = net.ParseIP(ipam.RangeEnd)
				if rangeEnd == nil {
					return errors.Errorf("failed to parse range end ip %s", ipam.RangeEnd)
				}
			}
			if rangeStart != nil || rangeEnd != nil {
				s.LeaseRange = &types.LeaseRange{}
				s.LeaseRange.StartIP = rangeStart
				s.LeaseRange.EndIP = rangeEnd
			}
			if util.IsIPv6(s.Subnet.IP) {
				network.IPv6Enabled = true
			}
			network.Subnets = append(network.Subnets, s)
		}
	}
	return nil
}

// getNetworkArgsFromConfList returns the map of args in a conflist, argType should be labels or options
func getNetworkArgsFromConfList(args map[string]interface{}, argType string) map[string]string {
	if args, ok := args[argType]; ok {
		if labels, ok := args.(map[string]interface{}); ok {
			result := make(map[string]string, len(labels))
			for k, v := range labels {
				if v, ok := v.(string); ok {
					result[k] = v
				}
			}
			return result
		}
	}
	return map[string]string{}
}

// createCNIConfigListFromNetwork will create a cni config file from the given network.
// It returns the cni config and the path to the file where the config was written.
// Set writeToDisk to false to only add this network into memory.
func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writeToDisk bool) (*libcni.NetworkConfigList, string, error) {
	var (
		routes     []ipamRoute
		ipamRanges [][]ipamLocalHostRangeConf
		ipamConf   ipamConfig
		err        error
	)
	if len(network.Subnets) > 0 {
		for _, subnet := range network.Subnets {
			route, err := newIPAMDefaultRoute(util.IsIPv6(subnet.Subnet.IP))
			if err != nil {
				return nil, "", err
			}
			routes = append(routes, route)
			ipam := newIPAMLocalHostRange(subnet.Subnet, subnet.LeaseRange, subnet.Gateway)
			ipamRanges = append(ipamRanges, []ipamLocalHostRangeConf{*ipam})
		}
		ipamConf = newIPAMHostLocalConf(routes, ipamRanges)
	} else {
		ipamConf = ipamConfig{PluginType: "dhcp"}
	}

	vlan := 0
	mtu := 0
	vlanPluginMode := ""
	for k, v := range network.Options {
		switch k {
		case "mtu":
			mtu, err = parseMTU(v)
			if err != nil {
				return nil, "", err
			}

		case "vlan":
			vlan, err = parseVlan(v)
			if err != nil {
				return nil, "", err
			}

		case "mode":
			switch network.Driver {
			case types.MacVLANNetworkDriver:
				if !pkgutil.StringInSlice(v, []string{"", "bridge", "private", "vepa", "passthru"}) {
					return nil, "", errors.Errorf("unknown macvlan mode %q", v)
				}
			case types.IPVLANNetworkDriver:
				if !pkgutil.StringInSlice(v, []string{"", "l2", "l3", "l3s"}) {
					return nil, "", errors.Errorf("unknown ipvlan mode %q", v)
				}
			default:
				return nil, "", errors.Errorf("cannot set option \"mode\" with driver %q", network.Driver)
			}
			vlanPluginMode = v

		default:
			return nil, "", errors.Errorf("unsupported network option %s", k)
		}
	}

	isGateway := true
	ipMasq := true
	if network.Internal {
		isGateway = false
		ipMasq = false
	}
	// create CNI plugin configuration
	// explicitly use CNI version 0.4.0 here, to use v1.0.0 at least containernetwork-plugins-1.0.1 has to be installed
	// the dnsname plugin also needs to be updated for 1.0.0
	// TODO change to 1.0.0 when most distros support it
	ncList := newNcList(network.Name, "0.4.0", network.Labels, network.Options)
	var plugins []interface{}

	switch network.Driver {
	case types.BridgeNetworkDriver:
		bridge := newHostLocalBridge(network.NetworkInterface, isGateway, ipMasq, mtu, vlan, ipamConf)
		plugins = append(plugins, bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin())
		// if we find the dnsname plugin we add configuration for it
		if hasDNSNamePlugin(n.cniPluginDirs) && network.DNSEnabled {
			// Note: in the future we might like to allow for dynamic domain names
			plugins = append(plugins, newDNSNamePlugin(defaultPodmanDomainName))
		}
		// Add the podman-machine CNI plugin if we are in a machine
		if n.isMachine {
			plugins = append(plugins, newPodmanMachinePlugin())
		}

	case types.MacVLANNetworkDriver:
		plugins = append(plugins, newVLANPlugin(types.MacVLANNetworkDriver, network.NetworkInterface, vlanPluginMode, mtu, ipamConf))

	case types.IPVLANNetworkDriver:
		plugins = append(plugins, newVLANPlugin(types.IPVLANNetworkDriver, network.NetworkInterface, vlanPluginMode, mtu, ipamConf))

	default:
		return nil, "", errors.Errorf("driver %q is not supported by cni", network.Driver)
	}
	ncList["plugins"] = plugins
	b, err := json.MarshalIndent(ncList, "", "   ")
	if err != nil {
		return nil, "", err
	}
	cniPathName := ""
	if writeToDisk {
		cniPathName = filepath.Join(n.cniConfigDir, network.Name+".conflist")
		err = ioutil.WriteFile(cniPathName, b, 0644)
		if err != nil {
			return nil, "", err
		}
		f, err := os.Stat(cniPathName)
		if err != nil {
			return nil, "", err
		}
		stat := f.Sys().(*syscall.Stat_t)
		network.Created = time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec))
	} else {
		network.Created = time.Now()
	}
	config, err := libcni.ConfListFromBytes(b)
	if err != nil {
		return nil, "", err
	}
	return config, cniPathName, nil
}

// parseMTU parses the mtu option
func parseMTU(mtu string) (int, error) {
	if mtu == "" {
		return 0, nil // default
	}
	m, err := strconv.Atoi(mtu)
	if err != nil {
		return 0, err
	}
	if m < 0 {
		return 0, errors.Errorf("mtu %d is less than zero", m)
	}
	return m, nil
}

// parseVlan parses the vlan option
func parseVlan(vlan string) (int, error) {
	if vlan == "" {
		return 0, nil // default
	}
	v, err := strconv.Atoi(vlan)
	if err != nil {
		return 0, err
	}
	if v < 0 || v > 4094 {
		return 0, errors.Errorf("vlan ID %d must be between 0 and 4094", v)
	}
	return v, nil
}

func convertSpecgenPortsToCNIPorts(ports []types.PortMapping) ([]cniPortMapEntry, error) {
	cniPorts := make([]cniPortMapEntry, 0, len(ports))
	for _, port := range ports {
		if port.Protocol == "" {
			return nil, errors.New("port protocol should not be empty")
		}
		protocols := strings.Split(port.Protocol, ",")

		for _, protocol := range protocols {
			if !pkgutil.StringInSlice(protocol, []string{"tcp", "udp", "sctp"}) {
				return nil, errors.Errorf("unknown port protocol %s", protocol)
			}
			cniPort := cniPortMapEntry{
				HostPort:      int(port.HostPort),
				ContainerPort: int(port.ContainerPort),
				HostIP:        port.HostIP,
				Protocol:      protocol,
			}
			cniPorts = append(cniPorts, cniPort)
			for i := 1; i < int(port.Range); i++ {
				cniPort := cniPortMapEntry{
					HostPort:      int(port.HostPort) + i,
					ContainerPort: int(port.ContainerPort) + i,
					HostIP:        port.HostIP,
					Protocol:      protocol,
				}
				cniPorts = append(cniPorts, cniPort)
			}
		}
	}
	return cniPorts, nil
}