diff --git a/cmd/nerdctl/network_create.go b/cmd/nerdctl/network_create.go index af745af060e..094e9928c16 100644 --- a/cmd/nerdctl/network_create.go +++ b/cmd/nerdctl/network_create.go @@ -19,8 +19,8 @@ package main import ( "fmt" + "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/identifiers" - "github.com/containerd/nerdctl/pkg/lockutil" "github.com/containerd/nerdctl/pkg/netutil" "github.com/containerd/nerdctl/pkg/strutil" @@ -95,52 +95,36 @@ func networkCreateAction(cmd *cobra.Command, args []string) error { if err != nil { return err } + labels = strutil.DedupeStrSlice(labels) - fn := func() error { - e, err := netutil.NewCNIEnv(cniPath, cniNetconfpath) - if err != nil { - return err - } - for _, n := range e.Networks { - if n.Name == name { - return fmt.Errorf("network with name %s already exists", name) - } - // TODO: check CIDR collision - } - id, err := e.AcquireNextID() - if err != nil { - return err - } - - if subnetStr == "" { - if gatewayStr != "" || ipRangeStr != "" { - return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually") - } - if id > 255 { - return fmt.Errorf("cannot determine subnet for ID %d, specify --subnet manually", id) - } - subnetStr = fmt.Sprintf("10.4.%d.0/24", id) + if subnetStr == "" { + if gatewayStr != "" || ipRangeStr != "" { + return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually") } + } - labels := strutil.DedupeStrSlice(labels) - ipam, err := netutil.GenerateIPAM(ipamDriver, subnetStr, gatewayStr, ipRangeStr, strutil.ConvertKVStringsToMap(ipamOpts)) - if err != nil { - return err - } - cniPlugins, err := e.GenerateCNIPlugins(driver, id, name, ipam, strutil.ConvertKVStringsToMap(opts)) - if err != nil { - return err - } - net, err := e.GenerateNetworkConfig(labels, id, name, cniPlugins) - if err != nil { - return err - } - if err := e.WriteNetworkConfig(net); err != nil { - return err + e, err := netutil.NewCNIEnv(cniPath, cniNetconfpath) + if err != nil { + return err + } + createOpts := netutil.CreateOptions{ + Name: name, + Driver: driver, + Options: strutil.ConvertKVStringsToMap(opts), + IPAMDriver: ipamDriver, + IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts), + Subnet: subnetStr, + Gateway: gatewayStr, + IPRange: ipRangeStr, + Labels: labels, + } + net, err := e.CreateNetwork(createOpts) + if err != nil { + if errdefs.IsAlreadyExists(err) { + return fmt.Errorf("network with name %s already exists", name) } - fmt.Fprintf(cmd.OutOrStdout(), "%d\n", id) - return nil + return err } - - return lockutil.WithDirLock(cniNetconfpath, fn) + _, err = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", *net.NerdctlID) + return err } diff --git a/cmd/nerdctl/network_create_linux_test.go b/cmd/nerdctl/network_create_linux_test.go index ca52f122775..f61ad37566c 100644 --- a/cmd/nerdctl/network_create_linux_test.go +++ b/cmd/nerdctl/network_create_linux_test.go @@ -20,9 +20,11 @@ import ( "testing" "github.com/containerd/nerdctl/pkg/testutil" + "gotest.tools/v3/assert" ) func TestNetworkCreateWithMTU(t *testing.T) { + t.Parallel() testNetwork := testutil.Identifier(t) base := testutil.NewBase(t) @@ -35,3 +37,22 @@ func TestNetworkCreateWithMTU(t *testing.T) { base.Cmd("run", "--rm", "--net", testNetwork, testutil.AlpineImage, "ifconfig", "eth0").AssertOutContains("MTU:9216") } + +func TestNetworkCreate(t *testing.T) { + t.Parallel() + base := testutil.NewBase(t) + testNetwork := testutil.Identifier(t) + + base.Cmd("network", "create", testNetwork).AssertOK() + defer base.Cmd("network", "rm", testNetwork).Run() + + net := base.InspectNetwork(testNetwork) + assert.Equal(t, len(net.IPAM.Config), 1) + + base.Cmd("run", "--rm", "--net", testNetwork, testutil.CommonImage, "ip", "route").AssertOutContains(net.IPAM.Config[0].Subnet) + + base.Cmd("network", "create", testNetwork+"-1").AssertOK() + defer base.Cmd("network", "rm", testNetwork+"-1").Run() + + base.Cmd("run", "--rm", "--net", testNetwork+"-1", testutil.CommonImage, "ip", "route").AssertNoOut(net.IPAM.Config[0].Subnet) +} diff --git a/cmd/nerdctl/network_ls.go b/cmd/nerdctl/network_ls.go index e65251589a5..78cacdd014f 100644 --- a/cmd/nerdctl/network_ls.go +++ b/cmd/nerdctl/network_ls.go @@ -20,7 +20,6 @@ import ( "bytes" "errors" "fmt" - "strconv" "text/tabwriter" "text/template" @@ -105,7 +104,10 @@ func networkLsAction(cmd *cobra.Command, args []string) error { file: n.File, } if n.NerdctlID != nil { - p.ID = strconv.Itoa(*n.NerdctlID) + p.ID = *n.NerdctlID + if len(p.ID) > 12 { + p.ID = p.ID[:12] + } } if n.NerdctlLabels != nil { p.Labels = formatLabels(*n.NerdctlLabels) diff --git a/cmd/nerdctl/network_rm.go b/cmd/nerdctl/network_rm.go index ca7a0949bb8..4b5e6e3326c 100644 --- a/cmd/nerdctl/network_rm.go +++ b/cmd/nerdctl/network_rm.go @@ -18,9 +18,7 @@ package main import ( "fmt" - "os" - "github.com/containerd/nerdctl/pkg/lockutil" "github.com/containerd/nerdctl/pkg/netutil" "github.com/spf13/cobra" @@ -54,36 +52,28 @@ func networkRmAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - fn := func() error { - netMap := e.NetworkMap() + netMap := e.NetworkMap() - for _, name := range args { - if name == "host" || name == "none" { - return fmt.Errorf("pseudo network %q cannot be removed", name) - } - l, ok := netMap[name] - if !ok { - return fmt.Errorf("no such network: %s", name) - } - if l.NerdctlID == nil { - return fmt.Errorf("%s is managed outside nerdctl and cannot be removed", name) - } - if l.File == "" { - return fmt.Errorf("%s is a pre-defined network and cannot be removed", name) - } - if err := os.RemoveAll(l.File); err != nil { - return err - } - // Remove the bridge network interface on the host. - if l.Plugins[0].Network.Type == "bridge" { - netIf := netutil.GetBridgeName(*l.NerdctlID) - removeBridgeNetworkInterface(netIf) - } - fmt.Fprintln(cmd.OutOrStdout(), name) + for _, name := range args { + if name == "host" || name == "none" { + return fmt.Errorf("pseudo network %q cannot be removed", name) } - return nil + net, ok := netMap[name] + if !ok { + return fmt.Errorf("no such network: %s", name) + } + if net.NerdctlID == nil { + return fmt.Errorf("%s is managed outside nerdctl and cannot be removed", name) + } + if net.File == "" { + return fmt.Errorf("%s is a pre-defined network and cannot be removed", name) + } + if err := e.RemoveNetwork(net); err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), name) } - return lockutil.WithDirLock(cniNetconfpath, fn) + return nil } func networkRmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/nerdctl/network_rm_freebsd.go b/cmd/nerdctl/network_rm_freebsd.go deleted file mode 100644 index 0e72074e0c3..00000000000 --- a/cmd/nerdctl/network_rm_freebsd.go +++ /dev/null @@ -1,19 +0,0 @@ -/* - 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 main - -func removeBridgeNetworkInterface(name string) {} diff --git a/cmd/nerdctl/network_rm_linux_test.go b/cmd/nerdctl/network_rm_linux_test.go new file mode 100644 index 00000000000..5507eacef22 --- /dev/null +++ b/cmd/nerdctl/network_rm_linux_test.go @@ -0,0 +1,51 @@ +/* + 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 main + +import ( + "testing" + + "github.com/containerd/nerdctl/pkg/rootlessutil" + "github.com/containerd/nerdctl/pkg/testutil" + "github.com/vishvananda/netlink" + "gotest.tools/v3/assert" +) + +func TestNetworkRemove(t *testing.T) { + t.Parallel() + if rootlessutil.IsRootless() { + t.Skip("test skipped for remove rootless network") + } + base := testutil.NewBase(t) + networkName := testutil.Identifier(t) + + base.Cmd("network", "create", networkName).AssertOK() + defer base.Cmd("network", "rm", networkName).Run() + + networkID := base.InspectNetwork(networkName).ID + + tID := testutil.Identifier(t) + base.Cmd("run", "--rm", "--net", networkName, "--name", tID, testutil.CommonImage).AssertOK() + + _, err := netlink.LinkByName("br-" + networkID[:12]) + assert.NilError(t, err) + + base.Cmd("network", "rm", networkName).AssertOK() + + _, err = netlink.LinkByName("br-" + networkID[:12]) + assert.Error(t, err, "Link not found") +} diff --git a/cmd/nerdctl/network_rm_windows.go b/cmd/nerdctl/network_rm_windows.go deleted file mode 100644 index 0e72074e0c3..00000000000 --- a/cmd/nerdctl/network_rm_windows.go +++ /dev/null @@ -1,19 +0,0 @@ -/* - 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 main - -func removeBridgeNetworkInterface(name string) {} diff --git a/go.mod b/go.mod index 1955d3b3ea9..1de07dc1e30 100644 --- a/go.mod +++ b/go.mod @@ -124,7 +124,7 @@ require ( github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 github.com/moby/locker v1.0.1 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/moby/sys/signal v0.7.0 diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index a35b07630ff..c5997825a7d 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -463,7 +463,7 @@ func NetworkFromNative(n *native.Network) (*Network, error) { } if n.NerdctlID != nil { - res.ID = strconv.Itoa(*n.NerdctlID) + res.ID = *n.NerdctlID } if n.NerdctlLabels != nil { diff --git a/pkg/inspecttypes/native/network.go b/pkg/inspecttypes/native/network.go index dadeb62a817..18d5e96bb1e 100644 --- a/pkg/inspecttypes/native/network.go +++ b/pkg/inspecttypes/native/network.go @@ -21,7 +21,7 @@ import "encoding/json" // Network corresponds to pkg/netutil.NetworkConfigList type Network struct { CNI json.RawMessage `json:"CNI,omitempty"` - NerdctlID *int `json:"NerdctlID"` + NerdctlID *string `json:"NerdctlID"` NerdctlLabels *map[string]string `json:"NerdctlLabels,omitempty"` File string `json:"File,omitempty"` } diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index 0898af06909..6ba008d5aed 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -17,6 +17,8 @@ package netutil import ( + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "net" @@ -28,6 +30,8 @@ import ( "strings" "github.com/containerd/containerd/errdefs" + "github.com/containerd/nerdctl/pkg/lockutil" + subnetutil "github.com/containerd/nerdctl/pkg/netutil/subnet" "github.com/containerd/nerdctl/pkg/strutil" "github.com/containernetworking/cni/libcni" ) @@ -43,15 +47,17 @@ func NewCNIEnv(cniPath, cniConfPath string) (*CNIEnv, error) { Path: cniPath, NetconfPath: cniConfPath, } - var err error - err = e.ensureDefaultNetworkConfig() - if err != nil { + if err := os.MkdirAll(e.NetconfPath, 0755); err != nil { + return nil, err + } + if err := e.ensureDefaultNetworkConfig(); err != nil { return nil, err } - e.Networks, err = e.networkConfigList() + networks, err := e.networkConfigList() if err != nil { return nil, err } + e.Networks = networks return &e, nil } @@ -63,9 +69,20 @@ func (e *CNIEnv) NetworkMap() map[string]*networkConfig { return m } +func (e *CNIEnv) usedSubnets() ([]*net.IPNet, error) { + usedSubnets, err := subnetutil.GetLiveNetworkSubnets() + if err != nil { + return nil, err + } + for _, net := range e.Networks { + usedSubnets = append(usedSubnets, net.subnets()...) + } + return usedSubnets, nil +} + type networkConfig struct { *libcni.NetworkConfigList - NerdctlID *int + NerdctlID *string NerdctlLabels *map[string]string File string } @@ -73,15 +90,89 @@ type networkConfig struct { type cniNetworkConfig struct { CNIVersion string `json:"cniVersion"` Name string `json:"name"` - ID int `json:"nerdctlID"` + ID string `json:"nerdctlID"` Labels map[string]string `json:"nerdctlLabels"` Plugins []CNIPlugin `json:"plugins"` } -// GenerateNetworkConfig creates networkConfig. -// GenerateNetworkConfig does not fill "File" field. -func (e *CNIEnv) GenerateNetworkConfig(labels []string, id int, name string, plugins []CNIPlugin) (*networkConfig, error) { - if e == nil || id < 0 || name == "" || len(plugins) == 0 { +type CreateOptions struct { + Name string + Driver string + Options map[string]string + IPAMDriver string + IPAMOptions map[string]string + Subnet string + Gateway string + IPRange string + Labels []string +} + +func (e *CNIEnv) CreateNetwork(opts CreateOptions) (*networkConfig, error) { + var net *networkConfig + if _, ok := e.NetworkMap()[opts.Name]; ok { + return nil, errdefs.ErrAlreadyExists + } + + fn := func() error { + ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnet, opts.Gateway, opts.IPRange, opts.IPAMOptions) + if err != nil { + return err + } + plugins, err := e.generateCNIPlugins(opts.Driver, opts.Name, ipam, opts.Options) + if err != nil { + return err + } + net, err = e.generateNetworkConfig(opts.Name, opts.Labels, plugins) + if err != nil { + return err + } + if err := e.writeNetworkConfig(net); err != nil { + return err + } + return nil + } + err := lockutil.WithDirLock(e.NetconfPath, fn) + if err != nil { + return nil, err + } + return net, nil +} + +func (e *CNIEnv) RemoveNetwork(net *networkConfig) error { + fn := func() error { + if err := os.RemoveAll(net.File); err != nil { + return err + } + if err := net.clean(); err != nil { + return err + } + return nil + } + return lockutil.WithDirLock(e.NetconfPath, fn) +} + +func (e *CNIEnv) ensureDefaultNetworkConfig() error { + filename := filepath.Join(e.NetconfPath, "nerdctl-"+DefaultNetworkName+".conflist") + if _, err := os.Stat(filename); err == nil { + return nil + } + opts := CreateOptions{ + Name: DefaultNetworkName, + Driver: DefaultNetworkName, + Subnet: DefaultCIDR, + IPAMDriver: "default", + } + _, err := e.CreateNetwork(opts) + if err != nil && !errdefs.IsAlreadyExists(err) { + return err + } + return nil +} + +// generateNetworkConfig creates networkConfig. +// generateNetworkConfig does not fill "File" field. +func (e *CNIEnv) generateNetworkConfig(name string, labels []string, plugins []CNIPlugin) (*networkConfig, error) { + if name == "" || len(plugins) == 0 { return nil, errdefs.ErrInvalidArgument } for _, f := range plugins { @@ -90,6 +181,7 @@ func (e *CNIEnv) GenerateNetworkConfig(labels []string, id int, name string, plu return nil, fmt.Errorf("needs CNI plugin %q to be installed in CNI_PATH (%q), see https://github.com/containernetworking/plugins/releases: %w", f.GetPluginType(), e.Path, err) } } + id := networkID(name) labelsMap := strutil.ConvertKVStringsToMap(labels) conf := &cniNetworkConfig{ @@ -117,11 +209,8 @@ func (e *CNIEnv) GenerateNetworkConfig(labels []string, id int, name string, plu }, nil } -// WriteNetworkConfig writes networkConfig file to cni config path. -func (e *CNIEnv) WriteNetworkConfig(net *networkConfig) error { - if err := os.MkdirAll(e.NetconfPath, 0755); err != nil { - return err - } +// writeNetworkConfig writes networkConfig file to cni config path. +func (e *CNIEnv) writeNetworkConfig(net *networkConfig) error { filename := filepath.Join(e.NetconfPath, "nerdctl-"+net.Name+".conflist") if _, err := os.Stat(filename); err == nil { return errdefs.ErrAlreadyExists @@ -132,20 +221,6 @@ func (e *CNIEnv) WriteNetworkConfig(net *networkConfig) error { return nil } -func (e *CNIEnv) ensureDefaultNetworkConfig() error { - ipam, _ := GenerateIPAM("default", DefaultCIDR, "", "", nil) - plugins, _ := e.GenerateCNIPlugins(DefaultNetworkName, DefaultID, DefaultNetworkName, ipam, nil) - conf, err := e.GenerateNetworkConfig(nil, DefaultID, DefaultNetworkName, plugins) - if err != nil { - return err - } - err = e.WriteNetworkConfig(conf) - if err != nil && !errdefs.IsAlreadyExists(err) { - return err - } - return nil -} - // networkConfigList loads config from dir if dir exists. func (e *CNIEnv) networkConfigList() ([]*networkConfig, error) { l := []*networkConfig{} @@ -171,56 +246,48 @@ func (e *CNIEnv) networkConfigList() ([]*networkConfig, error) { return nil, err } } + id, labels := nerdctlIDLabels(lcl.Bytes) l = append(l, &networkConfig{ NetworkConfigList: lcl, - NerdctlID: nerdctlID(lcl.Bytes), - NerdctlLabels: nerdctlLabels(lcl.Bytes), + NerdctlID: id, + NerdctlLabels: labels, File: fileName, }) } return l, nil } -// AcquireNextID suggests the next ID. -func (e *CNIEnv) AcquireNextID() (int, error) { - maxID := DefaultID - for _, n := range e.Networks { - if n.NerdctlID != nil && *n.NerdctlID > maxID { - maxID = *n.NerdctlID - } +func nerdctlIDLabels(b []byte) (*string, *map[string]string) { + type idLabels struct { + ID *string `json:"nerdctlID,omitempty"` + Labels *map[string]string `json:"nerdctlLabels,omitempty"` + } + var idl idLabels + if err := json.Unmarshal(b, &idl); err != nil { + return nil, nil } - nextID := maxID + 1 - return nextID, nil + return idl.ID, idl.Labels } -func nerdctlID(b []byte) *int { - type nerdctlConfigList struct { - NerdctlID *int `json:"nerdctlID,omitempty"` - } - var ncl nerdctlConfigList - if err := json.Unmarshal(b, &ncl); err != nil { - // The network is managed outside nerdctl - return nil - } - return ncl.NerdctlID +func networkID(name string) string { + hash := sha256.Sum256([]byte(name)) + return hex.EncodeToString(hash[:]) } -func nerdctlLabels(b []byte) *map[string]string { - type nerdctlConfigList struct { - NerdctlLabels *map[string]string `json:"nerdctlLabels,omitempty"` +func (e *CNIEnv) parseSubnet(subnetStr string) (*net.IPNet, error) { + usedSubnets, err := e.usedSubnets() + if err != nil { + return nil, err } - var ncl nerdctlConfigList - if err := json.Unmarshal(b, &ncl); err != nil { - return nil + if subnetStr == "" { + _, defaultSubnet, _ := net.ParseCIDR(DefaultCIDR) + subnet, err := subnetutil.GetFreeSubnet(defaultSubnet, usedSubnets) + if err != nil { + return nil, err + } + return subnet, nil } - return ncl.NerdctlLabels -} - -func GetBridgeName(id int) string { - return fmt.Sprintf("nerdctl%d", id) -} -func parseIPAMRange(subnetStr, gatewayStr, ipRangeStr string) (*IPAMRange, error) { subnetIP, subnet, err := net.ParseCIDR(subnetStr) if err != nil { return nil, fmt.Errorf("failed to parse subnet %q", subnetStr) @@ -228,7 +295,13 @@ func parseIPAMRange(subnetStr, gatewayStr, ipRangeStr string) (*IPAMRange, error if !subnet.IP.Equal(subnetIP) { return nil, fmt.Errorf("unexpected subnet %q, maybe you meant %q?", subnetStr, subnet.String()) } + if subnetutil.IntersectsWithNetworks(subnet, usedSubnets) { + return nil, fmt.Errorf("subnet %s overlaps with other one on this address space", subnetStr) + } + return subnet, nil +} +func parseIPAMRange(subnet *net.IPNet, gatewayStr, ipRangeStr string) (*IPAMRange, error) { var gateway, rangeStart, rangeEnd net.IP if gatewayStr != "" { gatewayIP := net.ParseIP(gatewayStr) @@ -236,11 +309,11 @@ func parseIPAMRange(subnetStr, gatewayStr, ipRangeStr string) (*IPAMRange, error return nil, fmt.Errorf("failed to parse gateway %q", gatewayStr) } if !subnet.Contains(gatewayIP) { - return nil, fmt.Errorf("no matching subnet %q for gateway %q", subnetStr, gatewayStr) + return nil, fmt.Errorf("no matching subnet %q for gateway %q", subnet, gatewayStr) } gateway = gatewayIP } else { - gateway, _ = firstIPInSubnet(subnet) + gateway, _ = subnetutil.FirstIPInSubnet(subnet) } res := &IPAMRange{ @@ -253,10 +326,10 @@ func parseIPAMRange(subnetStr, gatewayStr, ipRangeStr string) (*IPAMRange, error if err != nil { return nil, fmt.Errorf("failed to parse ip-range %q", ipRangeStr) } - rangeStart, _ = firstIPInSubnet(ipRange) - rangeEnd, _ = lastIPInSubnet(ipRange) + rangeStart, _ = subnetutil.FirstIPInSubnet(ipRange) + rangeEnd, _ = subnetutil.LastIPInSubnet(ipRange) if !subnet.Contains(rangeStart) || !subnet.Contains(rangeEnd) { - return nil, fmt.Errorf("no matching subnet %q for ip-range %q", subnetStr, ipRangeStr) + return nil, fmt.Errorf("no matching subnet %q for ip-range %q", subnet, ipRangeStr) } res.RangeStart = rangeStart.String() res.RangeEnd = rangeEnd.String() @@ -266,40 +339,6 @@ func parseIPAMRange(subnetStr, gatewayStr, ipRangeStr string) (*IPAMRange, error return res, nil } -// lastIPInSubnet gets the last IP in a subnet -// https://github.com/containers/podman/blob/v4.0.0-rc1/libpod/network/util/ip.go#L18 -func lastIPInSubnet(addr *net.IPNet) (net.IP, error) { - // re-parse to ensure clean network address - _, cidr, err := net.ParseCIDR(addr.String()) - if err != nil { - return nil, err - } - ones, bits := cidr.Mask.Size() - if ones == bits { - return cidr.IP, err - } - for i := range cidr.IP { - cidr.IP[i] = cidr.IP[i] | ^cidr.Mask[i] - } - return cidr.IP, err -} - -// firstIPInSubnet gets the first IP in a subnet -// https://github.com/containers/podman/blob/v4.0.0-rc1/libpod/network/util/ip.go#L36 -func firstIPInSubnet(addr *net.IPNet) (net.IP, error) { - // re-parse to ensure clean network address - _, cidr, err := net.ParseCIDR(addr.String()) - if err != nil { - return nil, err - } - ones, bits := cidr.Mask.Size() - if ones == bits { - return cidr.IP, err - } - cidr.IP[len(cidr.IP)-1]++ - return cidr.IP, err -} - // convert the struct to a map func structToMap(in interface{}) (map[string]interface{}, error) { out := make(map[string]interface{}) diff --git a/pkg/netutil/netutil_test.go b/pkg/netutil/netutil_test.go index db8d9c58aa9..a24058f34a7 100644 --- a/pkg/netutil/netutil_test.go +++ b/pkg/netutil/netutil_test.go @@ -17,6 +17,7 @@ package netutil import ( + "net" "testing" "gotest.tools/v3/assert" @@ -32,16 +33,6 @@ func TestParseIPAMRange(t *testing.T) { err string } testCases := []testCase{ - { - subnet: "", - expected: nil, - err: "failed to parse subnet", - }, - { - subnet: "10.1.100.1/24", - expected: nil, - err: "unexpected subnet", - }, { subnet: "10.1.100.0/24", expected: &IPAMRange{ @@ -96,7 +87,8 @@ func TestParseIPAMRange(t *testing.T) { }, } for _, tc := range testCases { - got, err := parseIPAMRange(tc.subnet, tc.gateway, tc.iprange) + _, subnet, _ := net.ParseCIDR(tc.subnet) + got, err := parseIPAMRange(subnet, tc.gateway, tc.iprange) if tc.err != "" { assert.ErrorContains(t, err, tc.err) } else { diff --git a/pkg/netutil/netutil_unix.go b/pkg/netutil/netutil_unix.go index b358b85e61c..64e9af3cb39 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -21,7 +21,9 @@ package netutil import ( "bytes" + "encoding/json" "fmt" + "net" "os/exec" "path/filepath" "strings" @@ -30,17 +32,57 @@ import ( "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/strutil" "github.com/containerd/nerdctl/pkg/systemutil" + "github.com/mitchellh/mapstructure" "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" ) const ( DefaultNetworkName = "bridge" - DefaultID = 0 DefaultCIDR = "10.4.0.0/24" DefaultIPAMDriver = "host-local" ) -func (e *CNIEnv) GenerateCNIPlugins(driver string, id int, name string, ipam map[string]interface{}, opts map[string]string) ([]CNIPlugin, error) { +func (n *networkConfig) subnets() []*net.IPNet { + var subnets []*net.IPNet + if len(n.Plugins) > 0 && n.Plugins[0].Network.Type == "bridge" { + var bridge bridgeConfig + if err := json.Unmarshal(n.Plugins[0].Bytes, &bridge); err != nil { + return subnets + } + if bridge.IPAM["type"] != "host-local" { + return subnets + } + var ipam hostLocalIPAMConfig + if err := mapstructure.Decode(bridge.IPAM, &ipam); err != nil { + return subnets + } + for _, irange := range ipam.Ranges { + if len(irange) > 0 { + _, subnet, err := net.ParseCIDR(irange[0].Subnet) + if err != nil { + continue + } + subnets = append(subnets, subnet) + } + } + } + return subnets +} + +func (n *networkConfig) clean() error { + // Remove the bridge network interface on the host. + if len(n.Plugins) > 0 && n.Plugins[0].Network.Type == "bridge" { + var bridge bridgeConfig + if err := json.Unmarshal(n.Plugins[0].Bytes, &bridge); err != nil { + return err + } + return removeBridgeNetworkInterface(bridge.BrName) + } + return nil +} + +func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string) ([]CNIPlugin, error) { var ( plugins []CNIPlugin err error @@ -59,7 +101,12 @@ func (e *CNIEnv) GenerateCNIPlugins(driver string, id int, name string, ipam map return nil, fmt.Errorf("unsupported %q network option %q", driver, opt) } } - bridge := newBridgePlugin(GetBridgeName(id)) + var bridge *bridgeConfig + if name == DefaultNetworkName { + bridge = newBridgePlugin("nerdctl0") + } else { + bridge = newBridgePlugin("br-" + networkID(name)[:12]) + } bridge.MTU = mtu bridge.IPAM = ipam bridge.IsGW = true @@ -109,11 +156,15 @@ func (e *CNIEnv) GenerateCNIPlugins(driver string, id int, name string, ipam map return plugins, nil } -func GenerateIPAM(driver string, subnetStr, gatewayStr, ipRangeStr string, opts map[string]string) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnetStr, gatewayStr, ipRangeStr string, opts map[string]string) (map[string]interface{}, error) { var ipamConfig interface{} switch driver { case "default", "host-local": - ipamRange, err := parseIPAMRange(subnetStr, gatewayStr, ipRangeStr) + subnet, err := e.parseSubnet(subnetStr) + if err != nil { + return nil, err + } + ipamRange, err := parseIPAMRange(subnet, gatewayStr, ipRangeStr) if err != nil { return nil, err } @@ -229,3 +280,13 @@ func guessFirewallPluginVersion(stderr string) (*semver.Version, error) { } return nil, fmt.Errorf("stderr %q does not have any line that starts with %q", stderr, prefix) } + +func removeBridgeNetworkInterface(netIf string) error { + link, err := netlink.LinkByName(netIf) + if err == nil { + if err := netlink.LinkDel(link); err != nil { + return fmt.Errorf("failed to remove network interface %s: %v", netIf, err) + } + } + return nil +} diff --git a/pkg/netutil/netutil_windows.go b/pkg/netutil/netutil_windows.go index 8a3319cedb5..d65e9e61f4c 100644 --- a/pkg/netutil/netutil_windows.go +++ b/pkg/netutil/netutil_windows.go @@ -17,16 +17,43 @@ package netutil import ( + "encoding/json" "fmt" + "net" + + "github.com/mitchellh/mapstructure" ) const ( DefaultNetworkName = "nat" - DefaultID = 0 DefaultCIDR = "10.4.0.0/24" ) -func (e *CNIEnv) GenerateCNIPlugins(driver string, id int, name string, ipam map[string]interface{}, opts map[string]string) ([]CNIPlugin, error) { +func (n *networkConfig) subnets() []*net.IPNet { + var subnets []*net.IPNet + if n.Plugins[0].Network.Type == "nat" { + var nat natConfig + if err := json.Unmarshal(n.Plugins[0].Bytes, &nat); err != nil { + return subnets + } + var ipam windowsIpamConfig + if err := mapstructure.Decode(nat.IPAM, &ipam); err != nil { + return subnets + } + _, subnet, err := net.ParseCIDR(ipam.Subnet) + if err != nil { + return subnets + } + subnets = append(subnets, subnet) + } + return subnets +} + +func (n *networkConfig) clean() error { + return nil +} + +func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string) ([]CNIPlugin, error) { var plugins []CNIPlugin switch driver { case "nat": @@ -39,8 +66,12 @@ func (e *CNIEnv) GenerateCNIPlugins(driver string, id int, name string, ipam map return plugins, nil } -func GenerateIPAM(driver string, subnetStr, gatewayStr, ipRangeStr string, opts map[string]string) (map[string]interface{}, error) { - ipamRange, err := parseIPAMRange(subnetStr, gatewayStr, ipRangeStr) +func (e *CNIEnv) generateIPAM(driver string, subnetStr, gatewayStr, ipRangeStr string, opts map[string]string) (map[string]interface{}, error) { + subnet, err := e.parseSubnet(subnetStr) + if err != nil { + return nil, err + } + ipamRange, err := parseIPAMRange(subnet, gatewayStr, ipRangeStr) if err != nil { return nil, err } @@ -62,3 +93,7 @@ func GenerateIPAM(driver string, subnetStr, gatewayStr, ipRangeStr string, opts } return ipam, nil } + +func removeBridgeNetworkInterface(name string) error { + return nil +} diff --git a/pkg/netutil/subnet/subnet.go b/pkg/netutil/subnet/subnet.go new file mode 100644 index 00000000000..188ab88e9f3 --- /dev/null +++ b/pkg/netutil/subnet/subnet.go @@ -0,0 +1,130 @@ +/* + 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 subnet + +import ( + "fmt" + "net" +) + +func GetLiveNetworkSubnets() ([]*net.IPNet, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return nil, err + } + nets := make([]*net.IPNet, 0, len(addrs)) + for _, address := range addrs { + _, n, err := net.ParseCIDR(address.String()) + if err != nil { + return nil, err + } + nets = append(nets, n) + } + return nets, nil +} + +// GetFreeSubnet try to find a free subnet in the given network +func GetFreeSubnet(n *net.IPNet, usedNetworks []*net.IPNet) (*net.IPNet, error) { + for { + if !IntersectsWithNetworks(n, usedNetworks) { + return n, nil + } + next, err := nextSubnet(n) + if err != nil { + break + } + n = next + } + return nil, fmt.Errorf("could not find free subnet") +} + +func nextSubnet(subnet *net.IPNet) (*net.IPNet, error) { + newSubnet := &net.IPNet{ + IP: subnet.IP, + Mask: subnet.Mask, + } + ones, bits := newSubnet.Mask.Size() + if ones == 0 { + return nil, fmt.Errorf("%s has only one subnet", subnet.String()) + } + zeroes := uint(bits - ones) + shift := zeroes % 8 + idx := (ones - 1) / 8 + if err := incByte(newSubnet, idx, shift); err != nil { + return nil, err + } + return newSubnet, nil +} + +func incByte(subnet *net.IPNet, idx int, shift uint) error { + if idx < 0 { + return fmt.Errorf("no more subnets left") + } + + var val byte = 1 << shift + // if overflow we have to inc the previous byte + if uint(subnet.IP[idx])+uint(val) > 255 { + if err := incByte(subnet, idx-1, 0); err != nil { + return err + } + } + subnet.IP[idx] += val + return nil +} + +func IntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) bool { + for _, nw := range networklist { + if n.Contains(nw.IP) || nw.Contains(n.IP) { + return true + } + } + return false +} + +// lastIPInSubnet gets the last IP in a subnet +// https://github.com/containers/podman/blob/v4.0.0-rc1/libpod/network/util/ip.go#L18 +func LastIPInSubnet(addr *net.IPNet) (net.IP, error) { + // re-parse to ensure clean network address + _, cidr, err := net.ParseCIDR(addr.String()) + if err != nil { + return nil, err + } + ones, bits := cidr.Mask.Size() + if ones == bits { + return cidr.IP, err + } + for i := range cidr.IP { + cidr.IP[i] = cidr.IP[i] | ^cidr.Mask[i] + } + return cidr.IP, err +} + +// firstIPInSubnet gets the first IP in a subnet +// https://github.com/containers/podman/blob/v4.0.0-rc1/libpod/network/util/ip.go#L36 +func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { + // re-parse to ensure clean network address + _, cidr, err := net.ParseCIDR(addr.String()) + if err != nil { + return nil, err + } + ones, bits := cidr.Mask.Size() + if ones == bits { + return cidr.IP, err + } + cidr.IP[len(cidr.IP)-1]++ + return cidr.IP, err +} diff --git a/cmd/nerdctl/network_rm_linux.go b/pkg/netutil/subnet/subnet_test.go similarity index 53% rename from cmd/nerdctl/network_rm_linux.go rename to pkg/netutil/subnet/subnet_test.go index ad7b4fac069..f61e719588e 100644 --- a/cmd/nerdctl/network_rm_linux.go +++ b/pkg/netutil/subnet/subnet_test.go @@ -14,18 +14,37 @@ limitations under the License. */ -package main +package subnet import ( - "github.com/sirupsen/logrus" - "github.com/vishvananda/netlink" + "net" + "testing" + + "gotest.tools/v3/assert" ) -func removeBridgeNetworkInterface(netIf string) { - link, err := netlink.LinkByName(netIf) - if err == nil { - if err = netlink.LinkDel(link); err != nil { - logrus.Warnf("Failed to remove network interface %s: %v", netIf, err) - } +func TestNextSubnet(t *testing.T) { + testCases := []struct { + subnet string + expect string + }{ + { + subnet: "10.4.1.0/24", + expect: "10.4.2.0/24", + }, + { + subnet: "10.4.255.0/24", + expect: "10.5.0.0/24", + }, + { + subnet: "10.4.255.0/16", + expect: "10.5.0.0/16", + }, + } + for _, tc := range testCases { + _, net, _ := net.ParseCIDR(tc.subnet) + nextSubnet, err := nextSubnet(net) + assert.NilError(t, err) + assert.Equal(t, nextSubnet.String(), tc.expect) } }