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