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