gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/runsc/cmd/do.go (about)

     1  // Copyright 2018 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cmd
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"math/rand"
    24  	"net"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"strconv"
    29  	"strings"
    30  
    31  	"github.com/google/subcommands"
    32  	specs "github.com/opencontainers/runtime-spec/specs-go"
    33  	"golang.org/x/sys/unix"
    34  	"gvisor.dev/gvisor/pkg/log"
    35  	"gvisor.dev/gvisor/runsc/cmd/util"
    36  	"gvisor.dev/gvisor/runsc/config"
    37  	"gvisor.dev/gvisor/runsc/console"
    38  	"gvisor.dev/gvisor/runsc/container"
    39  	"gvisor.dev/gvisor/runsc/flag"
    40  	"gvisor.dev/gvisor/runsc/specutils"
    41  )
    42  
    43  var errNoDefaultInterface = errors.New("no default interface found")
    44  
    45  // Do implements subcommands.Command for the "do" command. It sets up a simple
    46  // sandbox and executes the command inside it. See Usage() for more details.
    47  type Do struct {
    48  	root    string
    49  	cwd     string
    50  	ip      string
    51  	quiet   bool
    52  	overlay bool
    53  	uidMap  idMapSlice
    54  	gidMap  idMapSlice
    55  }
    56  
    57  // Name implements subcommands.Command.Name.
    58  func (*Do) Name() string {
    59  	return "do"
    60  }
    61  
    62  // Synopsis implements subcommands.Command.Synopsis.
    63  func (*Do) Synopsis() string {
    64  	return "Simplistic way to execute a command inside the sandbox. It's to be used for testing only."
    65  }
    66  
    67  // Usage implements subcommands.Command.Usage.
    68  func (*Do) Usage() string {
    69  	return `do [flags] <cmd> - runs a command.
    70  
    71  This command starts a sandbox with host filesystem mounted inside as readonly,
    72  with a writable tmpfs overlay on top of it. The given command is executed inside
    73  the sandbox. It's to be used to quickly test applications without having to
    74  install or run docker. It doesn't give nearly as many options and it's to be
    75  used for testing only.
    76  `
    77  }
    78  
    79  type idMapSlice []specs.LinuxIDMapping
    80  
    81  // String implements flag.Value.String.
    82  func (is *idMapSlice) String() string {
    83  	idMappings := make([]string, 0, len(*is))
    84  	for _, m := range *is {
    85  		idMappings = append(idMappings, fmt.Sprintf("%d %d %d", m.ContainerID, m.HostID, m.Size))
    86  	}
    87  	return strings.Join(idMappings, ",")
    88  }
    89  
    90  // Get implements flag.Value.Get.
    91  func (is *idMapSlice) Get() any {
    92  	return is
    93  }
    94  
    95  // Set implements flag.Value.Set. Set(String()) should be idempotent.
    96  func (is *idMapSlice) Set(s string) error {
    97  	for _, idMap := range strings.Split(s, ",") {
    98  		fs := strings.Fields(idMap)
    99  		if len(fs) != 3 {
   100  			return fmt.Errorf("invalid mapping: %s", idMap)
   101  		}
   102  		var cid, hid, size int
   103  		var err error
   104  		if cid, err = strconv.Atoi(fs[0]); err != nil {
   105  			return fmt.Errorf("invalid mapping: %s", idMap)
   106  		}
   107  		if hid, err = strconv.Atoi(fs[1]); err != nil {
   108  			return fmt.Errorf("invalid mapping: %s", idMap)
   109  		}
   110  		if size, err = strconv.Atoi(fs[2]); err != nil {
   111  			return fmt.Errorf("invalid mapping: %s", idMap)
   112  		}
   113  		m := specs.LinuxIDMapping{
   114  			ContainerID: uint32(cid),
   115  			HostID:      uint32(hid),
   116  			Size:        uint32(size),
   117  		}
   118  		*is = append(*is, m)
   119  	}
   120  	return nil
   121  }
   122  
   123  // SetFlags implements subcommands.Command.SetFlags.
   124  func (c *Do) SetFlags(f *flag.FlagSet) {
   125  	f.StringVar(&c.root, "root", "/", `path to the root directory, defaults to "/"`)
   126  	f.StringVar(&c.cwd, "cwd", ".", "path to the current directory, defaults to the current directory")
   127  	f.StringVar(&c.ip, "ip", "192.168.10.2", "IPv4 address for the sandbox")
   128  	f.BoolVar(&c.quiet, "quiet", false, "suppress runsc messages to stdout. Application output is still sent to stdout and stderr")
   129  	f.BoolVar(&c.overlay, "force-overlay", true, "use an overlay. WARNING: disabling gives the command write access to the host")
   130  	f.Var(&c.uidMap, "uid-map", "Add a user id mapping [ContainerID, HostID, Size]")
   131  	f.Var(&c.gidMap, "gid-map", "Add a group id mapping [ContainerID, HostID, Size]")
   132  }
   133  
   134  // Execute implements subcommands.Command.Execute.
   135  func (c *Do) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus {
   136  	if len(f.Args()) == 0 {
   137  		f.Usage()
   138  		return subcommands.ExitUsageError
   139  	}
   140  
   141  	conf := args[0].(*config.Config)
   142  	waitStatus := args[1].(*unix.WaitStatus)
   143  
   144  	if conf.Rootless {
   145  		if err := specutils.MaybeRunAsRoot(); err != nil {
   146  			return util.Errorf("Error executing inside namespace: %v", err)
   147  		}
   148  		// Execution will continue here if no more capabilities are needed...
   149  	}
   150  
   151  	hostname, err := os.Hostname()
   152  	if err != nil {
   153  		return util.Errorf("Error to retrieve hostname: %v", err)
   154  	}
   155  
   156  	// If c.overlay is set, then enable overlay.
   157  	conf.Overlay = false // conf.Overlay is deprecated.
   158  	if c.overlay {
   159  		conf.Overlay2.Set("all:memory")
   160  	} else {
   161  		conf.Overlay2.Set("none")
   162  	}
   163  	absRoot, err := resolvePath(c.root)
   164  	if err != nil {
   165  		return util.Errorf("Error resolving root: %v", err)
   166  	}
   167  	absCwd, err := resolvePath(c.cwd)
   168  	if err != nil {
   169  		return util.Errorf("Error resolving current directory: %v", err)
   170  	}
   171  
   172  	spec := &specs.Spec{
   173  		Root: &specs.Root{
   174  			Path: absRoot,
   175  		},
   176  		Process: &specs.Process{
   177  			Cwd:          absCwd,
   178  			Args:         f.Args(),
   179  			Env:          os.Environ(),
   180  			Capabilities: specutils.AllCapabilities(),
   181  			Terminal:     console.IsPty(os.Stdin.Fd()),
   182  		},
   183  		Hostname: hostname,
   184  	}
   185  
   186  	cid := fmt.Sprintf("runsc-%06d", rand.Int31n(1000000))
   187  
   188  	if c.uidMap != nil || c.gidMap != nil {
   189  		addNamespace(spec, specs.LinuxNamespace{Type: specs.UserNamespace})
   190  		spec.Linux.UIDMappings = c.uidMap
   191  		spec.Linux.GIDMappings = c.gidMap
   192  	}
   193  
   194  	if conf.Network == config.NetworkNone {
   195  		addNamespace(spec, specs.LinuxNamespace{Type: specs.NetworkNamespace})
   196  	} else if conf.Rootless {
   197  		if conf.Network == config.NetworkSandbox {
   198  			c.notifyUser("*** Warning: sandbox network isn't supported with --rootless, switching to host ***")
   199  			conf.Network = config.NetworkHost
   200  		}
   201  
   202  	} else {
   203  		switch clean, err := c.setupNet(cid, spec); err {
   204  		case errNoDefaultInterface:
   205  			log.Warningf("Network interface not found, using internal network")
   206  			addNamespace(spec, specs.LinuxNamespace{Type: specs.NetworkNamespace})
   207  			conf.Network = config.NetworkHost
   208  
   209  		case nil:
   210  			// Setup successfull.
   211  			defer clean()
   212  
   213  		default:
   214  			return util.Errorf("Error setting up network: %v", err)
   215  		}
   216  	}
   217  
   218  	return startContainerAndWait(spec, conf, cid, waitStatus)
   219  }
   220  
   221  func addNamespace(spec *specs.Spec, ns specs.LinuxNamespace) {
   222  	if spec.Linux == nil {
   223  		spec.Linux = &specs.Linux{}
   224  	}
   225  	spec.Linux.Namespaces = append(spec.Linux.Namespaces, ns)
   226  }
   227  
   228  func (c *Do) notifyUser(format string, v ...any) {
   229  	if !c.quiet {
   230  		fmt.Printf(format+"\n", v...)
   231  	}
   232  	log.Warningf(format, v...)
   233  }
   234  
   235  func resolvePath(path string) (string, error) {
   236  	var err error
   237  	path, err = filepath.Abs(path)
   238  	if err != nil {
   239  		return "", fmt.Errorf("resolving %q: %v", path, err)
   240  	}
   241  	path = filepath.Clean(path)
   242  	if err := unix.Access(path, 0); err != nil {
   243  		return "", fmt.Errorf("unable to access %q: %v", path, err)
   244  	}
   245  	return path, nil
   246  }
   247  
   248  // setupNet setups up the sandbox network, including the creation of a network
   249  // namespace, and iptable rules to redirect the traffic. Returns a cleanup
   250  // function to tear down the network. Returns errNoDefaultInterface when there
   251  // is no network interface available to setup the network.
   252  func (c *Do) setupNet(cid string, spec *specs.Spec) (func(), error) {
   253  	dev, err := defaultDevice()
   254  	if err != nil {
   255  		return nil, errNoDefaultInterface
   256  	}
   257  	mtu, err := deviceMTU(dev)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  	peerIP, err := calculatePeerIP(c.ip)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	veth, peer := deviceNames(cid)
   266  
   267  	cmds := []string{
   268  		fmt.Sprintf("ip link add %s mtu %v type veth peer name %s", veth, mtu, peer),
   269  
   270  		// Setup device outside the namespace.
   271  		fmt.Sprintf("ip addr add %s/24 dev %s", peerIP, peer),
   272  		fmt.Sprintf("ip link set %s up", peer),
   273  
   274  		// Setup device inside the namespace.
   275  		fmt.Sprintf("ip netns add %s", cid),
   276  		fmt.Sprintf("ip link set %s netns %s", veth, cid),
   277  		fmt.Sprintf("ip netns exec %s ip addr add %s/24 dev %s", cid, c.ip, veth),
   278  		fmt.Sprintf("ip netns exec %s ip link set %s up", cid, veth),
   279  		fmt.Sprintf("ip netns exec %s ip link set lo up", cid),
   280  		fmt.Sprintf("ip netns exec %s ip route add default via %s", cid, peerIP),
   281  
   282  		// Enable network access.
   283  		"sysctl -w net.ipv4.ip_forward=1",
   284  		fmt.Sprintf("iptables -t nat -A POSTROUTING -s %s -o %s -m comment --comment runsc-%s -j MASQUERADE", c.ip, dev, peer),
   285  		fmt.Sprintf("iptables -A FORWARD -i %s -o %s -j ACCEPT", dev, peer),
   286  		fmt.Sprintf("iptables -A FORWARD -o %s -i %s -j ACCEPT", dev, peer),
   287  	}
   288  
   289  	for _, cmd := range cmds {
   290  		log.Debugf("Run %q", cmd)
   291  		args := strings.Split(cmd, " ")
   292  		cmd := exec.Command(args[0], args[1:]...)
   293  		if err := cmd.Run(); err != nil {
   294  			c.cleanupNet(cid, dev, "", "", "")
   295  			return nil, fmt.Errorf("failed to run %q: %v", cmd, err)
   296  		}
   297  	}
   298  
   299  	resolvPath, err := makeFile("/etc/resolv.conf", "nameserver 8.8.8.8\n", spec)
   300  	if err != nil {
   301  		c.cleanupNet(cid, dev, "", "", "")
   302  		return nil, err
   303  	}
   304  	hostnamePath, err := makeFile("/etc/hostname", cid+"\n", spec)
   305  	if err != nil {
   306  		c.cleanupNet(cid, dev, resolvPath, "", "")
   307  		return nil, err
   308  	}
   309  	hosts := fmt.Sprintf("127.0.0.1\tlocalhost\n%s\t%s\n", c.ip, cid)
   310  	hostsPath, err := makeFile("/etc/hosts", hosts, spec)
   311  	if err != nil {
   312  		c.cleanupNet(cid, dev, resolvPath, hostnamePath, "")
   313  		return nil, err
   314  	}
   315  
   316  	netns := specs.LinuxNamespace{
   317  		Type: specs.NetworkNamespace,
   318  		Path: filepath.Join("/var/run/netns", cid),
   319  	}
   320  	addNamespace(spec, netns)
   321  
   322  	return func() { c.cleanupNet(cid, dev, resolvPath, hostnamePath, hostsPath) }, nil
   323  }
   324  
   325  // cleanupNet tries to cleanup the network setup in setupNet.
   326  //
   327  // It may be called when setupNet is only partially complete, in which case it
   328  // will cleanup as much as possible, logging warnings for the rest.
   329  //
   330  // Unfortunately none of this can be automatically cleaned up on process exit,
   331  // we must do so explicitly.
   332  func (c *Do) cleanupNet(cid, dev, resolvPath, hostnamePath, hostsPath string) {
   333  	_, peer := deviceNames(cid)
   334  
   335  	cmds := []string{
   336  		fmt.Sprintf("ip link delete %s", peer),
   337  		fmt.Sprintf("ip netns delete %s", cid),
   338  		fmt.Sprintf("iptables -t nat -D POSTROUTING -s %s -o %s -m comment --comment runsc-%s -j MASQUERADE", c.ip, dev, peer),
   339  		fmt.Sprintf("iptables -D FORWARD -i %s -o %s -j ACCEPT", dev, peer),
   340  		fmt.Sprintf("iptables -D FORWARD -o %s -i %s -j ACCEPT", dev, peer),
   341  	}
   342  
   343  	for _, cmd := range cmds {
   344  		log.Debugf("Run %q", cmd)
   345  		args := strings.Split(cmd, " ")
   346  		c := exec.Command(args[0], args[1:]...)
   347  		if err := c.Run(); err != nil {
   348  			log.Warningf("Failed to run %q: %v", cmd, err)
   349  		}
   350  	}
   351  
   352  	tryRemove(resolvPath)
   353  	tryRemove(hostnamePath)
   354  	tryRemove(hostsPath)
   355  }
   356  
   357  func deviceNames(cid string) (string, string) {
   358  	// Device name is limited to 15 letters.
   359  	return "ve-" + cid, "vp-" + cid
   360  
   361  }
   362  
   363  func defaultDevice() (string, error) {
   364  	out, err := exec.Command("ip", "route", "list", "default").CombinedOutput()
   365  	if err != nil {
   366  		return "", err
   367  	}
   368  	parts := strings.Split(string(out), " ")
   369  	if len(parts) < 5 {
   370  		return "", fmt.Errorf("malformed %q output: %q", "ip route list default", string(out))
   371  	}
   372  	return parts[4], nil
   373  }
   374  
   375  func deviceMTU(dev string) (int, error) {
   376  	intf, err := net.InterfaceByName(dev)
   377  	if err != nil {
   378  		return 0, err
   379  	}
   380  	return intf.MTU, nil
   381  }
   382  
   383  func makeFile(dest, content string, spec *specs.Spec) (string, error) {
   384  	tmpFile, err := ioutil.TempFile("", filepath.Base(dest))
   385  	if err != nil {
   386  		return "", err
   387  	}
   388  	if _, err := tmpFile.WriteString(content); err != nil {
   389  		if err := os.Remove(tmpFile.Name()); err != nil {
   390  			log.Warningf("Failed to remove %q: %v", tmpFile, err)
   391  		}
   392  		return "", err
   393  	}
   394  	spec.Mounts = append(spec.Mounts, specs.Mount{
   395  		Source:      tmpFile.Name(),
   396  		Destination: dest,
   397  		Type:        "bind",
   398  		Options:     []string{"ro"},
   399  	})
   400  	return tmpFile.Name(), nil
   401  }
   402  
   403  func tryRemove(path string) {
   404  	if path == "" {
   405  		return
   406  	}
   407  
   408  	if err := os.Remove(path); err != nil {
   409  		log.Warningf("Failed to remove %q: %v", path, err)
   410  	}
   411  }
   412  
   413  func calculatePeerIP(ip string) (string, error) {
   414  	parts := strings.Split(ip, ".")
   415  	if len(parts) != 4 {
   416  		return "", fmt.Errorf("invalid IP format %q", ip)
   417  	}
   418  	n, err := strconv.Atoi(parts[3])
   419  	if err != nil {
   420  		return "", fmt.Errorf("invalid IP format %q: %v", ip, err)
   421  	}
   422  	n++
   423  	if n > 255 {
   424  		n = 1
   425  	}
   426  	return fmt.Sprintf("%s.%s.%s.%d", parts[0], parts[1], parts[2], n), nil
   427  }
   428  
   429  func startContainerAndWait(spec *specs.Spec, conf *config.Config, cid string, waitStatus *unix.WaitStatus) subcommands.ExitStatus {
   430  	specutils.LogSpecDebug(spec, conf.OCISeccomp)
   431  
   432  	out, err := json.Marshal(spec)
   433  	if err != nil {
   434  		return util.Errorf("Error to marshal spec: %v", err)
   435  	}
   436  	tmpDir, err := ioutil.TempDir("", "runsc-do")
   437  	if err != nil {
   438  		return util.Errorf("Error to create tmp dir: %v", err)
   439  	}
   440  	defer os.RemoveAll(tmpDir)
   441  
   442  	log.Infof("Changing configuration RootDir to %q", tmpDir)
   443  	conf.RootDir = tmpDir
   444  
   445  	cfgPath := filepath.Join(tmpDir, "config.json")
   446  	if err := ioutil.WriteFile(cfgPath, out, 0755); err != nil {
   447  		return util.Errorf("Error write spec: %v", err)
   448  	}
   449  
   450  	containerArgs := container.Args{
   451  		ID:        cid,
   452  		Spec:      spec,
   453  		BundleDir: tmpDir,
   454  		Attached:  true,
   455  	}
   456  
   457  	ct, err := container.New(conf, containerArgs)
   458  	if err != nil {
   459  		return util.Errorf("creating container: %v", err)
   460  	}
   461  	defer ct.Destroy()
   462  
   463  	if err := ct.Start(conf); err != nil {
   464  		return util.Errorf("starting container: %v", err)
   465  	}
   466  
   467  	// Forward signals to init in the container. Thus if we get SIGINT from
   468  	// ^C, the container gracefully exit, and we can clean up.
   469  	//
   470  	// N.B. There is a still a window before this where a signal may kill
   471  	// this process, skipping cleanup.
   472  	stopForwarding := ct.ForwardSignals(0 /* pid */, spec.Process.Terminal /* fgProcess */)
   473  	defer stopForwarding()
   474  
   475  	ws, err := ct.Wait()
   476  	if err != nil {
   477  		return util.Errorf("waiting for container: %v", err)
   478  	}
   479  
   480  	*waitStatus = ws
   481  	return subcommands.ExitSuccess
   482  }