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