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 }