github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/contrib/cmd/recvtty/recvtty.go (about)

     1  /*
     2   * Copyright 2016 SUSE LLC
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package main
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"net"
    24  	"os"
    25  	"strings"
    26  	"sync"
    27  
    28  	"github.com/containerd/console"
    29  	"github.com/opencontainers/runc/libcontainer/utils"
    30  	"github.com/urfave/cli"
    31  )
    32  
    33  // version will be populated by the Makefile, read from
    34  // VERSION file of the source code.
    35  var version = ""
    36  
    37  // gitCommit will be the hash that the binary was built from
    38  // and will be populated by the Makefile
    39  var gitCommit = ""
    40  
    41  const (
    42  	usage = `Open Container Initiative contrib/cmd/recvtty
    43  
    44  recvtty is a reference implementation of a consumer of runC's --console-socket
    45  API. It has two main modes of operation:
    46  
    47    * single: Only permit one terminal to be sent to the socket, which is
    48  	then hooked up to the stdio of the recvtty process. This is useful
    49  	for rudimentary shell management of a container.
    50  
    51    * null: Permit as many terminals to be sent to the socket, but they
    52  	are read to /dev/null. This is used for testing, and imitates the
    53  	old runC API's --console=/dev/pts/ptmx hack which would allow for a
    54  	similar trick. This is probably not what you want to use, unless
    55  	you're doing something like our bats integration tests.
    56  
    57  To use recvtty, just specify a socket path at which you want to receive
    58  terminals:
    59  
    60      $ recvtty [--mode <single|null>] socket.sock
    61  `
    62  )
    63  
    64  func bail(err error) {
    65  	fmt.Fprintf(os.Stderr, "[recvtty] fatal error: %v\n", err)
    66  	os.Exit(1)
    67  }
    68  
    69  func handleSingle(path string, noStdin bool) error {
    70  	// Open a socket.
    71  	ln, err := net.Listen("unix", path)
    72  	if err != nil {
    73  		return err
    74  	}
    75  	defer ln.Close()
    76  
    77  	// We only accept a single connection, since we can only really have
    78  	// one reader for os.Stdin. Plus this is all a PoC.
    79  	conn, err := ln.Accept()
    80  	if err != nil {
    81  		return err
    82  	}
    83  	defer conn.Close()
    84  
    85  	// Close ln, to allow for other instances to take over.
    86  	ln.Close()
    87  
    88  	// Get the fd of the connection.
    89  	unixconn, ok := conn.(*net.UnixConn)
    90  	if !ok {
    91  		return errors.New("failed to cast to unixconn")
    92  	}
    93  
    94  	socket, err := unixconn.File()
    95  	if err != nil {
    96  		return err
    97  	}
    98  	defer socket.Close()
    99  
   100  	// Get the master file descriptor from runC.
   101  	master, err := utils.RecvFile(socket)
   102  	if err != nil {
   103  		return err
   104  	}
   105  	c, err := console.ConsoleFromFile(master)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	if err := console.ClearONLCR(c.Fd()); err != nil {
   110  		return err
   111  	}
   112  
   113  	// Copy from our stdio to the master fd.
   114  	var (
   115  		wg            sync.WaitGroup
   116  		inErr, outErr error
   117  	)
   118  	wg.Add(1)
   119  	go func() {
   120  		_, outErr = io.Copy(os.Stdout, c)
   121  		wg.Done()
   122  	}()
   123  	if !noStdin {
   124  		wg.Add(1)
   125  		go func() {
   126  			_, inErr = io.Copy(c, os.Stdin)
   127  			wg.Done()
   128  		}()
   129  	}
   130  
   131  	// Only close the master fd once we've stopped copying.
   132  	wg.Wait()
   133  	c.Close()
   134  
   135  	if outErr != nil {
   136  		return outErr
   137  	}
   138  
   139  	return inErr
   140  }
   141  
   142  func handleNull(path string) error {
   143  	// Open a socket.
   144  	ln, err := net.Listen("unix", path)
   145  	if err != nil {
   146  		return err
   147  	}
   148  	defer ln.Close()
   149  
   150  	// As opposed to handleSingle we accept as many connections as we get, but
   151  	// we don't interact with Stdin at all (and we copy stdout to /dev/null).
   152  	for {
   153  		conn, err := ln.Accept()
   154  		if err != nil {
   155  			return err
   156  		}
   157  		go func(conn net.Conn) {
   158  			// Don't leave references lying around.
   159  			defer conn.Close()
   160  
   161  			// Get the fd of the connection.
   162  			unixconn, ok := conn.(*net.UnixConn)
   163  			if !ok {
   164  				return
   165  			}
   166  
   167  			socket, err := unixconn.File()
   168  			if err != nil {
   169  				return
   170  			}
   171  			defer socket.Close()
   172  
   173  			// Get the master file descriptor from runC.
   174  			master, err := utils.RecvFile(socket)
   175  			if err != nil {
   176  				return
   177  			}
   178  
   179  			_, _ = io.Copy(io.Discard, master)
   180  		}(conn)
   181  	}
   182  }
   183  
   184  func main() {
   185  	app := cli.NewApp()
   186  	app.Name = "recvtty"
   187  	app.Usage = usage
   188  
   189  	// Set version to be the same as runC.
   190  	var v []string
   191  	if version != "" {
   192  		v = append(v, version)
   193  	}
   194  	if gitCommit != "" {
   195  		v = append(v, "commit: "+gitCommit)
   196  	}
   197  	app.Version = strings.Join(v, "\n")
   198  
   199  	// Set the flags.
   200  	app.Flags = []cli.Flag{
   201  		cli.StringFlag{
   202  			Name:  "mode, m",
   203  			Value: "single",
   204  			Usage: "Mode of operation (single or null)",
   205  		},
   206  		cli.StringFlag{
   207  			Name:  "pid-file",
   208  			Value: "",
   209  			Usage: "Path to write daemon process ID to",
   210  		},
   211  		cli.BoolFlag{
   212  			Name:  "no-stdin",
   213  			Usage: "Disable stdin handling (no-op for null mode)",
   214  		},
   215  	}
   216  
   217  	app.Action = func(ctx *cli.Context) error {
   218  		args := ctx.Args()
   219  		if len(args) != 1 {
   220  			return errors.New("need to specify a single socket path")
   221  		}
   222  		path := ctx.Args()[0]
   223  
   224  		pidPath := ctx.String("pid-file")
   225  		if pidPath != "" {
   226  			pid := fmt.Sprintf("%d\n", os.Getpid())
   227  			if err := os.WriteFile(pidPath, []byte(pid), 0o644); err != nil {
   228  				return err
   229  			}
   230  		}
   231  
   232  		noStdin := ctx.Bool("no-stdin")
   233  		switch ctx.String("mode") {
   234  		case "single":
   235  			if err := handleSingle(path, noStdin); err != nil {
   236  				return err
   237  			}
   238  		case "null":
   239  			if err := handleNull(path); err != nil {
   240  				return err
   241  			}
   242  		default:
   243  			return fmt.Errorf("need to select a valid mode: %s", ctx.String("mode"))
   244  		}
   245  		return nil
   246  	}
   247  	if err := app.Run(os.Args); err != nil {
   248  		bail(err)
   249  	}
   250  }