github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/netutil/netutil.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package netutil
    18  
    19  import (
    20  	"context"
    21  	"crypto/sha256"
    22  	"encoding/hex"
    23  	"encoding/json"
    24  	"fmt"
    25  	"net"
    26  	"os"
    27  	"os/exec"
    28  	"path/filepath"
    29  	"sort"
    30  	"strconv"
    31  	"strings"
    32  
    33  	"github.com/containerd/containerd"
    34  	"github.com/containerd/containerd/errdefs"
    35  	"github.com/containerd/containerd/namespaces"
    36  	"github.com/containerd/log"
    37  	"github.com/containerd/nerdctl/v2/pkg/api/types"
    38  	"github.com/containerd/nerdctl/v2/pkg/labels"
    39  	"github.com/containerd/nerdctl/v2/pkg/lockutil"
    40  	"github.com/containerd/nerdctl/v2/pkg/netutil/nettype"
    41  	subnetutil "github.com/containerd/nerdctl/v2/pkg/netutil/subnet"
    42  	"github.com/containerd/nerdctl/v2/pkg/strutil"
    43  	"github.com/containernetworking/cni/libcni"
    44  )
    45  
    46  type CNIEnv struct {
    47  	Path        string
    48  	NetconfPath string
    49  }
    50  
    51  type CNIEnvOpt func(e *CNIEnv) error
    52  
    53  func UsedNetworks(ctx context.Context, client *containerd.Client) (map[string][]string, error) {
    54  	nsService := client.NamespaceService()
    55  	nsList, err := nsService.List(ctx)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	used := make(map[string][]string)
    60  	for _, ns := range nsList {
    61  		nsCtx := namespaces.WithNamespace(ctx, ns)
    62  		containers, err := client.Containers(nsCtx)
    63  		if err != nil {
    64  			return nil, err
    65  		}
    66  		nsUsedN, err := namespaceUsedNetworks(nsCtx, containers)
    67  		if err != nil {
    68  			return nil, err
    69  		}
    70  
    71  		// merge
    72  		for k, v := range nsUsedN {
    73  			if value, ok := used[k]; ok {
    74  				used[k] = append(value, v...)
    75  			} else {
    76  				used[k] = v
    77  			}
    78  		}
    79  	}
    80  	return used, nil
    81  }
    82  
    83  func namespaceUsedNetworks(ctx context.Context, containers []containerd.Container) (map[string][]string, error) {
    84  	used := make(map[string][]string)
    85  	for _, c := range containers {
    86  		// Only tasks under the ctx namespace can be obtained here
    87  		task, err := c.Task(ctx, nil)
    88  		if err != nil {
    89  			if errdefs.IsNotFound(err) {
    90  				continue
    91  			}
    92  			return nil, err
    93  		}
    94  		status, err := task.Status(ctx)
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  		switch status.Status {
    99  		case containerd.Paused, containerd.Running:
   100  		default:
   101  			continue
   102  		}
   103  		l, err := c.Labels(ctx)
   104  		if err != nil {
   105  			return nil, err
   106  		}
   107  		networkJSON, ok := l[labels.Networks]
   108  		if !ok {
   109  			continue
   110  		}
   111  		var networks []string
   112  		if err := json.Unmarshal([]byte(networkJSON), &networks); err != nil {
   113  			return nil, err
   114  		}
   115  		netType, err := nettype.Detect(networks)
   116  		if err != nil {
   117  			return nil, err
   118  		}
   119  		if netType != nettype.CNI {
   120  			continue
   121  		}
   122  		for _, n := range networks {
   123  			used[n] = append(used[n], c.ID())
   124  		}
   125  	}
   126  	return used, nil
   127  }
   128  
   129  func WithDefaultNetwork() CNIEnvOpt {
   130  	return func(e *CNIEnv) error {
   131  		return e.ensureDefaultNetworkConfig()
   132  	}
   133  }
   134  
   135  func NewCNIEnv(cniPath, cniConfPath string, opts ...CNIEnvOpt) (*CNIEnv, error) {
   136  	e := CNIEnv{
   137  		Path:        cniPath,
   138  		NetconfPath: cniConfPath,
   139  	}
   140  	if err := os.MkdirAll(e.NetconfPath, 0755); err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	for _, o := range opts {
   145  		if err := o(&e); err != nil {
   146  			return nil, err
   147  		}
   148  	}
   149  
   150  	return &e, nil
   151  }
   152  
   153  func (e *CNIEnv) NetworkList() ([]*NetworkConfig, error) {
   154  	return e.networkConfigList()
   155  }
   156  
   157  func (e *CNIEnv) NetworkMap() (map[string]*NetworkConfig, error) { //nolint:revive
   158  	networks, err := e.networkConfigList()
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	m := make(map[string]*NetworkConfig, len(networks))
   164  	for _, n := range networks {
   165  		if original, exists := m[n.Name]; exists {
   166  			log.L.Warnf("duplicate network name %q, %#v will get superseded by %#v", n.Name, original, n)
   167  		}
   168  		m[n.Name] = n
   169  		if n.NerdctlID != nil {
   170  			id := *n.NerdctlID
   171  			m[id] = n
   172  			if len(id) > 12 {
   173  				id = id[:12]
   174  				m[id] = n
   175  			}
   176  		}
   177  	}
   178  	return m, nil
   179  }
   180  
   181  func (e *CNIEnv) FilterNetworks(filterf func(*NetworkConfig) bool) ([]*NetworkConfig, error) {
   182  	networkConfigs, err := e.networkConfigList()
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	result := []*NetworkConfig{}
   187  	for _, networkConfig := range networkConfigs {
   188  		if filterf(networkConfig) {
   189  			result = append(result, networkConfig)
   190  		}
   191  	}
   192  	return result, nil
   193  }
   194  
   195  func (e *CNIEnv) getConfigPathForNetworkName(netName string) string {
   196  	return filepath.Join(e.NetconfPath, "nerdctl-"+netName+".conflist")
   197  }
   198  
   199  func (e *CNIEnv) usedSubnets() ([]*net.IPNet, error) {
   200  	usedSubnets, err := subnetutil.GetLiveNetworkSubnets()
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	networkConfigs, err := e.networkConfigList()
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	for _, net := range networkConfigs {
   209  		usedSubnets = append(usedSubnets, net.subnets()...)
   210  	}
   211  	return usedSubnets, nil
   212  }
   213  
   214  type NetworkConfig struct {
   215  	*libcni.NetworkConfigList
   216  	NerdctlID     *string
   217  	NerdctlLabels *map[string]string
   218  	File          string
   219  }
   220  
   221  type cniNetworkConfig struct {
   222  	CNIVersion string            `json:"cniVersion"`
   223  	Name       string            `json:"name"`
   224  	ID         string            `json:"nerdctlID"`
   225  	Labels     map[string]string `json:"nerdctlLabels"`
   226  	Plugins    []CNIPlugin       `json:"plugins"`
   227  }
   228  
   229  func (e *CNIEnv) CreateNetwork(opts types.NetworkCreateOptions) (*NetworkConfig, error) { //nolint:revive
   230  	var net *NetworkConfig
   231  	netMap, err := e.NetworkMap()
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	if _, ok := netMap[opts.Name]; ok {
   237  		return nil, errdefs.ErrAlreadyExists
   238  	}
   239  
   240  	fn := func() error {
   241  		ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnets, opts.Gateway, opts.IPRange, opts.IPAMOptions, opts.IPv6)
   242  		if err != nil {
   243  			return err
   244  		}
   245  		plugins, err := e.generateCNIPlugins(opts.Driver, opts.Name, ipam, opts.Options, opts.IPv6)
   246  		if err != nil {
   247  			return err
   248  		}
   249  		net, err = e.generateNetworkConfig(opts.Name, opts.Labels, plugins)
   250  		if err != nil {
   251  			return err
   252  		}
   253  		return e.writeNetworkConfig(net)
   254  	}
   255  	err = lockutil.WithDirLock(e.NetconfPath, fn)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	return net, nil
   260  }
   261  
   262  func (e *CNIEnv) RemoveNetwork(net *NetworkConfig) error {
   263  	fn := func() error {
   264  		if err := os.RemoveAll(net.File); err != nil {
   265  			return err
   266  		}
   267  		return net.clean()
   268  	}
   269  	return lockutil.WithDirLock(e.NetconfPath, fn)
   270  }
   271  
   272  // GetDefaultNetworkConfig checks whether the default network exists
   273  // by first searching for if any network bears the `labels.NerdctlDefaultNetwork`
   274  // label, or falls back to checking whether any network bears the
   275  // `DefaultNetworkName` name.
   276  func (e *CNIEnv) GetDefaultNetworkConfig() (*NetworkConfig, error) {
   277  	// Search for networks bearing the `labels.NerdctlDefaultNetwork` label.
   278  	defaultLabelFilterF := func(nc *NetworkConfig) bool {
   279  		if nc.NerdctlLabels == nil {
   280  			return false
   281  		} else if _, ok := (*nc.NerdctlLabels)[labels.NerdctlDefaultNetwork]; ok {
   282  			return true
   283  		}
   284  		return false
   285  	}
   286  	labelMatches, err := e.FilterNetworks(defaultLabelFilterF)
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  	if len(labelMatches) >= 1 {
   291  		if len(labelMatches) > 1 {
   292  			log.L.Warnf("returning the first network bearing the %q label out of the multiple found: %#v", labels.NerdctlDefaultNetwork, labelMatches)
   293  		}
   294  		return labelMatches[0], nil
   295  	}
   296  
   297  	// Search for networks bearing the DefaultNetworkName.
   298  	defaultNameFilterF := func(nc *NetworkConfig) bool {
   299  		return nc.Name == DefaultNetworkName
   300  	}
   301  	nameMatches, err := e.FilterNetworks(defaultNameFilterF)
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  	if len(nameMatches) >= 1 {
   306  		if len(nameMatches) > 1 {
   307  			log.L.Warnf("returning the first network bearing the %q default network name out of the multiple found: %#v", DefaultNetworkName, nameMatches)
   308  		}
   309  
   310  		// Warn the user if the default network was not created by nerdctl.
   311  		match := nameMatches[0]
   312  		_, statErr := os.Stat(e.getConfigPathForNetworkName(DefaultNetworkName))
   313  		if match.NerdctlID == nil || statErr != nil {
   314  			log.L.Warnf("default network named %q does not have an internal nerdctl ID or nerdctl-managed config file, it was most likely NOT created by nerdctl", DefaultNetworkName)
   315  		}
   316  
   317  		return nameMatches[0], nil
   318  	}
   319  
   320  	return nil, nil
   321  }
   322  
   323  func (e *CNIEnv) ensureDefaultNetworkConfig() error {
   324  	defaultNet, err := e.GetDefaultNetworkConfig()
   325  	if err != nil {
   326  		return fmt.Errorf("failed to check for default network: %s", err)
   327  	}
   328  	if defaultNet == nil {
   329  		if err := e.createDefaultNetworkConfig(); err != nil {
   330  			return fmt.Errorf("failed to create default network: %s", err)
   331  		}
   332  	}
   333  	return nil
   334  }
   335  
   336  func (e *CNIEnv) createDefaultNetworkConfig() error {
   337  	filename := e.getConfigPathForNetworkName(DefaultNetworkName)
   338  	if _, err := os.Stat(filename); err == nil {
   339  		return fmt.Errorf("already found existing network config at %q, cannot create new network named %q", filename, DefaultNetworkName)
   340  	}
   341  	opts := types.NetworkCreateOptions{
   342  		Name:       DefaultNetworkName,
   343  		Driver:     DefaultNetworkName,
   344  		Subnets:    []string{DefaultCIDR},
   345  		IPAMDriver: "default",
   346  		Labels:     []string{fmt.Sprintf("%s=true", labels.NerdctlDefaultNetwork)},
   347  	}
   348  	_, err := e.CreateNetwork(opts)
   349  	if err != nil && !errdefs.IsAlreadyExists(err) {
   350  		return err
   351  	}
   352  	return nil
   353  }
   354  
   355  // generateNetworkConfig creates NetworkConfig.
   356  // generateNetworkConfig does not fill "File" field.
   357  func (e *CNIEnv) generateNetworkConfig(name string, labels []string, plugins []CNIPlugin) (*NetworkConfig, error) {
   358  	if name == "" || len(plugins) == 0 {
   359  		return nil, errdefs.ErrInvalidArgument
   360  	}
   361  	for _, f := range plugins {
   362  		p := filepath.Join(e.Path, f.GetPluginType())
   363  		if _, err := exec.LookPath(p); err != nil {
   364  			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)
   365  		}
   366  	}
   367  	id := networkID(name)
   368  	labelsMap := strutil.ConvertKVStringsToMap(labels)
   369  
   370  	conf := &cniNetworkConfig{
   371  		CNIVersion: "1.0.0",
   372  		Name:       name,
   373  		ID:         id,
   374  		Labels:     labelsMap,
   375  		Plugins:    plugins,
   376  	}
   377  
   378  	confJSON, err := json.MarshalIndent(conf, "", "  ")
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  
   383  	l, err := libcni.ConfListFromBytes(confJSON)
   384  	if err != nil {
   385  		return nil, err
   386  	}
   387  	return &NetworkConfig{
   388  		NetworkConfigList: l,
   389  		NerdctlID:         &id,
   390  		NerdctlLabels:     &labelsMap,
   391  		File:              "",
   392  	}, nil
   393  }
   394  
   395  // writeNetworkConfig writes NetworkConfig file to cni config path.
   396  func (e *CNIEnv) writeNetworkConfig(net *NetworkConfig) error {
   397  	filename := e.getConfigPathForNetworkName(net.Name)
   398  	if _, err := os.Stat(filename); err == nil {
   399  		return errdefs.ErrAlreadyExists
   400  	}
   401  	return os.WriteFile(filename, net.Bytes, 0644)
   402  }
   403  
   404  // networkConfigList loads config from dir if dir exists.
   405  func (e *CNIEnv) networkConfigList() ([]*NetworkConfig, error) {
   406  	l := []*NetworkConfig{}
   407  	fileNames, err := libcni.ConfFiles(e.NetconfPath, []string{".conf", ".conflist", ".json"})
   408  	if err != nil {
   409  		return nil, err
   410  	}
   411  	sort.Strings(fileNames)
   412  	for _, fileName := range fileNames {
   413  		var lcl *libcni.NetworkConfigList
   414  		if strings.HasSuffix(fileName, ".conflist") {
   415  			lcl, err = libcni.ConfListFromFile(fileName)
   416  			if err != nil {
   417  				return nil, err
   418  			}
   419  		} else {
   420  			lc, err := libcni.ConfFromFile(fileName)
   421  			if err != nil {
   422  				return nil, err
   423  			}
   424  			lcl, err = libcni.ConfListFromConf(lc)
   425  			if err != nil {
   426  				return nil, err
   427  			}
   428  		}
   429  		id, labels := nerdctlIDLabels(lcl.Bytes)
   430  		l = append(l, &NetworkConfig{
   431  			NetworkConfigList: lcl,
   432  			NerdctlID:         id,
   433  			NerdctlLabels:     labels,
   434  			File:              fileName,
   435  		})
   436  	}
   437  	return l, nil
   438  }
   439  
   440  func nerdctlIDLabels(b []byte) (*string, *map[string]string) {
   441  	type idLabels struct {
   442  		ID     *string            `json:"nerdctlID,omitempty"`
   443  		Labels *map[string]string `json:"nerdctlLabels,omitempty"`
   444  	}
   445  	var idl idLabels
   446  	if err := json.Unmarshal(b, &idl); err != nil {
   447  		return nil, nil
   448  	}
   449  	return idl.ID, idl.Labels
   450  }
   451  
   452  func networkID(name string) string {
   453  	hash := sha256.Sum256([]byte(name))
   454  	return hex.EncodeToString(hash[:])
   455  }
   456  
   457  func (e *CNIEnv) parseSubnet(subnetStr string) (*net.IPNet, error) {
   458  	usedSubnets, err := e.usedSubnets()
   459  	if err != nil {
   460  		return nil, err
   461  	}
   462  	if subnetStr == "" {
   463  		_, defaultSubnet, _ := net.ParseCIDR(StartingCIDR)
   464  		subnet, err := subnetutil.GetFreeSubnet(defaultSubnet, usedSubnets)
   465  		if err != nil {
   466  			return nil, err
   467  		}
   468  		return subnet, nil
   469  	}
   470  
   471  	subnetIP, subnet, err := net.ParseCIDR(subnetStr)
   472  	if err != nil {
   473  		return nil, fmt.Errorf("failed to parse subnet %q", subnetStr)
   474  	}
   475  	if !subnet.IP.Equal(subnetIP) {
   476  		return nil, fmt.Errorf("unexpected subnet %q, maybe you meant %q?", subnetStr, subnet.String())
   477  	}
   478  	if subnetutil.IntersectsWithNetworks(subnet, usedSubnets) {
   479  		return nil, fmt.Errorf("subnet %s overlaps with other one on this address space", subnetStr)
   480  	}
   481  	return subnet, nil
   482  }
   483  
   484  func parseIPAMRange(subnet *net.IPNet, gatewayStr, ipRangeStr string) (*IPAMRange, error) {
   485  	var gateway, rangeStart, rangeEnd net.IP
   486  	if gatewayStr != "" {
   487  		gatewayIP := net.ParseIP(gatewayStr)
   488  		if gatewayIP == nil {
   489  			return nil, fmt.Errorf("failed to parse gateway %q", gatewayStr)
   490  		}
   491  		if !subnet.Contains(gatewayIP) {
   492  			return nil, fmt.Errorf("no matching subnet %q for gateway %q", subnet, gatewayStr)
   493  		}
   494  		gateway = gatewayIP
   495  	} else {
   496  		gateway, _ = subnetutil.FirstIPInSubnet(subnet)
   497  	}
   498  
   499  	res := &IPAMRange{
   500  		Subnet:  subnet.String(),
   501  		Gateway: gateway.String(),
   502  	}
   503  
   504  	if ipRangeStr != "" {
   505  		_, ipRange, err := net.ParseCIDR(ipRangeStr)
   506  		if err != nil {
   507  			return nil, fmt.Errorf("failed to parse ip-range %q", ipRangeStr)
   508  		}
   509  		rangeStart, _ = subnetutil.FirstIPInSubnet(ipRange)
   510  		rangeEnd, _ = subnetutil.LastIPInSubnet(ipRange)
   511  		if !subnet.Contains(rangeStart) || !subnet.Contains(rangeEnd) {
   512  			return nil, fmt.Errorf("no matching subnet %q for ip-range %q", subnet, ipRangeStr)
   513  		}
   514  		res.RangeStart = rangeStart.String()
   515  		res.RangeEnd = rangeEnd.String()
   516  		res.IPRange = ipRangeStr
   517  	}
   518  
   519  	return res, nil
   520  }
   521  
   522  // convert the struct to a map
   523  func structToMap(in interface{}) (map[string]interface{}, error) {
   524  	out := make(map[string]interface{})
   525  	data, err := json.Marshal(in)
   526  	if err != nil {
   527  		return nil, err
   528  	}
   529  	if err := json.Unmarshal(data, &out); err != nil {
   530  		return nil, err
   531  	}
   532  	return out, nil
   533  }
   534  
   535  // ParseMTU parses the mtu option
   536  func ParseMTU(mtu string) (int, error) {
   537  	if mtu == "" {
   538  		return 0, nil // default
   539  	}
   540  	m, err := strconv.Atoi(mtu)
   541  	if err != nil {
   542  		return 0, err
   543  	}
   544  	if m < 0 {
   545  		return 0, fmt.Errorf("mtu %d is less than zero", m)
   546  	}
   547  	return m, nil
   548  }