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 }