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