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 }