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 }