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  }