diff --git a/cmd/nerdctl/network_create.go b/cmd/nerdctl/network_create.go index 712118adce7..6d7be030588 100644 --- a/cmd/nerdctl/network_create.go +++ b/cmd/nerdctl/network_create.go @@ -98,7 +98,15 @@ func networkCreateAction(cmd *cobra.Command, args []string) error { } labels := strutil.DedupeStrSlice(labels) - l, err := netutil.GenerateConfigList(e, labels, id, name, subnet) + ipam, err := netutil.GenerateIPAM("", subnet) + if err != nil { + return err + } + cniPlugins, err := netutil.GenerateCNIPlugins("", id, ipam) + if err != nil { + return err + } + l, err := netutil.GenerateConfigList(e, labels, id, name, cniPlugins) if err != nil { return err } diff --git a/cmd/nerdctl/network_rm.go b/cmd/nerdctl/network_rm.go index 3cfee32b978..50a3c207c3a 100644 --- a/cmd/nerdctl/network_rm.go +++ b/cmd/nerdctl/network_rm.go @@ -84,7 +84,7 @@ func networkRmAction(cmd *cobra.Command, args []string) error { } // Remove the bridge network interface on the host. if l.Plugins[0].Network.Type == "bridge" { - netIf := fmt.Sprintf("nerdctl%d", *l.NerdctlID) + netIf := netutil.GetBridgeName(*l.NerdctlID) removeBridgeNetworkInterface(netIf) } fmt.Fprintln(cmd.OutOrStdout(), name) diff --git a/docs/cni.md b/docs/cni.md index efb2e0f3c9c..9b6029601a4 100644 --- a/docs/cni.md +++ b/docs/cni.md @@ -13,6 +13,53 @@ system, the supported CNI plugin types are `nat` only. The default network `bridge` for Linux and `nat` for Windows if you don't set any network options. +Configuration of the default network `bridge` of Linux: + +```json +{ + "cniVersion": "0.4.0", + "name": "bridge", + "plugins": [ + { + "type": "bridge", + "bridge": "nerdctl0", + "isGateway": true, + "ipMasq": true, + "hairpinMode": true, + "ipam": { + "type": "host-local", + "routes": [{ "dst": "0.0.0.0/0" }], + "ranges": [ + [ + { + "subnet": "10.4.0.1", + "gateway": "10.4.0.0/24" + } + ] + ] + } + }, + { + "type": "portmap", + "capabilities": { + "portMappings": true + } + }, + { + "type": "firewall" + }, + { + "type": "tuning" + }, + { + "type": "isolation" + } + ] +} +``` + +When CNI plugin `isolation` be installed, will inject isolation configuration `{"type":"isolation"}` automatically. + ## Custom networks You can also customize your CNI network by providing configuration files. diff --git a/pkg/netutil/cni_plugin.go b/pkg/netutil/cni_plugin.go new file mode 100644 index 00000000000..daf98f28c7e --- /dev/null +++ b/pkg/netutil/cni_plugin.go @@ -0,0 +1,48 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package netutil + +type CNIPlugin interface { + GetPluginType() string +} + +type IPRange struct { + Subnet string `json:"subnet"` + RangeStart string `json:"rangeStart,omitempty"` + RangeEnd string `json:"rangeEnd,omitempty"` + Gateway string `json:"gateway,omitempty"` +} + +type IPAMRoute struct { + Dst string `json:"dst,omitempty"` + GW string `json:"gw,omitempty"` + Gateway string `json:"gateway,omitempty"` +} + +type isolationConfig struct { + PluginType string `json:"type"` +} + +func newIsolationPlugin() *isolationConfig { + return &isolationConfig{ + PluginType: "isolation", + } +} + +func (*isolationConfig) GetPluginType() string { + return "isolation" +} diff --git a/pkg/netutil/cni_plugin_unix.go b/pkg/netutil/cni_plugin_unix.go new file mode 100644 index 00000000000..c8f24aa4100 --- /dev/null +++ b/pkg/netutil/cni_plugin_unix.go @@ -0,0 +1,111 @@ +//go:build freebsd || linux +// +build freebsd linux + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package netutil + +// bridgeConfig describes the bridge plugin +type bridgeConfig struct { + PluginType string `json:"type"` + BrName string `json:"bridge,omitempty"` + IsGW bool `json:"isGateway,omitempty"` + IsDefaultGW bool `json:"isDefaultGateway,omitempty"` + ForceAddress bool `json:"forceAddress,omitempty"` + IPMasq bool `json:"ipMasq,omitempty"` + MTU int `json:"mtu,omitempty"` + HairpinMode bool `json:"hairpinMode,omitempty"` + PromiscMode bool `json:"promiscMode,omitempty"` + Vlan int `json:"vlan,omitempty"` + IPAM map[string]interface{} `json:"ipam"` +} + +func newBridgePlugin(bridgeName string) *bridgeConfig { + return &bridgeConfig{ + PluginType: "bridge", + BrName: bridgeName, + } +} + +func (*bridgeConfig) GetPluginType() string { + return "bridge" +} + +// portMapConfig describes the portmapping plugin +type portMapConfig struct { + PluginType string `json:"type"` + Capabilities map[string]bool `json:"capabilities"` +} + +func newPortMapPlugin() *portMapConfig { + return &portMapConfig{ + PluginType: "portmap", + Capabilities: map[string]bool{ + "portMappings": true, + }, + } +} + +func (*portMapConfig) GetPluginType() string { + return "portmap" +} + +// firewallConfig describes the firewall plugin +type firewallConfig struct { + PluginType string `json:"type"` + Backend string `json:"backend,omitempty"` +} + +func newFirewallPlugin() *firewallConfig { + return &firewallConfig{ + PluginType: "firewall", + } +} + +func (*firewallConfig) GetPluginType() string { + return "firewall" +} + +// tuningConfig describes the tuning plugin +type tuningConfig struct { + PluginType string `json:"type"` +} + +func newTuningPlugin() *tuningConfig { + return &tuningConfig{ + PluginType: "tuning", + } +} + +func (*tuningConfig) GetPluginType() string { + return "tuning" +} + +// https://github.com/containernetworking/plugins/blob/v1.0.1/plugins/ipam/host-local/backend/allocator/config.go#L47-L56 +type hostLocalIPAMConfig struct { + Type string `json:"type"` + Routes []IPAMRoute `json:"routes,omitempty"` + ResolveConf string `json:"resolveConf,omitempty"` + DataDir string `json:"dataDir,omitempty"` + Ranges [][]IPRange `json:"ranges,omitempty"` +} + +func newHostLocalIPAMConfig() *hostLocalIPAMConfig { + return &hostLocalIPAMConfig{ + Type: "host-local", + } +} diff --git a/pkg/netutil/cni_plugin_windows.go b/pkg/netutil/cni_plugin_windows.go new file mode 100644 index 00000000000..e8320b10c7e --- /dev/null +++ b/pkg/netutil/cni_plugin_windows.go @@ -0,0 +1,49 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package netutil + +type natConfig struct { + PluginType string `json:"type"` + Master string `json:"master,omitempty"` + IPAM map[string]interface{} `json:"ipam"` +} + +func (*natConfig) GetPluginType() string { + return "nat" +} + +func newNatPlugin(master string) *natConfig { + return &natConfig{ + PluginType: "nat", + Master: master, + } +} + +// https://github.com/microsoft/windows-container-networking/blob/v0.2.0/cni/cni.go#L55-L63 +type windowsIpamConfig struct { + Type string `json:"type"` + Environment string `json:"environment,omitempty"` + AddrSpace string `json:"addressSpace,omitempty"` + Subnet string `json:"subnet,omitempty"` + Address string `json:"ipAddress,omitempty"` + QueryInterval string `json:"queryInterval,omitempty"` + Routes []IPAMRoute `json:"routes,omitempty"` +} + +func newWindowsIPAMConfig() *windowsIpamConfig { + return &windowsIpamConfig{} +} diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index 76fb7c07dfe..3cff61f6210 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -17,7 +17,6 @@ package netutil import ( - "bytes" "encoding/json" "fmt" "net" @@ -26,7 +25,6 @@ import ( "path/filepath" "sort" "strings" - "text/template" "github.com/containerd/containerd/errdefs" "github.com/containerd/nerdctl/pkg/strutil" @@ -48,78 +46,62 @@ type CNIEnv struct { } func DefaultConfigList(e *CNIEnv) (*NetworkConfigList, error) { - return GenerateConfigList(e, nil, DefaultID, DefaultNetworkName, DefaultCIDR) + ipam, _ := GenerateIPAM("", DefaultCIDR) + plugins, _ := GenerateCNIPlugins("", DefaultID, ipam) + return GenerateConfigList(e, nil, DefaultID, DefaultNetworkName, plugins) } -type ConfigListTemplateOpts struct { - ID int - Name string // e.g. "nerdctl" - Labels string // e.g. `{"version":"1.1.0"}` - Subnet string // e.g. "10.4.0.0/24" - Gateway string // e.g. "10.4.0.1" - ExtraPlugins string // e.g. `,{"type":"isolation"}` +type cniNetworkConfig struct { + CNIVersion string `json:"cniVersion"` + Name string `json:"name"` + ID int `json:"nerdctlID"` + Labels map[string]string `json:"nerdctlLabels"` + Plugins []CNIPlugin `json:"plugins"` } // GenerateConfigList creates NetworkConfigList. // GenerateConfigList does not fill "File" field. // // TODO: enable CNI isolation plugin -func GenerateConfigList(e *CNIEnv, labels []string, id int, name, cidr string) (*NetworkConfigList, error) { - if e == nil || id < 0 || name == "" || cidr == "" { +func GenerateConfigList(e *CNIEnv, labels []string, id int, name string, plugins []CNIPlugin) (*NetworkConfigList, error) { + if e == nil || id < 0 || name == "" || len(plugins) == 0 { return nil, errdefs.ErrInvalidArgument } - for _, f := range basicPlugins { - p := filepath.Join(e.Path, f) + for _, f := range plugins { + p := filepath.Join(e.Path, f.GetPluginType()) if _, err := exec.LookPath(p); err != nil { return nil, fmt.Errorf("needs CNI plugin %q to be installed in CNI_PATH (%q), see https://github.com/containernetworking/plugins/releases: %w", f, e.Path, err) } } - var extraPlugins string + var extraPlugin CNIPlugin if _, err := exec.LookPath(filepath.Join(e.Path, "isolation")); err == nil { logrus.Debug("found CNI isolation plugin") - extraPlugins = ",\n {\n \"type\":\"isolation\"\n }" + extraPlugin = newIsolationPlugin() } else if name != DefaultNetworkName { // the warning is suppressed for DefaultNetworkName logrus.Warnf("To isolate bridge networks, CNI plugin \"isolation\" needs to be installed in CNI_PATH (%q), see https://github.com/AkihiroSuda/cni-isolation", e.Path) } - subnetIP, subnet, err := net.ParseCIDR(cidr) - if err != nil { - return nil, fmt.Errorf("failed to parse CIDR %q", cidr) - } - if !subnet.IP.Equal(subnetIP) { - return nil, fmt.Errorf("unexpected CIDR %q, maybe you meant %q?", cidr, subnet.String()) + if extraPlugin != nil { + plugins = append(plugins, extraPlugin) } - gateway := make(net.IP, len(subnet.IP)) - copy(gateway, subnet.IP) - gateway[3] += 1 - labelsMap := strutil.ConvertKVStringsToMap(labels) - labelsJson, err := json.Marshal(labelsMap) - if err != nil { - return nil, err - } - opts := &ConfigListTemplateOpts{ - ID: id, - Name: name, - Labels: string(labelsJson), - Subnet: subnet.String(), - Gateway: gateway.String(), - ExtraPlugins: extraPlugins, + conf := &cniNetworkConfig{ + CNIVersion: "0.4.0", + Name: name, + ID: id, + Labels: labelsMap, + Plugins: plugins, } - tmpl, err := template.New("").Parse(ConfigListTemplate) + confJSON, err := json.MarshalIndent(conf, "", " ") if err != nil { return nil, err } - var buf bytes.Buffer - if err := tmpl.Execute(&buf, opts); err != nil { - return nil, err - } - l, err := libcni.ConfListFromBytes(buf.Bytes()) + l, err := libcni.ConfListFromBytes(confJSON) if err != nil { return nil, err } @@ -211,3 +193,36 @@ func NerdctlLabels(b []byte) *map[string]string { } return ncl.NerdctlLabels } + +func GetBridgeName(id int) string { + return fmt.Sprintf("nerdctl%d", id) +} + +func parseSubnet(subnetStr string) (*net.IPNet, net.IP, error) { + subnetIP, subnet, err := net.ParseCIDR(subnetStr) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse subnet %q", subnetStr) + } + if !subnet.IP.Equal(subnetIP) { + return nil, nil, fmt.Errorf("unexpected subnet %q, maybe you meant %q?", subnetStr, subnet.String()) + } + + gateway := make(net.IP, len(subnet.IP)) + copy(gateway, subnet.IP) + gateway[3] += 1 + + return subnet, gateway, nil +} + +// convert the struct to a map +func structToMap(in interface{}) (map[string]interface{}, error) { + out := make(map[string]interface{}) + data, err := json.Marshal(in) + if err != nil { + return nil, err + } + if err := json.Unmarshal(data, &out); err != nil { + return nil, err + } + return out, nil +} diff --git a/pkg/netutil/netutil_unix.go b/pkg/netutil/netutil_unix.go index 7bc04612fa8..306e9b66d33 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -19,52 +19,67 @@ package netutil +import ( + "fmt" +) + const ( DefaultNetworkName = "bridge" DefaultID = 0 DefaultCIDR = "10.4.0.0/24" + DefaultCNIPlugin = "bridge" + DefaultIPAMDriver = "host-local" ) -// basicPlugins is used by ConfigListTemplate -var basicPlugins = []string{"bridge", "portmap", "firewall", "tuning"} +func GenerateCNIPlugins(driver string, id int, ipam map[string]interface{}) ([]CNIPlugin, error) { + if driver == "" { + driver = DefaultCNIPlugin + } + var plugins []CNIPlugin + switch driver { + case "bridge": + bridge := newBridgePlugin(GetBridgeName(id)) + bridge.IPAM = ipam + bridge.IsGW = true + bridge.IPMasq = true + bridge.HairpinMode = true + plugins = []CNIPlugin{bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin()} + default: + return nil, fmt.Errorf("unsupported cni driver %q", driver) + } + return plugins, nil +} + +func GenerateIPAM(driver string, subnetStr string) (map[string]interface{}, error) { + if driver == "" { + driver = DefaultIPAMDriver + } + subnet, gateway, err := parseSubnet(subnetStr) + if err != nil { + return nil, err + } + + var ipamConfig interface{} + switch driver { + case "host-local": + ipamConf := newHostLocalIPAMConfig() + ipamConf.Routes = []IPAMRoute{ + {Dst: "0.0.0.0/0"}, + } + ipamConf.Ranges = append(ipamConf.Ranges, []IPRange{ + { + Subnet: subnet.String(), + Gateway: gateway.String(), + }, + }) + ipamConfig = ipamConf + default: + return nil, fmt.Errorf("unsupported ipam driver %q", driver) + } -// ConfigListTemplate was copied from https://github.com/containers/podman/blob/v2.2.0/cni/87-podman-bridge.conflist -const ConfigListTemplate = `{ - "cniVersion": "0.4.0", - "name": "{{.Name}}", - "nerdctlID": {{.ID}}, - "nerdctlLabels": {{.Labels}}, - "plugins": [ - { - "type": "bridge", - "bridge": "nerdctl{{.ID}}", - "isGateway": true, - "ipMasq": true, - "hairpinMode": true, - "ipam": { - "type": "host-local", - "routes": [{ "dst": "0.0.0.0/0" }], - "ranges": [ - [ - { - "subnet": "{{.Subnet}}", - "gateway": "{{.Gateway}}" - } - ] - ] - } - }, - { - "type": "portmap", - "capabilities": { - "portMappings": true - } - }, - { - "type": "firewall" - }, - { - "type": "tuning" - }{{.ExtraPlugins}} - ] -}` + ipam, err := structToMap(ipamConfig) + if err != nil { + return nil, err + } + return ipam, nil +} diff --git a/pkg/netutil/netutil_windows.go b/pkg/netutil/netutil_windows.go index 464cd94b4a7..f9fcebd2cf7 100644 --- a/pkg/netutil/netutil_windows.go +++ b/pkg/netutil/netutil_windows.go @@ -16,43 +16,53 @@ package netutil +import ( + "fmt" +) + const ( DefaultNetworkName = "nat" DefaultID = 0 DefaultCIDR = "10.4.0.0/24" + DefaultCNIPlugin = "nat" ) -// basicPlugins is used by ConfigListTemplate -var basicPlugins = []string{"nat"} - -// ConfigListTemplate was copied from https://github.com/containers/podman/blob/v2.2.0/cni/87-podman-bridge.conflist - -const ConfigListTemplate = `{ - "cniVersion": "0.4.0", - "name": "{{.Name}}", - "nerdctlID": {{.ID}}, - "nerdctlLabels": {{.Labels}}, - "plugins": [ - { - "type": "nat", - "master": "Ethernet", - "ipam": { - "subnet": "{{.Subnet}}", - "routes": [ - { - "gateway": "{{.Gateway}}" - } - ] - } - }, - { - "type": "portmap", - "capabilities": { - "portMappings": true - } - }, - { - "type": "tuning" - }{{.ExtraPlugins}} - ] -}` +func GenerateCNIPlugins(driver string, id int, ipam map[string]interface{}) ([]CNIPlugin, error) { + if driver == "" { + driver = DefaultCNIPlugin + } + var plugins []CNIPlugin + switch driver { + case "nat": + nat := newNatPlugin("Ethernet") + nat.IPAM = ipam + plugins = []CNIPlugin{nat} + default: + return nil, fmt.Errorf("unsupported cni driver %q", driver) + } + return plugins, nil +} + +func GenerateIPAM(driver string, subnetStr string) (map[string]interface{}, error) { + subnet, gateway, err := parseSubnet(subnetStr) + if err != nil { + return nil, err + } + + var ipamConfig interface{} + switch driver { + case "": + ipamConf := newWindowsIPAMConfig() + ipamConf.Subnet = subnet.String() + ipamConf.Routes = append(ipamConf.Routes, IPAMRoute{Gateway: gateway.String()}) + ipamConfig = ipamConf + default: + return nil, fmt.Errorf("unsupported ipam driver %q", driver) + } + + ipam, err := structToMap(ipamConfig) + if err != nil { + return nil, err + } + return ipam, nil +}