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 }