dc9f5c2ca3
Under the convoluted code path for the transport configuration, TLSConfig was being set even though the socket type is unix. This caused other code detecting the TLSConfig to assume https, rather than using the http scheme. This led to a situation where if `DOCKER_CERT_PATH` is set, unix sockets start reverting to https. There is other odd behavior from go-connections that is also reproduced here. For the most part, we try to reproduce the side-effecting behavior from go-connections to retain the current docker behavior. This whole mess needs to ripped out and fixed, as this pile spaghetti is unnacceptable. This code is way to convoluted for an http client. We'll need to fix this but the Go API will break to do it. Signed-off-by: Stephen J Day <stephen.day@docker.com>
174 lines
5.1 KiB
Go
174 lines
5.1 KiB
Go
package client
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/pkg/tlsconfig"
|
|
"github.com/docker/go-connections/sockets"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
// tlsClientCon holds tls information and a dialed connection.
|
|
type tlsClientCon struct {
|
|
*tls.Conn
|
|
rawConn net.Conn
|
|
}
|
|
|
|
func (c *tlsClientCon) CloseWrite() error {
|
|
// Go standard tls.Conn doesn't provide the CloseWrite() method so we do it
|
|
// on its underlying connection.
|
|
if conn, ok := c.rawConn.(types.CloseWriter); ok {
|
|
return conn.CloseWrite()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// postHijacked sends a POST request and hijacks the connection.
|
|
func (cli *Client) postHijacked(ctx context.Context, path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) {
|
|
bodyEncoded, err := encodeData(body)
|
|
if err != nil {
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
|
|
req, err := cli.newRequest("POST", path, query, bodyEncoded, headers)
|
|
if err != nil {
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
req.Host = cli.addr
|
|
|
|
req.Header.Set("Connection", "Upgrade")
|
|
req.Header.Set("Upgrade", "tcp")
|
|
|
|
conn, err := dial(cli.proto, cli.addr, resolveTLSConfig(cli.client.Transport))
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "connection refused") {
|
|
return types.HijackedResponse{}, fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?")
|
|
}
|
|
return types.HijackedResponse{}, err
|
|
}
|
|
|
|
// When we set up a TCP connection for hijack, there could be long periods
|
|
// of inactivity (a long running command with no output) that in certain
|
|
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
|
|
// state. Setting TCP KeepAlive on the socket connection will prohibit
|
|
// ECONNTIMEOUT unless the socket connection truly is broken
|
|
if tcpConn, ok := conn.(*net.TCPConn); ok {
|
|
tcpConn.SetKeepAlive(true)
|
|
tcpConn.SetKeepAlivePeriod(30 * time.Second)
|
|
}
|
|
|
|
clientconn := httputil.NewClientConn(conn, nil)
|
|
defer clientconn.Close()
|
|
|
|
// Server hijacks the connection, error 'connection closed' expected
|
|
_, err = clientconn.Do(req)
|
|
|
|
rwc, br := clientconn.Hijack()
|
|
|
|
return types.HijackedResponse{Conn: rwc, Reader: br}, err
|
|
}
|
|
|
|
func tlsDial(network, addr string, config *tls.Config) (net.Conn, error) {
|
|
return tlsDialWithDialer(new(net.Dialer), network, addr, config)
|
|
}
|
|
|
|
// We need to copy Go's implementation of tls.Dial (pkg/cryptor/tls/tls.go) in
|
|
// order to return our custom tlsClientCon struct which holds both the tls.Conn
|
|
// object _and_ its underlying raw connection. The rationale for this is that
|
|
// we need to be able to close the write end of the connection when attaching,
|
|
// which tls.Conn does not provide.
|
|
func tlsDialWithDialer(dialer *net.Dialer, network, addr string, config *tls.Config) (net.Conn, error) {
|
|
// We want the Timeout and Deadline values from dialer to cover the
|
|
// whole process: TCP connection and TLS handshake. This means that we
|
|
// also need to start our own timers now.
|
|
timeout := dialer.Timeout
|
|
|
|
if !dialer.Deadline.IsZero() {
|
|
deadlineTimeout := dialer.Deadline.Sub(time.Now())
|
|
if timeout == 0 || deadlineTimeout < timeout {
|
|
timeout = deadlineTimeout
|
|
}
|
|
}
|
|
|
|
var errChannel chan error
|
|
|
|
if timeout != 0 {
|
|
errChannel = make(chan error, 2)
|
|
time.AfterFunc(timeout, func() {
|
|
errChannel <- errors.New("")
|
|
})
|
|
}
|
|
|
|
proxyDialer, err := sockets.DialerFromEnvironment(dialer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rawConn, err := proxyDialer.Dial(network, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// When we set up a TCP connection for hijack, there could be long periods
|
|
// of inactivity (a long running command with no output) that in certain
|
|
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
|
|
// state. Setting TCP KeepAlive on the socket connection will prohibit
|
|
// ECONNTIMEOUT unless the socket connection truly is broken
|
|
if tcpConn, ok := rawConn.(*net.TCPConn); ok {
|
|
tcpConn.SetKeepAlive(true)
|
|
tcpConn.SetKeepAlivePeriod(30 * time.Second)
|
|
}
|
|
|
|
colonPos := strings.LastIndex(addr, ":")
|
|
if colonPos == -1 {
|
|
colonPos = len(addr)
|
|
}
|
|
hostname := addr[:colonPos]
|
|
|
|
// If no ServerName is set, infer the ServerName
|
|
// from the hostname we're connecting to.
|
|
if config.ServerName == "" {
|
|
// Make a copy to avoid polluting argument or default.
|
|
config = tlsconfig.Clone(config)
|
|
config.ServerName = hostname
|
|
}
|
|
|
|
conn := tls.Client(rawConn, config)
|
|
|
|
if timeout == 0 {
|
|
err = conn.Handshake()
|
|
} else {
|
|
go func() {
|
|
errChannel <- conn.Handshake()
|
|
}()
|
|
|
|
err = <-errChannel
|
|
}
|
|
|
|
if err != nil {
|
|
rawConn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// This is Docker difference with standard's crypto/tls package: returned a
|
|
// wrapper which holds both the TLS and raw connections.
|
|
return &tlsClientCon{conn, rawConn}, nil
|
|
}
|
|
|
|
func dial(proto, addr string, tlsConfig *tls.Config) (net.Conn, error) {
|
|
if tlsConfig != nil && proto != "unix" && proto != "npipe" {
|
|
// Notice this isn't Go standard's tls.Dial function
|
|
return tlsDial(proto, addr, tlsConfig)
|
|
}
|
|
if proto == "npipe" {
|
|
return sockets.DialPipe(addr, 32*time.Second)
|
|
}
|
|
return net.Dial(proto, addr)
|
|
}
|