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 }