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