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  }