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 }