github.com/rkt/rkt@v1.30.1-0.20200224141603-171c416fac02/stage1/iottymux/iottymux.go (about) 1 // Copyright 2016 The rkt 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 //+build linux 16 17 package main 18 19 import ( 20 "bufio" 21 "encoding/json" 22 "flag" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "net" 27 "os" 28 "os/signal" 29 "path/filepath" 30 "strconv" 31 "syscall" 32 "time" 33 34 "github.com/appc/spec/schema/types" 35 "github.com/coreos/go-systemd/daemon" 36 "github.com/kr/pty" 37 rktlog "github.com/rkt/rkt/pkg/log" 38 stage1initcommon "github.com/rkt/rkt/stage1/init/common" 39 ) 40 41 var ( 42 log *rktlog.Logger 43 diag *rktlog.Logger 44 action string 45 appName string 46 debug bool 47 ) 48 49 const ( 50 // iottymux store several bits of information for a 51 // specific instance under /rkt/iottymux/<app>/ 52 pathPrefix = "/rkt/iottymux" 53 54 // curren version of JSON API (for `list`) 55 apiVersion = 1 56 ) 57 58 func init() { 59 // debug flag is not here, as it comes from env instead of CLI 60 flag.StringVar(&action, "action", "list", "Sub-action to perform") 61 flag.StringVar(&appName, "app", "", "Target application name") 62 } 63 64 // Endpoint represents a single attachable endpoint for an application 65 type Endpoint struct { 66 // Name, freeform (eg. stdin, tty, etc.) 67 Name string `json:"name"` 68 // Domain, golang compatible (eg. tcp4, unix, etc.) 69 Domain string `json:"domain"` 70 // Address, golang compatible (eg. 127.0.0.1:3333, /tmp/file.sock, etc.) 71 Address string `json:"address"` 72 } 73 74 // Targets references all attachable endpoints, for status persistence 75 type Targets struct { 76 Version int `json:"version"` 77 Targets []Endpoint `json:"targets"` 78 } 79 80 // iottymux is a multi-action binary which can be used for: 81 // * creating and muxing a TTY for an application 82 // * proxying streams for an application (stdin/stdout/stderr) over TCP 83 // * listing available attachable endpoints for an application 84 func main() { 85 var err error 86 // Parse flag and initialize logging 87 flag.Parse() 88 if os.Getenv("STAGE1_DEBUG") == "true" { 89 debug = true 90 } 91 stage1initcommon.InitDebug(debug) 92 log, diag, _ = rktlog.NewLogSet("iottymux", debug) 93 if !debug { 94 diag.SetOutput(ioutil.Discard) 95 } 96 97 // validate app name 98 _, err = types.NewACName(appName) 99 if err != nil { 100 log.Printf("invalid app name (%s): %v", appName, err) 101 os.Exit(254) 102 } 103 104 var r error 105 statusFile := filepath.Join(pathPrefix, appName, "endpoints") 106 107 // TODO(lucab): split this some more. Mux is part of pod service, 108 // while other actions are called from stage0. Those should be split. 109 switch action { 110 // attaching 111 case "auto-attach": 112 r = actionAttach(statusFile, true) 113 case "custom-attach": 114 r = actionAttach(statusFile, false) 115 116 // muxing and proxying 117 case "iomux": 118 r = actionIOMux(statusFile) 119 case "ttymux": 120 r = actionTTYMux(statusFile) 121 122 // listing 123 case "list": 124 r = actionPrint(statusFile, os.Stdout) 125 126 default: 127 r = fmt.Errorf("unknown action %q", action) 128 } 129 130 if r != nil && r != io.EOF { 131 log.FatalE("runtime failure", r) 132 } 133 os.Exit(0) 134 } 135 136 // actionAttach handles the attach action, either in "automatic" or "custom endpoints" mode. 137 func actionAttach(statusPath string, autoMode bool) error { 138 var endpoints Targets 139 dialTimeout := 15 * time.Second 140 141 // retrieve available endpoints 142 statusFile, err := os.OpenFile(statusPath, os.O_RDONLY, os.ModePerm) 143 if err != nil { 144 return err 145 } 146 err = json.NewDecoder(statusFile).Decode(&endpoints) 147 _ = statusFile.Close() 148 if err != nil { 149 return err 150 } 151 152 // retrieve custom attaching modes 153 customTargets := struct { 154 ttyIn bool 155 ttyOut bool 156 stdin bool 157 stdout bool 158 stderr bool 159 }{} 160 if !autoMode { 161 customTargets.ttyIn, _ = strconv.ParseBool(os.Getenv("STAGE2_ATTACH_TTYIN")) 162 customTargets.ttyOut, _ = strconv.ParseBool(os.Getenv("STAGE2_ATTACH_TTYOUT")) 163 customTargets.stdin, _ = strconv.ParseBool(os.Getenv("STAGE2_ATTACH_STDIN")) 164 customTargets.stdout, _ = strconv.ParseBool(os.Getenv("STAGE2_ATTACH_STDOUT")) 165 customTargets.stderr, _ = strconv.ParseBool(os.Getenv("STAGE2_ATTACH_STDERR")) 166 } 167 168 // Proxy I/O between this process and the iottymux service: 169 // - input (stdin, tty-in) copying routines can only be canceled by process killing (ie. user detaching) 170 // - output (stdout, stderr, tty-out) copying routines are canceled by errors when reading from remote service 171 c := make(chan error) 172 copyOut := func(w io.Writer, conn net.Conn) { 173 _, err := io.Copy(w, conn) 174 c <- err 175 } 176 177 for _, ep := range endpoints.Targets { 178 d := net.Dialer{Timeout: dialTimeout} 179 conn, err := d.Dial(ep.Domain, ep.Address) 180 if err != nil { 181 return err 182 } 183 defer conn.Close() 184 switch ep.Name { 185 case "stdin": 186 if autoMode || customTargets.stdin { 187 go io.Copy(conn, os.Stdin) 188 } 189 case "stdout": 190 if autoMode || customTargets.stdout { 191 go copyOut(os.Stdout, conn) 192 } 193 case "stderr": 194 if autoMode || customTargets.stderr { 195 go copyOut(os.Stderr, conn) 196 } 197 case "tty": 198 if autoMode || customTargets.ttyIn { 199 go io.Copy(conn, os.Stdin) 200 } 201 202 if autoMode || customTargets.ttyOut { 203 go copyOut(os.Stdout, conn) 204 } else { 205 go copyOut(ioutil.Discard, conn) 206 } 207 } 208 } 209 210 // as soon as one output copying routine fails, this unblocks and the whole process exits 211 return <-c 212 } 213 214 // actionPrint prints out available endpoints by unmarshalling the Targets struct 215 // from JSON at the given path. This is used by external tools to see which attaching 216 // modes are available for an application (eg. `rkt attach --mode=list`) 217 func actionPrint(path string, out io.Writer) error { 218 var endpoints Targets 219 statusFile, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm) 220 if err != nil { 221 return err 222 } 223 224 err = json.NewDecoder(statusFile).Decode(&endpoints) 225 _ = statusFile.Close() 226 if err != nil { 227 return err 228 } 229 230 // TODO(lucab): move to encoder.SetIndent (golang >= 1.7) 231 status, err := json.MarshalIndent(endpoints, "", " ") 232 if err != nil { 233 return nil 234 } 235 _, err = out.Write(status) 236 return err 237 } 238 239 // actionTTYMux handles TTY muxing and proxying. 240 // It creates a PTY pair and bind-mounts the slave to `/rkt/iottymux/<app>/stage2-pts`. 241 // Once ready, it sd-notifies as READY so that the main application can be started. 242 func actionTTYMux(statusFile string) error { 243 // Open a new TTY pair (master/slave) 244 ptm, pts, err := pty.Open() 245 if err != nil { 246 return err 247 } 248 ttySlavePath := pts.Name() 249 _ = pts.Close() 250 defer ptm.Close() 251 diag.Printf("TTY created, slave pty at %q\n", ttySlavePath) 252 253 // TODO(lucab): set a sane TTY mode here (echo, controls and such). 254 255 // Slave TTY has a dynamic name (eg. /dev/pts/<n>) but a predictable name 256 // is needed, in order to be used as `TTYPath=` value in application unit. 257 // A bind mount is put in place for that, here. 258 ttypath := filepath.Join(pathPrefix, appName, "stage2-pts") 259 f, err := os.Create(ttypath) 260 if err != nil { 261 return err 262 } 263 err = syscall.Mount(pts.Name(), ttypath, "", syscall.MS_BIND, "") 264 if err != nil { 265 return err 266 } 267 // TODO(lucab): double-check this is fine here (alternatives: app-stop or app-rm) 268 defer syscall.Unmount(ttypath, 0) 269 defer f.Close() 270 271 // TODO(lucab): investigate sending fd to systemd-manager to ensure we never close 272 // the PTY master fd. Open questions: dupfd and ownership. 273 274 // signal to systemd that the PTY is ready and application can start. 275 // sd-notify is required here, so a non-delivered status is an hard failure. 276 ok, err := daemon.SdNotify(true, "READY=1") 277 if !ok { 278 return fmt.Errorf("failure during startup notification: %v", err) 279 } 280 diag.Print("TTY handler ready\n") 281 282 // Open sockets 283 ep := Endpoint{ 284 Name: "tty", 285 Domain: "unix", 286 Address: filepath.Join(pathPrefix, appName, "sock-tty"), 287 } 288 endpoints := Targets{apiVersion, []Endpoint{ep}} 289 listener, err := net.Listen(ep.Domain, ep.Address) 290 if err != nil { 291 return fmt.Errorf("unable to open tty listener: %s", err) 292 } 293 defer listener.Close() 294 diag.Printf("Listening for TTY on %s\n", ep.Address) 295 296 // write available endpoints to status file 297 sf, err := os.OpenFile(statusFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) 298 if err != nil { 299 return err 300 } 301 err = json.NewEncoder(sf).Encode(endpoints) 302 _ = sf.Close() 303 if err != nil { 304 return err 305 } 306 307 // Proxy between TTY and remote clients. 308 c := make(chan error) 309 clients := make(chan net.Conn) 310 go acceptConn(listener, clients, "tty") 311 go proxyIO(clients, ptm, c) 312 313 dispatchSig(c) 314 315 // If nothing else fails, ttymux service will be waiting here forever 316 // and be terminated by systemd only when the main application exits. 317 return <-c 318 } 319 320 // actionIOMux handles I/O streams muxing and proxying (stdin/stdout/stderr) 321 func actionIOMux(statusFile string) error { 322 // Slice containing mapping for "fdnum -> stream -> fifo -> socket": 323 // 0 -> stdin -> /rkt/iottymux/<app>/stage2-stdin -> /rkt/iottymux/<app>/sock-stdin 324 // 1 -> stdout -> /rkt/iottymux/<app>/stage2-stdout -> /rkt/iottymux/<app>/sock-stdout 325 // 2 -> stderr -> /rkt/iottymux/<app>/stage2-stderr -> /rkt/iottymux/<app>/sock-stderr 326 streams := [3]struct { 327 listener net.Listener 328 fifo *os.File 329 }{} 330 331 // open FIFOs and create sockets 332 streamsSetup := [3]struct { 333 streamName string 334 isEnabled bool 335 fifoPath string 336 fifoOpenFlags int 337 socketDomain string 338 socketAddress string 339 }{ 340 { 341 "stdin", 342 false, 343 filepath.Join(pathPrefix, appName, "stage2-stdin"), 344 os.O_WRONLY, 345 "unix", 346 filepath.Join(pathPrefix, appName, "sock-stdin"), 347 }, 348 { 349 "stdout", 350 false, 351 filepath.Join(pathPrefix, appName, "stage2-stdout"), 352 os.O_RDONLY, 353 "unix", 354 filepath.Join(pathPrefix, appName, "sock-stdout"), 355 }, 356 { 357 "stderr", 358 false, 359 filepath.Join(pathPrefix, appName, "stage2-stderr"), 360 os.O_RDONLY, 361 "unix", 362 filepath.Join(pathPrefix, appName, "sock-stderr"), 363 }, 364 } 365 for i, f := range [3]string{"STAGE2_STDIN", "STAGE2_STDOUT", "STAGE2_STDERR"} { 366 streamsSetup[i].isEnabled, _ = strconv.ParseBool(os.Getenv(f)) 367 } 368 369 var endpoints Targets 370 endpoints.Version = 1 371 for i, entry := range streamsSetup { 372 if streamsSetup[i].isEnabled { 373 var err error 374 ep := Endpoint{ 375 Name: entry.streamName, 376 Domain: entry.socketDomain, 377 Address: entry.socketAddress, 378 } 379 streams[i].fifo, err = os.OpenFile(entry.fifoPath, entry.fifoOpenFlags, os.ModeNamedPipe) 380 if err != nil { 381 return fmt.Errorf("invalid %s FIFO: %s", entry.streamName, err) 382 } 383 defer streams[i].fifo.Close() 384 streams[i].listener, err = net.Listen(ep.Domain, ep.Address) 385 if err != nil { 386 return fmt.Errorf("unable to open %s listener: %s", entry.streamName, err) 387 } 388 defer streams[i].listener.Close() 389 endpoints.Targets = append(endpoints.Targets, ep) 390 diag.Printf("Listening for %s on %s\n", entry.streamName, ep.Address) 391 } 392 } 393 394 // write available endpoints to status file 395 sf, err := os.OpenFile(statusFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) 396 if err != nil { 397 return err 398 } 399 err = json.NewEncoder(sf).Encode(endpoints) 400 _ = sf.Close() 401 if err != nil { 402 return err 403 } 404 405 c := make(chan error) 406 407 // TODO(lucab): finalize custom logging modes 408 logMode := os.Getenv("STAGE1_LOGMODE") 409 var logFile *os.File 410 switch logMode { 411 case "k8s-plain": 412 var err error 413 414 // TODO(nhlfr): logPath coming from CRI/kubelet should be always a file name, 415 // but we may want to ensure that here and check that value explicitly. 416 logPath := os.Getenv("KUBERNETES_LOG_PATH") 417 logFullPath := filepath.Clean(filepath.Join("/rkt/kubernetes/log", logPath)) 418 419 match, err := filepath.Match("/rkt/kubernetes/log/*", logFullPath) 420 if err != nil { 421 return fmt.Errorf("couldn't analyze the full log path %s: %s", logFullPath, err) 422 } else if !match { 423 return fmt.Errorf("log path is not inside /rkt/kubernetes/log, refusing path traversal") 424 } 425 426 logFile, err = os.OpenFile(logFullPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) 427 if err != nil { 428 return err 429 } 430 defer logFile.Close() 431 } 432 433 // proxy stdin 434 if streams[0].fifo != nil && streams[0].listener != nil { 435 clients := make(chan net.Conn) 436 go acceptConn(streams[0].listener, clients, "stdin") 437 go muxInput(clients, streams[0].fifo) 438 } 439 440 // proxy stdout 441 if streams[1].fifo != nil && streams[1].listener != nil { 442 localTargets := make(chan io.WriteCloser) 443 clients := make(chan net.Conn) 444 lines := make(chan []byte) 445 go bufferLine(streams[1].fifo, lines, c) 446 go acceptConn(streams[1].listener, clients, "stdout") 447 go muxOutput("stdout", lines, clients, localTargets) 448 if logFile != nil { 449 localTargets <- logFile 450 } 451 } 452 453 // proxy stderr 454 if streams[2].fifo != nil && streams[2].listener != nil { 455 localTargets := make(chan io.WriteCloser) 456 clients := make(chan net.Conn) 457 lines := make(chan []byte) 458 go bufferLine(streams[2].fifo, lines, c) 459 go acceptConn(streams[2].listener, clients, "stderr") 460 go muxOutput("stderr", lines, clients, localTargets) 461 if logFile != nil { 462 localTargets <- logFile 463 } 464 } 465 466 dispatchSig(c) 467 468 // If nothing else fails, iomux service will be waiting here forever 469 // and be terminated by systemd only when the main application exits. 470 return <-c 471 } 472 473 // dispatchSig launches a goroutine and closes the given stop channel 474 // when SIGTERM, SIGHUP, or SIGINT is received. 475 func dispatchSig(stop chan<- error) { 476 sigChan := make(chan os.Signal) 477 signal.Notify( 478 sigChan, 479 syscall.SIGTERM, 480 syscall.SIGHUP, 481 syscall.SIGINT, 482 ) 483 484 go func() { 485 diag.Println("Waiting for signal") 486 sig := <-sigChan 487 diag.Printf("Received signal %v\n", sig) 488 close(stop) 489 }() 490 } 491 492 // bufferLine buffers and queues a single line from a Reader to a multiplexer 493 // If reading from src fails, it hard-fails and propagates the error back. 494 func bufferLine(src io.Reader, c chan<- []byte, ec chan<- error) { 495 rd := bufio.NewReader(src) 496 for { 497 lineOut, err := rd.ReadBytes('\n') 498 if len(lineOut) > 0 { 499 c <- lineOut 500 } 501 if err != nil { 502 ec <- err 503 } 504 } 505 } 506 507 // acceptConn accepts a single client and queues it for further proxying 508 // It is never canceled explicitly, as it is bound to the lifetime of the main process. 509 func acceptConn(socket net.Listener, c chan<- net.Conn, stream string) { 510 for { 511 conn, err := socket.Accept() 512 if err == nil { 513 diag.Printf("Accepted new connection for %s\n", stream) 514 c <- conn 515 } 516 } 517 } 518 519 // proxyIO performs bi-directional byte-by-byte forwarding 520 // TODO(lucab): this may become line-buffered and muxed to logs 521 // TODO(lucab): reset terminal state on new attach 522 func proxyIO(clients <-chan net.Conn, tty *os.File, ttyFailure chan<- error) { 523 ec := make(chan error) 524 525 // ttyToRemote copies output from application TTY to remote client. 526 // If copier reaches TTY EOF, it hard-fails and propagates the error up. 527 ttyToRemote := func(dst net.Conn, src *os.File) { 528 _, err := io.Copy(dst, src) 529 if err == nil { 530 _ = dst.Close() 531 close(ec) 532 } 533 } 534 535 // remoteToTTY copies input from remote client to application TTY. 536 // When copying stops/fails, it recovers and just closes this connection. 537 remoteToTTY := func(dst *os.File, src net.Conn) { 538 io.Copy(dst, src) 539 src.Close() 540 } 541 542 for { 543 select { 544 // a new remote client 545 case cl := <-clients: 546 go ttyToRemote(cl, tty) 547 go remoteToTTY(tty, cl) 548 549 // a TTY failure from one of the copier 550 case tf := <-ec: 551 ttyFailure <- tf 552 return 553 } 554 } 555 } 556 557 // muxInput accepts remote clients and multiplex input line from them 558 func muxInput(clients <-chan net.Conn, stdin *os.File) { 559 for { 560 select { 561 case c := <-clients: 562 go bufferInput(c, stdin) 563 } 564 } 565 } 566 567 // bufferInput buffers and write a single line from a remote client to the local app 568 func bufferInput(conn net.Conn, stdin *os.File) { 569 rd := bufio.NewReader(conn) 570 defer conn.Close() 571 for { 572 lineIn, err := rd.ReadBytes('\n') 573 if len(lineIn) == 0 && err != nil { 574 return 575 } 576 _, err = stdin.Write(lineIn) 577 if err != nil { 578 return 579 } 580 } 581 } 582 583 // muxOutput receives remote clients and local log targets, 584 // multiplexing output lines to them 585 func muxOutput(streamLabel string, lines chan []byte, clients <-chan net.Conn, targets <-chan io.WriteCloser) { 586 var logs []io.WriteCloser 587 var conns []io.WriteCloser 588 589 writeAndFilter := func(wc io.WriteCloser, line []byte) bool { 590 _, err := wc.Write(line) 591 if err != nil { 592 wc.Close() 593 } 594 return err != nil 595 } 596 597 logsWriteAndFilter := func(wc io.WriteCloser, line []byte) bool { 598 out := []byte(fmt.Sprintf("%s %s %s", time.Now().Format(time.RFC3339Nano), streamLabel, line)) 599 return writeAndFilter(wc, out) 600 } 601 602 for { 603 select { 604 // an incoming output line to multiplex 605 // TODO(lucab): ordered non-blocking writes 606 case l := <-lines: 607 conns = filterTargets(conns, l, writeAndFilter) 608 logs = filterTargets(logs, l, logsWriteAndFilter) 609 610 // a new remote client 611 case c := <-clients: 612 conns = append(conns, c) 613 614 // a new local log target 615 case t := <-targets: 616 logs = append(logs, t) 617 } 618 } 619 } 620 621 // filterTargets passes line to each writer in wcs, 622 // filtering out single writers if filter returns true. 623 func filterTargets( 624 wcs []io.WriteCloser, 625 line []byte, 626 filter func(io.WriteCloser, []byte) bool, 627 ) []io.WriteCloser { 628 var filteredTargets []io.WriteCloser 629 630 for _, c := range wcs { 631 if !filter(c, line) { 632 filteredTargets = append(filteredTargets, c) 633 } 634 } 635 return filteredTargets 636 }