github.com/containerd/nerdctl@v1.7.7/pkg/ocihook/ocihook.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 ocihook 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "net" 26 "os" 27 "path/filepath" 28 "strings" 29 30 gocni "github.com/containerd/go-cni" 31 "github.com/containerd/log" 32 "github.com/containerd/nerdctl/pkg/bypass4netnsutil" 33 "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore" 34 "github.com/containerd/nerdctl/pkg/labels" 35 "github.com/containerd/nerdctl/pkg/namestore" 36 "github.com/containerd/nerdctl/pkg/netutil" 37 "github.com/containerd/nerdctl/pkg/netutil/nettype" 38 "github.com/containerd/nerdctl/pkg/rootlessutil" 39 types100 "github.com/containernetworking/cni/pkg/types/100" 40 "github.com/opencontainers/runtime-spec/specs-go" 41 42 b4nndclient "github.com/rootless-containers/bypass4netns/pkg/api/daemon/client" 43 rlkclient "github.com/rootless-containers/rootlesskit/pkg/api/client" 44 ) 45 46 const ( 47 // NetworkNamespace is the network namespace path to be passed to the CNI plugins. 48 // When this annotation is set from the runtime spec.State payload, it takes 49 // precedence over the PID based resolution (/proc/<pid>/ns/net) where pid is 50 // spec.State.Pid. 51 // This is mostly used for VM based runtime, where the spec.State PID does not 52 // necessarily lives in the created container networking namespace. 53 // 54 // On Windows, this label will contain the UUID of a namespace managed by 55 // the Host Compute Network Service (HCN) API. 56 NetworkNamespace = labels.Prefix + "network-namespace" 57 ) 58 59 func Run(stdin io.Reader, stderr io.Writer, event, dataStore, cniPath, cniNetconfPath string) error { 60 if stdin == nil || event == "" || dataStore == "" || cniPath == "" || cniNetconfPath == "" { 61 return errors.New("got insufficient args") 62 } 63 64 var state specs.State 65 if err := json.NewDecoder(stdin).Decode(&state); err != nil { 66 return err 67 } 68 69 containerStateDir := state.Annotations[labels.StateDir] 70 if containerStateDir == "" { 71 return errors.New("state dir must be set") 72 } 73 if err := os.MkdirAll(containerStateDir, 0700); err != nil { 74 return fmt.Errorf("failed to create %q: %w", containerStateDir, err) 75 } 76 logFilePath := filepath.Join(containerStateDir, "oci-hook."+event+".log") 77 logFile, err := os.Create(logFilePath) 78 if err != nil { 79 return err 80 } 81 defer logFile.Close() 82 log.L.Logger.SetOutput(io.MultiWriter(stderr, logFile)) 83 84 opts, err := newHandlerOpts(&state, dataStore, cniPath, cniNetconfPath) 85 if err != nil { 86 return err 87 } 88 89 switch event { 90 case "createRuntime": 91 return onCreateRuntime(opts) 92 case "postStop": 93 return onPostStop(opts) 94 default: 95 return fmt.Errorf("unexpected event %q", event) 96 } 97 } 98 99 func newHandlerOpts(state *specs.State, dataStore, cniPath, cniNetconfPath string) (*handlerOpts, error) { 100 o := &handlerOpts{ 101 state: state, 102 dataStore: dataStore, 103 } 104 105 extraHosts, err := getExtraHosts(state) 106 if err != nil { 107 return nil, err 108 } 109 o.extraHosts = extraHosts 110 111 hs, err := loadSpec(o.state.Bundle) 112 if err != nil { 113 return nil, err 114 } 115 o.rootfs = hs.Root.Path 116 if !filepath.IsAbs(o.rootfs) { 117 o.rootfs = filepath.Join(o.state.Bundle, o.rootfs) 118 } 119 120 namespace := o.state.Annotations[labels.Namespace] 121 if namespace == "" { 122 return nil, errors.New("namespace must be set") 123 } 124 if o.state.ID == "" { 125 return nil, errors.New("state.ID must be set") 126 } 127 o.fullID = namespace + "-" + o.state.ID 128 129 networksJSON := o.state.Annotations[labels.Networks] 130 var networks []string 131 if err := json.Unmarshal([]byte(networksJSON), &networks); err != nil { 132 return nil, err 133 } 134 135 netType, err := nettype.Detect(networks) 136 if err != nil { 137 return nil, err 138 } 139 140 switch netType { 141 case nettype.Host, nettype.None, nettype.Container: 142 // NOP 143 case nettype.CNI: 144 e, err := netutil.NewCNIEnv(cniPath, cniNetconfPath, netutil.WithDefaultNetwork()) 145 if err != nil { 146 return nil, err 147 } 148 cniOpts := []gocni.Opt{ 149 gocni.WithPluginDir([]string{cniPath}), 150 } 151 netMap, err := e.NetworkMap() 152 if err != nil { 153 return nil, err 154 } 155 for _, netstr := range networks { 156 net, ok := netMap[netstr] 157 if !ok { 158 return nil, fmt.Errorf("no such network: %q", netstr) 159 } 160 cniOpts = append(cniOpts, gocni.WithConfListBytes(net.Bytes)) 161 o.cniNames = append(o.cniNames, netstr) 162 } 163 o.cni, err = gocni.New(cniOpts...) 164 if err != nil { 165 return nil, err 166 } 167 if o.cni == nil { 168 log.L.Warnf("no CNI network could be loaded from the provided network names: %v", networks) 169 } 170 default: 171 return nil, fmt.Errorf("unexpected network type %v", netType) 172 } 173 174 if pidFile := o.state.Annotations[labels.PIDFile]; pidFile != "" { 175 if err := writePidFile(pidFile, state.Pid); err != nil { 176 return nil, err 177 } 178 } 179 180 if portsJSON := o.state.Annotations[labels.Ports]; portsJSON != "" { 181 if err := json.Unmarshal([]byte(portsJSON), &o.ports); err != nil { 182 return nil, err 183 } 184 } 185 186 if ipAddress, ok := o.state.Annotations[labels.IPAddress]; ok { 187 o.containerIP = ipAddress 188 } 189 190 if macAddress, ok := o.state.Annotations[labels.MACAddress]; ok { 191 o.containerMAC = macAddress 192 } 193 194 if ip6Address, ok := o.state.Annotations[labels.IP6Address]; ok { 195 o.containerIP6 = ip6Address 196 } 197 198 if rootlessutil.IsRootlessChild() { 199 o.rootlessKitClient, err = rootlessutil.NewRootlessKitClient() 200 if err != nil { 201 return nil, err 202 } 203 b4nnEnabled, err := bypass4netnsutil.IsBypass4netnsEnabled(o.state.Annotations) 204 if err != nil { 205 return nil, err 206 } 207 if b4nnEnabled { 208 socketPath, err := bypass4netnsutil.GetBypass4NetnsdDefaultSocketPath() 209 if err != nil { 210 return nil, err 211 } 212 o.bypassClient, err = b4nndclient.New(socketPath) 213 if err != nil { 214 return nil, fmt.Errorf("bypass4netnsd not running? (Hint: run `containerd-rootless-setuptool.sh install-bypass4netnsd`): %w", err) 215 } 216 } 217 } 218 return o, nil 219 } 220 221 type handlerOpts struct { 222 state *specs.State 223 dataStore string 224 rootfs string 225 ports []gocni.PortMapping 226 cni gocni.CNI 227 cniNames []string 228 fullID string 229 rootlessKitClient rlkclient.Client 230 bypassClient b4nndclient.Client 231 extraHosts map[string]string // host:ip 232 containerIP string 233 containerMAC string 234 containerIP6 string 235 } 236 237 // hookSpec is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/containerd/command/oci-hook.go#L59-L64 238 type hookSpec struct { 239 Root struct { 240 Path string `json:"path"` 241 } `json:"root"` 242 } 243 244 // loadSpec is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/containerd/command/oci-hook.go#L65-L76 245 func loadSpec(bundle string) (*hookSpec, error) { 246 f, err := os.Open(filepath.Join(bundle, "config.json")) 247 if err != nil { 248 return nil, err 249 } 250 defer f.Close() 251 var s hookSpec 252 if err := json.NewDecoder(f).Decode(&s); err != nil { 253 return nil, err 254 } 255 return &s, nil 256 } 257 258 func getExtraHosts(state *specs.State) (map[string]string, error) { 259 extraHostsJSON := state.Annotations[labels.ExtraHosts] 260 var extraHosts []string 261 if err := json.Unmarshal([]byte(extraHostsJSON), &extraHosts); err != nil { 262 return nil, err 263 } 264 265 hosts := make(map[string]string) 266 for _, host := range extraHosts { 267 if v := strings.SplitN(host, ":", 2); len(v) == 2 { 268 hosts[v[0]] = v[1] 269 } 270 } 271 return hosts, nil 272 } 273 274 func getNetNSPath(state *specs.State) (string, error) { 275 // If we have a network-namespace annotation we use it over the passed Pid. 276 netNsPath, netNsFound := state.Annotations[NetworkNamespace] 277 if netNsFound { 278 if _, err := os.Stat(netNsPath); err != nil { 279 return "", err 280 } 281 282 return netNsPath, nil 283 } 284 285 if state.Pid == 0 && !netNsFound { 286 return "", errors.New("both state.Pid and the netNs annotation are unset") 287 } 288 289 // We dont't have a networking namespace annotation, but we have a PID. 290 s := fmt.Sprintf("/proc/%d/ns/net", state.Pid) 291 if _, err := os.Stat(s); err != nil { 292 return "", err 293 } 294 return s, nil 295 } 296 297 func getPortMapOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) { 298 if len(opts.ports) > 0 { 299 if !rootlessutil.IsRootlessChild() { 300 return []gocni.NamespaceOpts{gocni.WithCapabilityPortMap(opts.ports)}, nil 301 } 302 var ( 303 childIP net.IP 304 portDriverDisallowsLoopbackChildIP bool 305 ) 306 info, err := opts.rootlessKitClient.Info(context.TODO()) 307 if err != nil { 308 log.L.WithError(err).Warn("cannot call RootlessKit Info API, make sure you have RootlessKit v0.14.1 or later") 309 } else { 310 childIP = info.NetworkDriver.ChildIP 311 portDriverDisallowsLoopbackChildIP = info.PortDriver.DisallowLoopbackChildIP // true for slirp4netns port driver 312 } 313 // For rootless, we need to modify the hostIP that is not bindable in the child namespace. 314 // https: //github.com/containerd/nerdctl/issues/88 315 // 316 // We must NOT modify opts.ports here, because we use the unmodified opts.ports for 317 // interaction with RootlessKit API. 318 ports := make([]gocni.PortMapping, len(opts.ports)) 319 for i, p := range opts.ports { 320 if hostIP := net.ParseIP(p.HostIP); hostIP != nil && !hostIP.IsUnspecified() { 321 // loopback address is always bindable in the child namespace, but other addresses are unlikely. 322 if !hostIP.IsLoopback() { 323 if !(childIP != nil && childIP.Equal(hostIP)) { 324 if portDriverDisallowsLoopbackChildIP { 325 p.HostIP = childIP.String() 326 } else { 327 p.HostIP = "127.0.0.1" 328 } 329 } 330 } else if portDriverDisallowsLoopbackChildIP { 331 p.HostIP = childIP.String() 332 } 333 } 334 ports[i] = p 335 } 336 return []gocni.NamespaceOpts{gocni.WithCapabilityPortMap(ports)}, nil 337 } 338 return nil, nil 339 } 340 341 func getIPAddressOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) { 342 if opts.containerIP != "" { 343 if rootlessutil.IsRootlessChild() { 344 log.L.Debug("container IP assignment is not fully supported in rootless mode. The IP is not accessible from the host (but still accessible from other containers).") 345 } 346 347 return []gocni.NamespaceOpts{ 348 gocni.WithLabels(map[string]string{ 349 // Special tick for go-cni. Because go-cni marks all labels and args as same 350 // So, we need add a special label to pass the containerIP to the host-local plugin. 351 // FYI: https://github.com/containerd/go-cni/blob/v1.1.3/README.md?plain=1#L57-L64 352 "IgnoreUnknown": "1", 353 }), 354 gocni.WithArgs("IP", opts.containerIP), 355 }, nil 356 } 357 return nil, nil 358 } 359 360 func getMACAddressOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) { 361 if opts.containerMAC != "" { 362 return []gocni.NamespaceOpts{ 363 gocni.WithLabels(map[string]string{ 364 // allow loose CNI argument verification 365 // FYI: https://github.com/containernetworking/cni/issues/560 366 "IgnoreUnknown": "1", 367 }), 368 gocni.WithArgs("MAC", opts.containerMAC), 369 }, nil 370 } 371 return nil, nil 372 } 373 374 func getIP6AddressOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) { 375 if opts.containerIP6 != "" { 376 if rootlessutil.IsRootlessChild() { 377 log.L.Debug("container IP6 assignment is not fully supported in rootless mode. The IP6 is not accessible from the host (but still accessible from other containers).") 378 } 379 return []gocni.NamespaceOpts{ 380 gocni.WithLabels(map[string]string{ 381 // allow loose CNI argument verification 382 // FYI: https://github.com/containernetworking/cni/issues/560 383 "IgnoreUnknown": "1", 384 }), 385 gocni.WithCapability("ips", []string{opts.containerIP6}), 386 }, nil 387 } 388 return nil, nil 389 } 390 391 func onCreateRuntime(opts *handlerOpts) error { 392 loadAppArmor() 393 394 if opts.cni != nil { 395 portMapOpts, err := getPortMapOpts(opts) 396 if err != nil { 397 return err 398 } 399 nsPath, err := getNetNSPath(opts.state) 400 if err != nil { 401 return err 402 } 403 ctx := context.Background() 404 hs, err := hostsstore.NewStore(opts.dataStore) 405 if err != nil { 406 return err 407 } 408 ipAddressOpts, err := getIPAddressOpts(opts) 409 if err != nil { 410 return err 411 } 412 macAddressOpts, err := getMACAddressOpts(opts) 413 if err != nil { 414 return err 415 } 416 ip6AddressOpts, err := getIP6AddressOpts(opts) 417 if err != nil { 418 return err 419 } 420 var namespaceOpts []gocni.NamespaceOpts 421 namespaceOpts = append(namespaceOpts, portMapOpts...) 422 namespaceOpts = append(namespaceOpts, ipAddressOpts...) 423 namespaceOpts = append(namespaceOpts, macAddressOpts...) 424 namespaceOpts = append(namespaceOpts, ip6AddressOpts...) 425 hsMeta := hostsstore.Meta{ 426 Namespace: opts.state.Annotations[labels.Namespace], 427 ID: opts.state.ID, 428 Networks: make(map[string]*types100.Result, len(opts.cniNames)), 429 Hostname: opts.state.Annotations[labels.Hostname], 430 ExtraHosts: opts.extraHosts, 431 Name: opts.state.Annotations[labels.Name], 432 } 433 cniRes, err := opts.cni.Setup(ctx, opts.fullID, nsPath, namespaceOpts...) 434 if err != nil { 435 return fmt.Errorf("failed to call cni.Setup: %w", err) 436 } 437 cniResRaw := cniRes.Raw() 438 for i, cniName := range opts.cniNames { 439 hsMeta.Networks[cniName] = cniResRaw[i] 440 } 441 442 b4nnEnabled, err := bypass4netnsutil.IsBypass4netnsEnabled(opts.state.Annotations) 443 if err != nil { 444 return err 445 } 446 447 if err := hs.Acquire(hsMeta); err != nil { 448 return err 449 } 450 451 if rootlessutil.IsRootlessChild() { 452 if b4nnEnabled { 453 bm, err := bypass4netnsutil.NewBypass4netnsCNIBypassManager(opts.bypassClient, opts.rootlessKitClient) 454 if err != nil { 455 return err 456 } 457 err = bm.StartBypass(ctx, opts.ports, opts.state.ID, opts.state.Annotations[labels.StateDir]) 458 if err != nil { 459 return fmt.Errorf("bypass4netnsd not running? (Hint: run `containerd-rootless-setuptool.sh install-bypass4netnsd`): %w", err) 460 } 461 } else if len(opts.ports) > 0 { 462 if err := exposePortsRootless(ctx, opts.rootlessKitClient, opts.ports); err != nil { 463 return fmt.Errorf("failed to expose ports in rootless mode: %s", err) 464 } 465 } 466 } 467 } 468 return nil 469 } 470 471 func onPostStop(opts *handlerOpts) error { 472 ctx := context.Background() 473 ns := opts.state.Annotations[labels.Namespace] 474 if opts.cni != nil { 475 var err error 476 b4nnEnabled, err := bypass4netnsutil.IsBypass4netnsEnabled(opts.state.Annotations) 477 if err != nil { 478 return err 479 } 480 if rootlessutil.IsRootlessChild() { 481 if b4nnEnabled { 482 bm, err := bypass4netnsutil.NewBypass4netnsCNIBypassManager(opts.bypassClient, opts.rootlessKitClient) 483 if err != nil { 484 return err 485 } 486 err = bm.StopBypass(ctx, opts.state.ID) 487 if err != nil { 488 return err 489 } 490 } else if len(opts.ports) > 0 { 491 if err := unexposePortsRootless(ctx, opts.rootlessKitClient, opts.ports); err != nil { 492 return fmt.Errorf("failed to unexpose ports in rootless mode: %s", err) 493 } 494 } 495 } 496 portMapOpts, err := getPortMapOpts(opts) 497 if err != nil { 498 return err 499 } 500 ipAddressOpts, err := getIPAddressOpts(opts) 501 if err != nil { 502 return err 503 } 504 macAddressOpts, err := getMACAddressOpts(opts) 505 if err != nil { 506 return err 507 } 508 ip6AddressOpts, err := getIP6AddressOpts(opts) 509 if err != nil { 510 return err 511 } 512 var namespaceOpts []gocni.NamespaceOpts 513 namespaceOpts = append(namespaceOpts, portMapOpts...) 514 namespaceOpts = append(namespaceOpts, ipAddressOpts...) 515 namespaceOpts = append(namespaceOpts, macAddressOpts...) 516 namespaceOpts = append(namespaceOpts, ip6AddressOpts...) 517 if err := opts.cni.Remove(ctx, opts.fullID, "", namespaceOpts...); err != nil { 518 log.L.WithError(err).Errorf("failed to call cni.Remove") 519 return err 520 } 521 hs, err := hostsstore.NewStore(opts.dataStore) 522 if err != nil { 523 return err 524 } 525 if err := hs.Release(ns, opts.state.ID); err != nil { 526 return err 527 } 528 } 529 namst, err := namestore.New(opts.dataStore, ns) 530 if err != nil { 531 return err 532 } 533 name := opts.state.Annotations[labels.Name] 534 if err := namst.Release(name, opts.state.ID); err != nil { 535 return fmt.Errorf("failed to release container name %s: %w", name, err) 536 } 537 return nil 538 } 539 540 // writePidFile writes the pid atomically to a file. 541 // From https://github.com/containerd/containerd/blob/v1.7.0-rc.2/cmd/ctr/commands/commands.go#L265-L282 542 func writePidFile(path string, pid int) error { 543 path, err := filepath.Abs(path) 544 if err != nil { 545 return err 546 } 547 tempPath := filepath.Join(filepath.Dir(path), fmt.Sprintf(".%s", filepath.Base(path))) 548 f, err := os.OpenFile(tempPath, os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_SYNC, 0666) 549 if err != nil { 550 return err 551 } 552 _, err = fmt.Fprint(f, pid) 553 f.Close() 554 if err != nil { 555 return err 556 } 557 return os.Rename(tempPath, path) 558 }