-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Closed
Closed
Copy link
Labels
Description
What version of Go are you using (go version
)?
$ go version go version go1.16.6 darwin/amd64
Does this issue reproduce with the latest release?
Yes
What operating system and processor architecture are you using (go env
)?
go env
Output
$ go env GO111MODULE="" GOARCH="amd64" GOBIN="" GOCACHE="/Users/prologic/Library/Caches/go-build" GOENV="/Users/prologic/Library/Application Support/go/env" GOEXE="" GOFLAGS="" GOHOSTARCH="amd64" GOHOSTOS="darwin" GOINSECURE="" GOMODCACHE="/Users/prologic/go/pkg/mod" GONOPROXY="" GONOSUMDB="" GOOS="darwin" GOPATH="/Users/prologic/go" GOPRIVATE="" GOPROXY="https://goproxy.mills.io/" GOROOT="/usr/local/Cellar/go/1.16.6/libexec" GOSUMDB="sum.golang.org" GOTMPDIR="" GOTOOLDIR="/usr/local/Cellar/go/1.16.6/libexec/pkg/tool/darwin_amd64" GOVCS="" GOVERSION="go1.16.6" GCCGO="gccgo" AR="ar" CC="clang" CXX="clang++" CGO_ENABLED="1" GOMOD="/Users/prologic/Projects/go-http-bug/go.mod" CGO_CFLAGS="-g -O2" CGO_CPPFLAGS="" CGO_CXXFLAGS="-g -O2" CGO_FFLAGS="-g -O2" CGO_LDFLAGS="-g -O2" PKG_CONFIG="pkg-config" GOGCCFLAGS="-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/8h/c9gl4gms3fjb0kyf7nb42ybh0000gn/T/go-build2407089176=/tmp/go-build -gno-record-gcc-switches -fno-common"
What did you do?
The follow code (re-pasted here for posterity) exhibits a bug? very similar to this issue that was discussed and closed with no fixes/changes? to the Go std library some years ago.
package main
import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"sync"
"time"
"github.com/gliderlabs/ssh"
log "github.com/sirupsen/logrus"
"github.com/tv42/httpunix"
)
// StdioAddr implements net.Addr to provide an emulated network
// address for use with StdioConn.
type StdioAddr struct {
id string
}
func (s *StdioAddr) Network() string {
return "stdio"
}
func (s *StdioAddr) String() string {
return s.id
}
func NewStdioConn(localID, remoteID string, in io.ReadCloser, out io.WriteCloser) *StdioConn {
return &StdioConn{
local: &StdioAddr{localID},
remote: &StdioAddr{remoteID},
in: in,
out: out,
}
}
// StdioConn implements net.Conn to provide an emulated network
// connection between two processes over stdin/stdout.
type StdioConn struct {
sync.RWMutex
readClosed bool
writeClosed bool
local *StdioAddr
remote *StdioAddr
in io.ReadCloser
out io.WriteCloser
}
func (sc *StdioConn) String() string {
return fmt.Sprintf("%s<->%s", sc.LocalAddr(), sc.RemoteAddr())
}
func (sc *StdioConn) isClosed() bool {
sc.RLock()
defer sc.RUnlock()
return sc.writeClosed && sc.readClosed
}
func (sc *StdioConn) isReadClosed() bool {
sc.RLock()
defer sc.RUnlock()
return sc.readClosed
}
func (sc *StdioConn) isWriteClosed() bool {
sc.RLock()
defer sc.RUnlock()
return sc.writeClosed
}
func (sc *StdioConn) Read(b []byte) (int, error) {
if sc.isReadClosed() {
return 0, errors.New("read on closed conn")
}
return sc.in.Read(b)
}
func (sc *StdioConn) Write(b []byte) (int, error) {
if sc.isWriteClosed() {
return 0, errors.New("write on closed conn")
}
return sc.out.Write(b)
}
func (sc *StdioConn) CloseRead() error {
sc.Lock()
defer sc.Unlock()
sc.readClosed = true
return sc.in.Close()
}
func (sc *StdioConn) CloseWrite() error {
sc.Lock()
defer sc.Unlock()
sc.writeClosed = true
return sc.out.Close()
}
func (sc *StdioConn) Close() error {
if err := sc.CloseRead(); err != nil {
return err
}
return sc.CloseWrite()
}
func (sc *StdioConn) LocalAddr() net.Addr {
return sc.local
}
func (sc *StdioConn) RemoteAddr() net.Addr {
return sc.remote
}
func (sc *StdioConn) SetDeadline(t time.Time) error {
return nil
}
func (sc *StdioConn) SetReadDeadline(t time.Time) error {
return nil
}
func (sc *StdioConn) SetWriteDeadline(t time.Time) error {
return nil
}
func NewStdioListener(conn *StdioConn) *StdioListener {
sl := &StdioListener{
ready: make(chan *StdioConn),
conn: conn,
}
sl.Close()
return sl
}
// StdioListener wraps a *StdioConn to implement net.Listener.
type StdioListener struct {
ready chan *StdioConn
conn *StdioConn
}
func (sl *StdioListener) Accept() (net.Conn, error) {
if sl.conn.isClosed() {
return nil, errors.New("accept on closed conn")
}
return <-sl.ready, nil
}
func (sl *StdioListener) Addr() net.Addr {
return sl.conn.LocalAddr()
}
func (sl *StdioListener) Close() error {
if sl.conn.isClosed() {
return nil
}
go func() {
sl.ready <- sl.conn
}()
return nil
}
func main() {
ssh.Handle(func(sess ssh.Session) {
switch sess.RawCommand() {
case "docker system dial-stdio":
default:
sess.Write([]byte("This is not a normal SSH server\n"))
sess.Exit(255)
}
conn := NewStdioConn(
sess.LocalAddr().String(),
sess.RemoteAddr().String(),
sess.(io.ReadCloser),
sess.(io.WriteCloser),
)
ln := NewStdioListener(conn)
t := &httpunix.Transport{
DialTimeout: 100 * time.Millisecond,
RequestTimeout: 1 * time.Second,
ResponseHeaderTimeout: 1 * time.Second,
}
t.RegisterLocation("docker", "/var/run/docker.sock")
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = "http+unix"
req.URL.Host = "docker"
},
Transport: t,
}
svr := &http.Server{Handler: proxy}
if err := svr.Serve(ln); err != nil {
log.WithError(err).Error("error serving http session")
}
})
log.Fatal(ssh.ListenAndServe(":2222", nil))
}
Compiling and running this:
- Ensure you have a Docker daemon running on your system (either Linux or Docker for Desktop)
- Compile the above code with Go111Modules enabled
- Run without arguments
- Connect a
docker
client with a simple:
DOCKER_HOST=ssh://localhost:2222 docker run -i --rm alpine echo Hello World
For clarity:
$ mkdir go-http-hijack-bug
$ cd go-http-hijack-bug
$ wget https://gist.github.com/prologic/62dfb545e86aa1d066f8e7e1f3bf8de7/raw/5e0e8abe6572be0afb24116b1fe1c747363ae537/main.go
$ go mod init go-http-hijack-bug
$ go mod tidy
$ go build
$ ./go-http-hijack-bug
And connect a Docker CLI client:
$ DOCKER_HOST=ssh://localhost:2222 docker run -i --rm alpine echo Hello World
What did you expect to see?
I expect to see:
Hello World
from running the Docker CLI client with the above sample.
This should also in turn start and attach to the created container. You can see the container has in fact been created in a "created" state:
$ docker ps -a | head
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e2c51aca3d97 alpine "echo Hello World" About a minute ago Created zealous_goldberg
What did you see instead?
Observe that the Docker CLI client above "hangs" with the expected output of "Hello World" not shown and the container is never run.