2019-03-01 09:34:36 -05:00
// Package commandconn provides a net.Conn implementation that can be used for
// proxying (or emulating) stream via a custom command.
//
// For example, to provide an http.Client that can connect to a Docker daemon
// running in a Docker container ("DIND"):
//
2022-07-13 06:29:49 -04:00
// httpClient := &http.Client{
// Transport: &http.Transport{
// DialContext: func(ctx context.Context, _network, _addr string) (net.Conn, error) {
// return commandconn.New(ctx, "docker", "exec", "-it", containerID, "docker", "system", "dial-stdio")
// },
// },
// }
2019-03-01 09:34:36 -05:00
package commandconn
import (
"bytes"
"context"
"fmt"
"io"
"net"
"os"
2023-05-25 20:03:45 -04:00
"os/exec"
2019-03-01 09:34:36 -05:00
"runtime"
"strings"
"sync"
2023-04-24 04:57:16 -04:00
"sync/atomic"
2019-03-01 09:34:36 -05:00
"syscall"
"time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// New returns net.Conn
2023-03-30 10:45:07 -04:00
func New ( _ context . Context , cmd string , args ... string ) ( net . Conn , error ) {
2019-03-01 09:34:36 -05:00
var (
c commandConn
err error
)
2022-12-04 22:09:17 -05:00
c . cmd = exec . Command ( cmd , args ... )
2019-03-01 09:34:36 -05:00
// we assume that args never contains sensitive information
logrus . Debugf ( "commandconn: starting %s with %v" , cmd , args )
c . cmd . Env = os . Environ ( )
2019-03-05 01:56:50 -05:00
c . cmd . SysProcAttr = & syscall . SysProcAttr { }
2019-03-01 09:34:36 -05:00
setPdeathsig ( c . cmd )
2019-03-05 01:56:50 -05:00
createSession ( c . cmd )
2019-03-01 09:34:36 -05:00
c . stdin , err = c . cmd . StdinPipe ( )
if err != nil {
return nil , err
}
c . stdout , err = c . cmd . StdoutPipe ( )
if err != nil {
return nil , err
}
c . cmd . Stderr = & stderrWriter {
stderrMu : & c . stderrMu ,
stderr : & c . stderr ,
debugPrefix : fmt . Sprintf ( "commandconn (%s):" , cmd ) ,
}
c . localAddr = dummyAddr { network : "dummy" , s : "dummy-0" }
c . remoteAddr = dummyAddr { network : "dummy" , s : "dummy-1" }
return & c , c . cmd . Start ( )
}
// commandConn implements net.Conn
type commandConn struct {
2023-04-24 04:57:16 -04:00
cmdMutex sync . Mutex // for cmd, cmdWaitErr
cmd * exec . Cmd
cmdWaitErr error
cmdExited atomic . Bool
stdin io . WriteCloser
stdout io . ReadCloser
stderrMu sync . Mutex // for stderr
stderr bytes . Buffer
stdinClosed atomic . Bool
stdoutClosed atomic . Bool
closing atomic . Bool
localAddr net . Addr
remoteAddr net . Addr
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
// kill terminates the process. On Windows it kills the process directly,
// whereas on other platforms, a SIGTERM is sent, before forcefully terminating
// the process after 3 seconds.
func ( c * commandConn ) kill ( ) {
if c . cmdExited . Load ( ) {
return
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
c . cmdMutex . Lock ( )
2019-03-01 09:34:36 -05:00
var werr error
if runtime . GOOS != "windows" {
werrCh := make ( chan error )
2023-04-24 04:57:16 -04:00
go func ( ) { werrCh <- c . cmd . Wait ( ) } ( )
_ = c . cmd . Process . Signal ( syscall . SIGTERM )
2019-03-01 09:34:36 -05:00
select {
case werr = <- werrCh :
case <- time . After ( 3 * time . Second ) :
2023-04-24 04:57:16 -04:00
_ = c . cmd . Process . Kill ( )
2019-03-01 09:34:36 -05:00
werr = <- werrCh
}
} else {
2023-04-24 04:57:16 -04:00
_ = c . cmd . Process . Kill ( )
werr = c . cmd . Wait ( )
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
c . cmdWaitErr = werr
c . cmdMutex . Unlock ( )
c . cmdExited . Store ( true )
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
// handleEOF handles io.EOF errors while reading or writing from the underlying
// command pipes.
//
// When we've received an EOF we expect that the command will
// be terminated soon. As such, we call Wait() on the command
// and return EOF or the error depending on whether the command
// exited with an error.
//
// If Wait() does not return within 10s, an error is returned
func ( c * commandConn ) handleEOF ( err error ) error {
if err != io . EOF {
return err
2019-03-01 09:34:36 -05:00
}
c . cmdMutex . Lock ( )
2023-04-24 04:57:16 -04:00
defer c . cmdMutex . Unlock ( )
var werr error
if c . cmdExited . Load ( ) {
2019-03-01 09:34:36 -05:00
werr = c . cmdWaitErr
} else {
werrCh := make ( chan error )
go func ( ) { werrCh <- c . cmd . Wait ( ) } ( )
select {
case werr = <- werrCh :
c . cmdWaitErr = werr
2023-04-24 04:57:16 -04:00
c . cmdExited . Store ( true )
2019-03-01 09:34:36 -05:00
case <- time . After ( 10 * time . Second ) :
c . stderrMu . Lock ( )
stderr := c . stderr . String ( )
c . stderrMu . Unlock ( )
2023-04-24 04:57:16 -04:00
return errors . Errorf ( "command %v did not exit after %v: stderr=%q" , c . cmd . Args , err , stderr )
2019-03-01 09:34:36 -05:00
}
}
2023-04-24 04:57:16 -04:00
2019-03-01 09:34:36 -05:00
if werr == nil {
2023-04-24 04:57:16 -04:00
return err
2019-03-01 09:34:36 -05:00
}
c . stderrMu . Lock ( )
stderr := c . stderr . String ( )
c . stderrMu . Unlock ( )
return errors . Errorf ( "command %v has exited with %v, please make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=%s" , c . cmd . Args , werr , stderr )
}
func ignorableCloseError ( err error ) bool {
2023-04-24 04:57:16 -04:00
return strings . Contains ( err . Error ( ) , os . ErrClosed . Error ( ) )
}
func ( c * commandConn ) Read ( p [ ] byte ) ( int , error ) {
n , err := c . stdout . Read ( p )
// check after the call to Read, since
// it is blocking, and while waiting on it
// Close might get called
if c . closing . Load ( ) {
// If we're currently closing the connection
// we don't want to call onEOF, but we do want
// to return an io.EOF
return 0 , io . EOF
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
return n , c . handleEOF ( err )
}
func ( c * commandConn ) Write ( p [ ] byte ) ( int , error ) {
n , err := c . stdin . Write ( p )
// check after the call to Write, since
// it is blocking, and while waiting on it
// Close might get called
if c . closing . Load ( ) {
// If we're currently closing the connection
// we don't want to call onEOF, but we do want
// to return an io.EOF
return 0 , io . EOF
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
return n , c . handleEOF ( err )
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
// CloseRead allows commandConn to implement halfCloser
2019-03-01 09:34:36 -05:00
func ( c * commandConn ) CloseRead ( ) error {
// NOTE: maybe already closed here
if err := c . stdout . Close ( ) ; err != nil && ! ignorableCloseError ( err ) {
2023-04-24 04:57:16 -04:00
return err
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
c . stdoutClosed . Store ( true )
2019-03-01 09:34:36 -05:00
2023-04-24 04:57:16 -04:00
if c . stdinClosed . Load ( ) {
c . kill ( )
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
return nil
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
// CloseWrite allows commandConn to implement halfCloser
2019-03-01 09:34:36 -05:00
func ( c * commandConn ) CloseWrite ( ) error {
// NOTE: maybe already closed here
if err := c . stdin . Close ( ) ; err != nil && ! ignorableCloseError ( err ) {
2023-04-24 04:57:16 -04:00
return err
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
c . stdinClosed . Store ( true )
2019-03-01 09:34:36 -05:00
2023-04-24 04:57:16 -04:00
if c . stdoutClosed . Load ( ) {
c . kill ( )
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
return nil
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
// Close is the net.Conn func that gets called
// by the transport when a dial is cancelled
// due to it's context timing out. Any blocked
// Read or Write calls will be unblocked and
// return errors. It will block until the underlying
// command has terminated.
2019-03-01 09:34:36 -05:00
func ( c * commandConn ) Close ( ) error {
2023-04-24 04:57:16 -04:00
c . closing . Store ( true )
defer c . closing . Store ( false )
if err := c . CloseRead ( ) ; err != nil {
2019-03-01 09:34:36 -05:00
logrus . Warnf ( "commandConn.Close: CloseRead: %v" , err )
2023-04-24 04:57:16 -04:00
return err
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
if err := c . CloseWrite ( ) ; err != nil {
2019-03-01 09:34:36 -05:00
logrus . Warnf ( "commandConn.Close: CloseWrite: %v" , err )
2023-04-24 04:57:16 -04:00
return err
2019-03-01 09:34:36 -05:00
}
2023-04-24 04:57:16 -04:00
return nil
2019-03-01 09:34:36 -05:00
}
func ( c * commandConn ) LocalAddr ( ) net . Addr {
return c . localAddr
}
2022-09-29 11:21:51 -04:00
2019-03-01 09:34:36 -05:00
func ( c * commandConn ) RemoteAddr ( ) net . Addr {
return c . remoteAddr
}
2022-09-29 11:21:51 -04:00
2019-03-01 09:34:36 -05:00
func ( c * commandConn ) SetDeadline ( t time . Time ) error {
logrus . Debugf ( "unimplemented call: SetDeadline(%v)" , t )
return nil
}
2022-09-29 11:21:51 -04:00
2019-03-01 09:34:36 -05:00
func ( c * commandConn ) SetReadDeadline ( t time . Time ) error {
logrus . Debugf ( "unimplemented call: SetReadDeadline(%v)" , t )
return nil
}
2022-09-29 11:21:51 -04:00
2019-03-01 09:34:36 -05:00
func ( c * commandConn ) SetWriteDeadline ( t time . Time ) error {
logrus . Debugf ( "unimplemented call: SetWriteDeadline(%v)" , t )
return nil
}
type dummyAddr struct {
network string
s string
}
func ( d dummyAddr ) Network ( ) string {
return d . network
}
func ( d dummyAddr ) String ( ) string {
return d . s
}
type stderrWriter struct {
stderrMu * sync . Mutex
stderr * bytes . Buffer
debugPrefix string
}
func ( w * stderrWriter ) Write ( p [ ] byte ) ( int , error ) {
logrus . Debugf ( "%s%s" , w . debugPrefix , string ( p ) )
w . stderrMu . Lock ( )
if w . stderr . Len ( ) > 4096 {
w . stderr . Reset ( )
}
n , err := w . stderr . Write ( p )
w . stderrMu . Unlock ( )
return n , err
}