github.com/containerd/nerdctl@v1.7.7/pkg/netutil/netutil_unix.go (about)

     1  //go:build freebsd || linux
     2  
     3  /*
     4     Copyright The containerd Authors.
     5  
     6     Licensed under the Apache License, Version 2.0 (the "License");
     7     you may not use this file except in compliance with the License.
     8     You may obtain a copy of the License at
     9  
    10         http://www.apache.org/licenses/LICENSE-2.0
    11  
    12     Unless required by applicable law or agreed to in writing, software
    13     distributed under the License is distributed on an "AS IS" BASIS,
    14     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15     See the License for the specific language governing permissions and
    16     limitations under the License.
    17  */
    18  
    19  package netutil
    20  
    21  import (
    22  	"bytes"
    23  	"encoding/json"
    24  	"fmt"
    25  	"net"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"strconv"
    29  	"strings"
    30  
    31  	"github.com/Masterminds/semver/v3"
    32  	"github.com/containerd/log"
    33  	"github.com/containerd/nerdctl/pkg/defaults"
    34  	"github.com/containerd/nerdctl/pkg/strutil"
    35  	"github.com/containerd/nerdctl/pkg/systemutil"
    36  	"github.com/mitchellh/mapstructure"
    37  	"github.com/vishvananda/netlink"
    38  )
    39  
    40  const (
    41  	DefaultNetworkName = "bridge"
    42  	DefaultCIDR        = "10.4.0.0/24"
    43  	DefaultIPAMDriver  = "host-local"
    44  
    45  	// When creating non-default network without passing in `--subnet` option,
    46  	// nerdctl assigns subnet address for the creation starting from `StartingCIDR`
    47  	// This prevents subnet address overlapping with `DefaultCIDR` used by the default network
    48  	StartingCIDR = "10.4.1.0/24"
    49  )
    50  
    51  func (n *NetworkConfig) subnets() []*net.IPNet {
    52  	var subnets []*net.IPNet
    53  	if len(n.Plugins) > 0 && n.Plugins[0].Network.Type == "bridge" {
    54  		var bridge bridgeConfig
    55  		if err := json.Unmarshal(n.Plugins[0].Bytes, &bridge); err != nil {
    56  			return subnets
    57  		}
    58  		if bridge.IPAM["type"] != "host-local" {
    59  			return subnets
    60  		}
    61  		var ipam hostLocalIPAMConfig
    62  		if err := mapstructure.Decode(bridge.IPAM, &ipam); err != nil {
    63  			return subnets
    64  		}
    65  		for _, irange := range ipam.Ranges {
    66  			if len(irange) > 0 {
    67  				_, subnet, err := net.ParseCIDR(irange[0].Subnet)
    68  				if err != nil {
    69  					continue
    70  				}
    71  				subnets = append(subnets, subnet)
    72  			}
    73  		}
    74  	}
    75  	return subnets
    76  }
    77  
    78  func (n *NetworkConfig) clean() error {
    79  	// Remove the bridge network interface on the host.
    80  	if len(n.Plugins) > 0 && n.Plugins[0].Network.Type == "bridge" {
    81  		var bridge bridgeConfig
    82  		if err := json.Unmarshal(n.Plugins[0].Bytes, &bridge); err != nil {
    83  			return err
    84  		}
    85  		return removeBridgeNetworkInterface(bridge.BrName)
    86  	}
    87  	return nil
    88  }
    89  
    90  func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string, ipv6 bool) ([]CNIPlugin, error) {
    91  	var (
    92  		plugins []CNIPlugin
    93  		err     error
    94  	)
    95  	switch driver {
    96  	case "bridge":
    97  		mtu := 0
    98  		iPMasq := true
    99  		for opt, v := range opts {
   100  			switch opt {
   101  			case "mtu", "com.docker.network.driver.mtu":
   102  				mtu, err = ParseMTU(v)
   103  				if err != nil {
   104  					return nil, err
   105  				}
   106  			case "ip-masq", "com.docker.network.bridge.enable_ip_masquerade":
   107  				iPMasq, err = strconv.ParseBool(v)
   108  				if err != nil {
   109  					return nil, err
   110  				}
   111  			default:
   112  				return nil, fmt.Errorf("unsupported %q network option %q", driver, opt)
   113  			}
   114  		}
   115  		var bridge *bridgeConfig
   116  		if name == DefaultNetworkName {
   117  			bridge = newBridgePlugin("nerdctl0")
   118  		} else {
   119  			bridge = newBridgePlugin("br-" + networkID(name)[:12])
   120  		}
   121  		bridge.MTU = mtu
   122  		bridge.IPAM = ipam
   123  		bridge.IsGW = true
   124  		bridge.IPMasq = iPMasq
   125  		bridge.HairpinMode = true
   126  		if ipv6 {
   127  			bridge.Capabilities["ips"] = true
   128  		}
   129  		plugins = []CNIPlugin{bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin()}
   130  		plugins = fixUpIsolation(e, name, plugins)
   131  	case "macvlan", "ipvlan":
   132  		mtu := 0
   133  		mode := ""
   134  		master := ""
   135  		for opt, v := range opts {
   136  			switch opt {
   137  			case "mtu", "com.docker.network.driver.mtu":
   138  				mtu, err = ParseMTU(v)
   139  				if err != nil {
   140  					return nil, err
   141  				}
   142  			case "mode", "macvlan_mode", "ipvlan_mode":
   143  				if driver == "macvlan" && opt != "ipvlan_mode" {
   144  					if !strutil.InStringSlice([]string{"bridge"}, v) {
   145  						return nil, fmt.Errorf("unknown macvlan mode %q", v)
   146  					}
   147  				} else if driver == "ipvlan" && opt != "macvlan_mode" {
   148  					if !strutil.InStringSlice([]string{"l2", "l3"}, v) {
   149  						return nil, fmt.Errorf("unknown ipvlan mode %q", v)
   150  					}
   151  				} else {
   152  					return nil, fmt.Errorf("unsupported %q network option %q", driver, opt)
   153  				}
   154  				mode = v
   155  			case "parent":
   156  				master = v
   157  			default:
   158  				return nil, fmt.Errorf("unsupported %q network option %q", driver, opt)
   159  			}
   160  		}
   161  		vlan := newVLANPlugin(driver)
   162  		vlan.MTU = mtu
   163  		vlan.Master = master
   164  		vlan.Mode = mode
   165  		vlan.IPAM = ipam
   166  		if ipv6 {
   167  			vlan.Capabilities["ips"] = true
   168  		}
   169  		plugins = []CNIPlugin{vlan}
   170  	default:
   171  		return nil, fmt.Errorf("unsupported cni driver %q", driver)
   172  	}
   173  	return plugins, nil
   174  }
   175  
   176  func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRangeStr string, opts map[string]string, ipv6 bool) (map[string]interface{}, error) {
   177  	var ipamConfig interface{}
   178  	switch driver {
   179  	case "default", "host-local":
   180  		ipamConf := newHostLocalIPAMConfig()
   181  		ipamConf.Routes = []IPAMRoute{
   182  			{Dst: "0.0.0.0/0"},
   183  		}
   184  		ranges, findIPv4, err := e.parseIPAMRanges(subnets, gatewayStr, ipRangeStr, ipv6)
   185  		if err != nil {
   186  			return nil, err
   187  		}
   188  		ipamConf.Ranges = append(ipamConf.Ranges, ranges...)
   189  		if !findIPv4 {
   190  			ranges, _, _ = e.parseIPAMRanges([]string{""}, gatewayStr, ipRangeStr, ipv6)
   191  			ipamConf.Ranges = append(ipamConf.Ranges, ranges...)
   192  		}
   193  		ipamConfig = ipamConf
   194  	case "dhcp":
   195  		ipamConf := newDHCPIPAMConfig()
   196  		ipamConf.DaemonSocketPath = filepath.Join(defaults.CNIRuntimeDir(), "dhcp.sock")
   197  		// TODO: support IPAM options for dhcp
   198  		if err := systemutil.IsSocketAccessible(ipamConf.DaemonSocketPath); err != nil {
   199  			log.L.Warnf("cannot access dhcp socket %q (hint: try running with `dhcp daemon --socketpath=%s &` in CNI_PATH to launch the dhcp daemon)", ipamConf.DaemonSocketPath, ipamConf.DaemonSocketPath)
   200  		}
   201  		ipamConfig = ipamConf
   202  	default:
   203  		return nil, fmt.Errorf("unsupported ipam driver %q", driver)
   204  	}
   205  
   206  	ipam, err := structToMap(ipamConfig)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	return ipam, nil
   211  }
   212  
   213  func (e *CNIEnv) parseIPAMRanges(subnets []string, gateway, ipRange string, ipv6 bool) ([][]IPAMRange, bool, error) {
   214  	findIPv4 := false
   215  	ranges := make([][]IPAMRange, 0, len(subnets))
   216  	for i := range subnets {
   217  		subnet, err := e.parseSubnet(subnets[i])
   218  		if err != nil {
   219  			return nil, findIPv4, err
   220  		}
   221  		// if ipv6 flag is not set, subnets of ipv6 should be excluded
   222  		if !ipv6 && subnet.IP.To4() == nil {
   223  			continue
   224  		}
   225  		if !findIPv4 && subnet.IP.To4() != nil {
   226  			findIPv4 = true
   227  		}
   228  		ipamRange, err := parseIPAMRange(subnet, gateway, ipRange)
   229  		if err != nil {
   230  			return nil, findIPv4, err
   231  		}
   232  		ranges = append(ranges, []IPAMRange{*ipamRange})
   233  	}
   234  	return ranges, findIPv4, nil
   235  }
   236  
   237  func fixUpIsolation(e *CNIEnv, name string, plugins []CNIPlugin) []CNIPlugin {
   238  	isolationPath := filepath.Join(e.Path, "isolation")
   239  	if _, err := exec.LookPath(isolationPath); err == nil {
   240  		// the warning is suppressed for DefaultNetworkName (because multi-bridge networking is not involved)
   241  		if name != DefaultNetworkName {
   242  			log.L.Warnf(`network %q: Using the deprecated CNI "isolation" plugin instead of CNI "firewall" plugin (>= 1.1.0) ingressPolicy.
   243  To dismiss this warning, uninstall %q and install CNI "firewall" plugin (>= 1.1.0) from https://github.com/containernetworking/plugins`,
   244  				name, isolationPath)
   245  		}
   246  		plugins = append(plugins, newIsolationPlugin())
   247  		for _, f := range plugins {
   248  			if x, ok := f.(*firewallConfig); ok {
   249  				if name != DefaultNetworkName {
   250  					log.L.Warnf("network %q: Unsetting firewall ingressPolicy %q (because using the deprecated \"isolation\" plugin)", name, x.IngressPolicy)
   251  				}
   252  				x.IngressPolicy = ""
   253  			}
   254  		}
   255  	} else if name != DefaultNetworkName {
   256  		firewallPath := filepath.Join(e.Path, "firewall")
   257  		ok, err := firewallPluginGEQ110(firewallPath)
   258  		if err != nil {
   259  			log.L.WithError(err).Warnf("Failed to detect whether %q is newer than v1.1.0", firewallPath)
   260  		}
   261  		if !ok {
   262  			log.L.Warnf("To isolate bridge networks, CNI plugin \"firewall\" (>= 1.1.0) needs to be installed in CNI_PATH (%q), see https://github.com/containernetworking/plugins",
   263  				e.Path)
   264  		}
   265  	}
   266  
   267  	return plugins
   268  }
   269  
   270  func firewallPluginGEQ110(firewallPath string) (bool, error) {
   271  	// TODO: guess true by default in 2023
   272  	guessed := false
   273  
   274  	// Parse the stderr (NOT stdout) of `firewall`, such as "CNI firewall plugin v1.1.0\n", or "CNI firewall plugin version unknown\n"
   275  	//
   276  	// We do NOT set `CNI_COMMAND=VERSION` here, because the CNI "VERSION" command reports the version of the CNI spec,
   277  	// not the version of the firewall plugin implementation.
   278  	//
   279  	// ```
   280  	// $ /opt/cni/bin/firewall
   281  	// CNI firewall plugin v1.1.0
   282  	// $ CNI_COMMAND=VERSION /opt/cni/bin/firewall
   283  	// {"cniVersion":"1.0.0","supportedVersions":["0.4.0","1.0.0"]}
   284  	// ```
   285  	//
   286  	cmd := exec.Command(firewallPath)
   287  	var stdout, stderr bytes.Buffer
   288  	cmd.Stdout = &stdout
   289  	cmd.Stderr = &stderr
   290  	if err := cmd.Run(); err != nil {
   291  		err = fmt.Errorf("failed to run %v: %w (stdout=%q, stderr=%q)", cmd.Args, err, stdout.String(), stderr.String())
   292  		return guessed, err
   293  	}
   294  
   295  	ver, err := guessFirewallPluginVersion(stderr.String()) // NOT stdout
   296  	if err != nil {
   297  		return guessed, fmt.Errorf("failed to guess the version of %q: %w", firewallPath, err)
   298  	}
   299  	ver110 := semver.MustParse("v1.1.0")
   300  	return ver.GreaterThan(ver110) || ver.Equal(ver110), nil
   301  }
   302  
   303  // guesssFirewallPluginVersion guess the version of the CNI firewall plugin (not the version of the implemented CNI spec).
   304  //
   305  // stderr is like "CNI firewall plugin v1.1.0\n", or "CNI firewall plugin version unknown\n"
   306  func guessFirewallPluginVersion(stderr string) (*semver.Version, error) {
   307  	const prefix = "CNI firewall plugin "
   308  	lines := strings.Split(stderr, "\n")
   309  	for i, l := range lines {
   310  		trimmed := strings.TrimPrefix(l, prefix)
   311  		if trimmed == l { // l does not have the expected prefix
   312  			continue
   313  		}
   314  		// trimmed is like "v1.1.1", "v1.1.0", ..., "v0.8.0", or "version unknown"
   315  		ver, err := semver.NewVersion(trimmed)
   316  		if err != nil {
   317  			return nil, fmt.Errorf("failed to parse %q (line %d of stderr=%q) as a semver: %w", trimmed, i+1, stderr, err)
   318  		}
   319  		return ver, nil
   320  	}
   321  	return nil, fmt.Errorf("stderr %q does not have any line that starts with %q", stderr, prefix)
   322  }
   323  
   324  func removeBridgeNetworkInterface(netIf string) error {
   325  	link, err := netlink.LinkByName(netIf)
   326  	if err == nil {
   327  		if err := netlink.LinkDel(link); err != nil {
   328  			return fmt.Errorf("failed to remove network interface %s: %v", netIf, err)
   329  		}
   330  	}
   331  	return nil
   332  }