github.com/system-transparency/u-root@v6.0.1-0.20190919065413-ed07a650de4c+incompatible/cmds/exp/cpu/cpu.go (about) 1 // Copyright 2018-2019 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bytes" 9 "context" 10 "flag" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "log" 15 "net" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "strings" 20 "syscall" 21 "unsafe" 22 23 // We use this ssh because it implements port redirection. 24 // It can not, however, unpack password-protected keys yet. 25 "github.com/gliderlabs/ssh" 26 "github.com/kr/pty" // TODO: get rid of krpty 27 "github.com/u-root/u-root/pkg/termios" 28 "github.com/u-root/u-root/pkg/uroot/util" 29 // We use this ssh because it can unpack password-protected private keys. 30 ossh "golang.org/x/crypto/ssh" 31 "golang.org/x/sys/unix" 32 ) 33 34 var ( 35 // For the ssh server part 36 hostKeyFile = flag.String("hk", "" /*"/etc/ssh/ssh_host_rsa_key"*/, "file for host key") 37 pubKeyFile = flag.String("pk", "key.pub", "file for public key") 38 port = flag.String("sp", "2222", "ssh default port") 39 40 debug = flag.Bool("d", false, "enable debug prints") 41 runAsInit = flag.Bool("init", false, "run as init (Debug only; normal test is if we are pid 1") 42 v = func(string, ...interface{}) {} 43 remote = flag.Bool("remote", false, "indicates we are the remote side of the cpu session") 44 network = flag.String("network", "tcp", "network to use") 45 keyFile = flag.String("key", filepath.Join(os.Getenv("HOME"), ".ssh/cpu_rsa"), "key file") 46 srv9p = flag.String("srv", "unpfs", "what server to run") 47 bin = flag.String("bin", "cpu", "path of cpu binary") 48 port9p = flag.String("port9p", "", "port9p # on remote machine for 9p mount") 49 dbg9p = flag.Bool("dbg9p", false, "show 9p io") 50 root = flag.String("root", "/", "9p root") 51 bindover = flag.String("bindover", "/lib:/lib64:/lib32:/usr:/bin:/etc", ": separated list of directories in /tmp/cpu to bind over /") 52 ) 53 54 func verbose(f string, a ...interface{}) { 55 v(f+"\r\n", a...) 56 } 57 58 func dial(n, a string, config *ossh.ClientConfig) (*ossh.Client, error) { 59 client, err := ossh.Dial(n, a, config) 60 if err != nil { 61 return nil, fmt.Errorf("Failed to dial: %v", err) 62 } 63 return client, nil 64 } 65 66 func config(kf string) (*ossh.ClientConfig, error) { 67 cb := ossh.InsecureIgnoreHostKey() 68 //var hostKey ssh.PublicKey 69 // A public key may be used to authenticate against the remote 70 // server by using an unencrypted PEM-encoded private key file. 71 // 72 // If you have an encrypted private key, the crypto/x509 package 73 // can be used to decrypt it. 74 key, err := ioutil.ReadFile(kf) 75 if err != nil { 76 return nil, fmt.Errorf("unable to read private key %v: %v", kf, err) 77 } 78 79 // Create the Signer for this private key. 80 signer, err := ossh.ParsePrivateKey(key) 81 if err != nil { 82 return nil, fmt.Errorf("ParsePrivateKey %v: %v", kf, err) 83 } 84 if *hostKeyFile != "" { 85 hk, err := ioutil.ReadFile(*hostKeyFile) 86 if err != nil { 87 return nil, fmt.Errorf("unable to read host key %v: %v", *hostKeyFile, err) 88 } 89 pk, err := ossh.ParsePublicKey(hk) 90 if err != nil { 91 return nil, fmt.Errorf("host key %v: %v", string(hk), err) 92 } 93 cb = ossh.FixedHostKey(pk) 94 } 95 config := &ossh.ClientConfig{ 96 User: os.Getenv("USER"), 97 Auth: []ossh.AuthMethod{ 98 // Use the PublicKeys method for remote authentication. 99 ossh.PublicKeys(signer), 100 }, 101 HostKeyCallback: cb, 102 } 103 return config, nil 104 } 105 106 func cmd(client *ossh.Client, s string) ([]byte, error) { 107 session, err := client.NewSession() 108 if err != nil { 109 return nil, fmt.Errorf("Failed to create session: %v", err) 110 } 111 defer session.Close() 112 113 var b bytes.Buffer 114 session.Stdout = &b 115 if err := session.Run(s); err != nil { 116 return nil, fmt.Errorf("Failed to run %v: %v", s, err.Error()) 117 } 118 return b.Bytes(), nil 119 } 120 121 func dropPrivs() error { 122 uid := unix.Getuid() 123 v("dropPrives: uid is %v", uid) 124 if uid == 0 { 125 v("dropPrivs: not dropping privs") 126 return nil 127 } 128 gid := unix.Getgid() 129 v("dropPrivs: gid is %v", gid) 130 if err := unix.Setreuid(-1, uid); err != nil { 131 return err 132 } 133 return unix.Setregid(-1, gid) 134 } 135 136 // start up a namespace. We must 137 // mkdir /tmp/cpu on the remote machine 138 // issue the mount command 139 // test via an ls of /tmp/cpu 140 // TODO: unshare first 141 // We enter here as uid 0 and once the mount is done, back down. 142 func runRemote(cmd, port9p string) error { 143 // for some reason echo is not set. 144 t, err := termios.New() 145 if err != nil { 146 log.Printf("can't get a termios; oh well; %v", err) 147 } else { 148 term, err := t.Get() 149 if err != nil { 150 log.Printf("can't get a termios; oh well; %v", err) 151 } else { 152 term.Lflag |= unix.ECHO | unix.ECHONL 153 if err := t.Set(term); err != nil { 154 log.Printf("can't set a termios; oh well; %v", err) 155 } 156 } 157 } 158 159 // It's true we are making this directory while still root. 160 // This ought to be safe as it is a private namespace mount. 161 for _, n := range []string{"/tmp/cpu", "/tmp/local", "/tmp/merge", "/tmp/root"} { 162 if err := os.Mkdir(n, 0666); err != nil && !os.IsExist(err) { 163 log.Println(err) 164 } 165 } 166 167 user := os.Getenv("USER") 168 if user == "" { 169 user = "nouser" 170 } 171 flags := uintptr(unix.MS_NODEV | unix.MS_NOSUID) 172 opts := fmt.Sprintf("version=9p2000.L,trans=tcp,port=%v,uname=%v", port9p, user) 173 if err := unix.Mount("127.0.0.1", "/tmp/cpu", "9p", flags, opts); err != nil { 174 return fmt.Errorf("9p mount %v", err) 175 } 176 177 // Further, bind / onto /tmp/local so a non-hacked-on version may be visible. 178 if err := unix.Mount("/", "/tmp/local", "", syscall.MS_BIND, ""); err != nil { 179 log.Printf("Warning: binding / over /tmp/cpu did not work: %v, continuing anyway", err) 180 } 181 182 var overlaid bool 183 if util.FindFileSystem("overlay") == nil { 184 if err := unix.Mount("overlay", "/tmp/root", "overlay", unix.MS_MGC_VAL, "lowerdir=/tmp/cpu,upperdir=/tmp/local,workdir=/tmp/merge"); err == nil { 185 //overlaid = true 186 } else { 187 log.Printf("Overlayfs mount failed: %v. Proceeding with selective mounts from /tmp/cpu into /", err) 188 } 189 } 190 if !overlaid { 191 // We could not get an overlayfs mount. 192 // There are lots of cases where binaries REQUIRE that ld.so be in the right place. 193 // In some cases if you set LD_LIBRARY_PATH it is ignored. 194 // This is disappointing to say the least. We just bind a few things into / 195 // bind *may* hide local resources but for now it's the least worst option. 196 dirs := strings.Split(*bindover, ":") 197 for _, n := range dirs { 198 t := filepath.Join("/tmp/cpu", n) 199 if err := unix.Mount(t, n, "", syscall.MS_BIND, ""); err != nil { 200 log.Printf("Warning: mounting %v on %v failed: %v", t, n, err) 201 } else { 202 log.Printf("Mounted %v on %v", t, n) 203 } 204 205 } 206 } 207 // We don't want to run as the wrong uid. 208 if err := dropPrivs(); err != nil { 209 return err 210 } 211 // The unmount happens for free since we unshared. 212 v("runRemote: command is %q", cmd) 213 c := exec.Command("/bin/sh", "-c", cmd) 214 c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr 215 return c.Run() 216 } 217 218 // srv on 5641. 219 // TODO: make it more private, and also, have server only take 220 // one connection or use stdin/stdout 221 func srv(ctx context.Context) (net.Conn, *exec.Cmd, error) { 222 c := exec.CommandContext(ctx, "unpfs", "tcp!localhost!5641", *root) 223 o, err := c.StdoutPipe() 224 if err != nil { 225 return nil, nil, err 226 } 227 c.Stderr = c.Stdout 228 if err := c.Start(); err != nil { 229 return nil, nil, err 230 } 231 // Wait for the ready message. 232 var b = make([]byte, 8192) 233 n, err := o.Read(b) 234 if err != nil { 235 return nil, nil, err 236 } 237 v("Server says: %q", string(b[:n])) 238 239 srvSock, err := net.Dial("tcp", "localhost:5641") 240 if err != nil { 241 return nil, nil, err 242 } 243 return srvSock, c, nil 244 } 245 246 // We only do one accept for now. 247 func forward(l net.Listener, s net.Conn) error { 248 //if err := l.SetDeadline(time.Now().Add(10 * time.Second)); err != nil { 249 //return fmt.Errorf("Can't set 9p client listen deadline: %v", err) 250 //} 251 c, err := l.Accept() 252 v("forward: c %v err %v", c, err) 253 if err != nil { 254 v("forward: accept: %v", err) 255 return err 256 } 257 go io.Copy(s, c) 258 go io.Copy(c, s) 259 return nil 260 } 261 262 // To make sure defer gets run and you tty is sane on exit 263 func runClient(host, a string) error { 264 c, err := config(*keyFile) 265 if err != nil { 266 return err 267 } 268 cl, err := dial(*network, host+":"+*port, c) 269 if err != nil { 270 return err 271 } 272 ctx, cancel := context.WithCancel(context.Background()) 273 srvSock, p, err := srv(ctx) 274 if err != nil { 275 cancel() 276 return err 277 } 278 defer func() { 279 cancel() 280 p.Wait() 281 }() 282 // Arrange port forwarding from remote ssh to our server. 283 284 // Request the remote side to open port 5640 on all interfaces. 285 // Note: cl.Listen returns a TCP listener with network is "tcp" 286 // or variants. This lets us use a listen deadline. 287 l, err := cl.Listen("tcp", "127.0.0.1:0") 288 if err != nil { 289 return fmt.Errorf("First cl.Listen %v", err) 290 } 291 ap := strings.Split(l.Addr().String(), ":") 292 if len(ap) == 0 { 293 return fmt.Errorf("Can't find a port number in %v", l.Addr().String()) 294 } 295 port := ap[len(ap)-1] 296 v("listener %T %v addr %v port %v", l, l, l.Addr().String(), port) 297 298 go forward(l, srvSock) 299 v("Connected to %v", cl) 300 301 // now run stuff. 302 if err := shell(cl, a, port); err != nil { 303 return err 304 } 305 return nil 306 } 307 308 // env sets environment variables. While we might think we ought to set 309 // HOME and PATH, it's possibly not a great idea. We leave them here as markers 310 // to remind ourselves not to try it later. 311 // We don't just grab all environment variables because complex bash functions 312 // will have no meaning to elvish. If there are simpler environment variables 313 // you want to set, add them here. Note however that even basic ones like TERM 314 // don't work either. 315 func env(s *ossh.Session) { 316 e := []string{"HOME", "PATH", "LD_LIBRARY_PATH"} 317 // HOME and PATH are not allowed to be set by many sshds. Annoying. 318 for _, v := range e { 319 if err := s.Setenv(v, os.Getenv(v)); err != nil { 320 log.Printf("Warning: s.Setenv(%q, %q): %v", v, os.Getenv(v), err) 321 } 322 } 323 } 324 325 func shell(client *ossh.Client, a, port9p string) error { 326 t, err := termios.New() 327 if err != nil { 328 return err 329 } 330 r, err := t.Raw() 331 if err != nil { 332 return err 333 } 334 defer t.Set(r) 335 if *bin == "" { 336 if *bin, err = exec.LookPath("cpu"); err != nil { 337 return err 338 } 339 } 340 a = fmt.Sprintf("%v -remote -port9p %v -bin %v %v", *bin, port9p, *bin, a) 341 v("command is %q", a) 342 session, err := client.NewSession() 343 if err != nil { 344 return err 345 } 346 defer session.Close() 347 env(session) 348 // Set up terminal modes 349 modes := ossh.TerminalModes{ 350 ossh.ECHO: 0, // disable echoing 351 ossh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 352 ossh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 353 } 354 // Request pseudo terminal 355 if err := session.RequestPty("ansi", 40, 80, modes); err != nil { 356 log.Fatal("request for pseudo terminal failed: ", err) 357 } 358 i, err := session.StdinPipe() 359 if err != nil { 360 return err 361 } 362 o, err := session.StdoutPipe() 363 if err != nil { 364 return err 365 } 366 e, err := session.StderrPipe() 367 if err != nil { 368 return err 369 } 370 371 // sshd doesn't want to set us set the HOME and PATH via the normal 372 // request route. So we do this nasty hack to ensure we can find 373 // the cpu binary. We append our paths to the one the shell has. 374 // This should suffice for u-root systems with paths including 375 // /bbin and /ubin as well as more conventional systems. 376 // The only possible flaw in this approach is elvish, which 377 // has a very odd PATH syntax. For elvish, the PATH= is ignored, 378 // so does no harm. Our use case for elvish is u-root, and 379 // we will have the right path anyway, so it will still work. 380 // It is working well in testing. 381 // cmd := fmt.Sprintf("PATH=$PATH:%s %s", os.Getenv("PATH"), a) 382 cmd := a 383 v("Start remote with command %q", cmd) 384 if err := session.Start(cmd); err != nil { 385 return fmt.Errorf("Failed to run %v: %v", a, err.Error()) 386 } 387 go io.Copy(i, os.Stdin) 388 go io.Copy(os.Stdout, o) 389 go io.Copy(os.Stderr, e) 390 return session.Wait() 391 } 392 393 // We do flag parsing in init so we can 394 // Unshare if needed while we are still 395 // single threaded. 396 func init() { 397 flag.Parse() 398 if *debug { 399 v = log.Printf 400 } 401 if os.Getpid() == 1 { 402 *runAsInit, *debug = true, true 403 v = log.Printf 404 } 405 if *remote { 406 // The unshare system call in Linux doesn't unshare mount points 407 // mounted with --shared. Systemd mounts / with --shared. For a 408 // long discussion of the pros and cons of this see debian bug 739593. 409 // The Go model of unsharing is more like Plan 9, where you ask 410 // to unshare and the namespaces are unconditionally unshared. 411 // To make this model work we must further mark / as MS_PRIVATE. 412 // This is what the standard unshare command does. 413 var ( 414 none = [...]byte{'n', 'o', 'n', 'e', 0} 415 slash = [...]byte{'/', 0} 416 flags = uintptr(unix.MS_PRIVATE | unix.MS_REC) // Thanks for nothing Linux. 417 ) 418 if err := syscall.Unshare(syscall.CLONE_NEWNS); err != nil { 419 log.Printf("bad Unshare: %v", err) 420 } 421 _, _, err1 := syscall.RawSyscall6(unix.SYS_MOUNT, uintptr(unsafe.Pointer(&none[0])), uintptr(unsafe.Pointer(&slash[0])), 0, flags, 0, 0) 422 if err1 != 0 { 423 log.Printf("Warning: unshare failed (%v). There will be no private 9p mount", err1) 424 } 425 flags = 0 426 if err := unix.Mount("cpu", "/tmp", "tmpfs", flags, ""); err != nil { 427 log.Printf("Warning: tmpfs mount on /tmp (%v) failed. There will be no 9p mount", err) 428 } 429 } 430 } 431 432 func setWinsize(f *os.File, w, h int) { 433 syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), 434 uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) 435 } 436 437 // adjust adjusts environment variables containing paths from 438 // / to /tmp/cpu. On Plan 9 this is done with a union mount. 439 // PATH variables are the union mounts of Unix so we use them 440 // instead. 441 func adjust(env []string) []string { 442 var res []string 443 for _, e := range env { 444 n := strings.SplitN(e, "=", 2) 445 if len(n) < 2 { 446 res = append(res, e) 447 continue 448 } 449 v := strings.Split(n[1], ":") 450 for i := range v { 451 if filepath.IsAbs(v[i]) { 452 v[i] = filepath.Join("/tmp/cpu", v[i]) 453 } 454 } 455 res = append(res, n[0]+"="+strings.Join(v, ":")) 456 } 457 return res 458 } 459 460 func handler(s ssh.Session) { 461 a := s.Command() 462 verbose("the handler is here, cmd is %v", a) 463 cmd := exec.Command(a[0], a[1:]...) 464 log.Printf("cmd.Env ius %v", cmd.Env) 465 adj := adjust(s.Environ()) 466 log.Printf("s.Environt is %v, adjusted is %v", s.Environ(), adj) 467 cmd.Env = append(cmd.Env, adj...) 468 ptyReq, winCh, isPty := s.Pty() 469 verbose("the command is %v", *cmd) 470 if isPty { 471 cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term)) 472 f, err := pty.Start(cmd) 473 verbose("command started with pty") 474 if err != nil { 475 log.Print(err) 476 return 477 } 478 go func() { 479 for win := range winCh { 480 setWinsize(f, win.Width, win.Height) 481 } 482 }() 483 go func() { 484 io.Copy(f, s) // stdin 485 }() 486 io.Copy(s, f) // stdout 487 } else { 488 cmd.Stdin, cmd.Stdout, cmd.Stderr = s, s, s 489 verbose("running command without pty") 490 if err := cmd.Run(); err != nil { 491 log.Print(err) 492 return 493 } 494 } 495 verbose("handler exits") 496 } 497 498 func doInit() error { 499 if err := cpuSetup(); err != nil { 500 log.Printf("CPU setup error with cpu running as init: %v", err) 501 } 502 cmds := [][]string{{"/bin/defaultsh"}, {"/bbin/dhclient", "-v"}} 503 verbose("Try to run %v", cmds) 504 505 for _, v := range cmds { 506 verbose("Let's try to run %v", v) 507 if _, err := os.Stat(v[0]); os.IsNotExist(err) { 508 verbose("it's not there") 509 continue 510 } 511 512 // I *love* special cases. Evaluate just the top-most symlink. 513 // 514 // In source mode, this would be a symlink like 515 // /buildbin/defaultsh -> /buildbin/elvish -> 516 // /buildbin/installcommand. 517 // 518 // To actually get the command to build, argv[0] has to end 519 // with /elvish, so we resolve one level of symlink. 520 if filepath.Base(v[0]) == "defaultsh" { 521 s, err := os.Readlink(v[0]) 522 if err == nil { 523 v[0] = s 524 } 525 verbose("readlink of %v returns %v", v[0], s) 526 // and, well, it might be a relative link. 527 // We must go deeper. 528 d, b := filepath.Split(v[0]) 529 d = filepath.Base(d) 530 v[0] = filepath.Join("/", os.Getenv("UROOT_ROOT"), d, b) 531 verbose("is now %v", v[0]) 532 } 533 534 cmd := exec.Command(v[0], v[1:]...) 535 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr 536 cmd.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true} 537 verbose("Run %v", cmd) 538 if err := cmd.Start(); err != nil { 539 log.Printf("Error starting %v: %v", v, err) 540 continue 541 } 542 } 543 publicKeyOption := func(ctx ssh.Context, key ssh.PublicKey) bool { 544 // Glob the users's home directory for all the 545 // possible keys? 546 data, err := ioutil.ReadFile(*pubKeyFile) 547 if err != nil { 548 fmt.Print(err) 549 return false 550 } 551 allowed, _, _, _, _ := ssh.ParseAuthorizedKey(data) 552 return ssh.KeysEqual(key, allowed) 553 } 554 555 // Now we run as an ssh server, and each time we get a connection, 556 // we run that command after setting things up for it. 557 server := ssh.Server{ 558 LocalPortForwardingCallback: ssh.LocalPortForwardingCallback(func(ctx ssh.Context, dhost string, dport uint32) bool { 559 log.Println("Accepted forward", dhost, dport) 560 return true 561 }), 562 Addr: ":" + *port, 563 PublicKeyHandler: publicKeyOption, 564 ReversePortForwardingCallback: ssh.ReversePortForwardingCallback(func(ctx ssh.Context, host string, port uint32) bool { 565 log.Println("attempt to bind", host, port, "granted") 566 return true 567 }), 568 Handler: handler, 569 } 570 571 // start the process reaper 572 procs := make(chan int) 573 go cpuDone(procs) 574 575 server.SetOption(ssh.HostKeyFile(*hostKeyFile)) 576 log.Println("starting ssh server on port " + *port) 577 if err := server.ListenAndServe(); err != nil { 578 log.Print(err) 579 } 580 verbose("server.ListenAndServer returned") 581 582 numprocs := <-procs 583 verbose("Reaped %d procs", numprocs) 584 return nil 585 } 586 587 // TODO: we've been tryinmg to figure out the right way to do usage for years. 588 // If this is a good way, it belongs in the uroot package. 589 func usage() { 590 var b bytes.Buffer 591 flag.CommandLine.SetOutput(&b) 592 flag.PrintDefaults() 593 log.Fatalf("Usage: cpu [options] host [shell command]:\n%v", b.String()) 594 } 595 596 func main() { 597 verbose("Args %v pid %d *runasinit %v *remote %v", os.Args, os.Getpid(), *runAsInit, *remote) 598 args := flag.Args() 599 switch { 600 case *runAsInit: 601 verbose("Running as Init") 602 if err := doInit(); err != nil { 603 log.Fatal(err) 604 } 605 case *remote: 606 verbose("Running as remote") 607 if err := runRemote(strings.Join(flag.Args(), " "), *port9p); err != nil { 608 log.Fatal(err) 609 } 610 default: 611 if len(args) == 0 { 612 usage() 613 } 614 host := args[0] 615 a := strings.Join(args[1:], " ") 616 verbose("Running as client") 617 if a == "" { 618 a = os.Getenv("SHELL") 619 } 620 if err := runClient(host, a); err != nil { 621 log.Fatal(err) 622 } 623 } 624 }