소스 검색

Ignore invalid host header between go1.6 and old docker clients

BenchmarkWithHack-4	   50000	     37082 ns/op	  44.50
MB/s	    1920 B/op	      30 allocs/op
BenchmarkNoHack-4  	   50000	     30829 ns/op	  53.52
MB/s	       0 B/op	       0 allocs/op

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
Signed-off-by: Antonio Murdaca <runcom@redhat.com>
Antonio Murdaca 9 년 전
부모
커밋
3d6f5984f5

+ 3 - 2
cmd/dockerd/daemon.go

@@ -228,10 +228,11 @@ func (cli *DaemonCli) start() (err error) {
 		if proto == "tcp" && (serverConfig.TLSConfig == nil || serverConfig.TLSConfig.ClientAuth != tls.RequireAndVerifyClientCert) {
 		if proto == "tcp" && (serverConfig.TLSConfig == nil || serverConfig.TLSConfig.ClientAuth != tls.RequireAndVerifyClientCert) {
 			logrus.Warn("[!] DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING [!]")
 			logrus.Warn("[!] DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING [!]")
 		}
 		}
-		l, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
+		ls, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
+		ls = wrapListeners(proto, ls)
 		// If we're binding to a TCP port, make sure that a container doesn't try to use it.
 		// If we're binding to a TCP port, make sure that a container doesn't try to use it.
 		if proto == "tcp" {
 		if proto == "tcp" {
 			if err := allocateDaemonPort(addr); err != nil {
 			if err := allocateDaemonPort(addr); err != nil {
@@ -239,7 +240,7 @@ func (cli *DaemonCli) start() (err error) {
 			}
 			}
 		}
 		}
 		logrus.Debugf("Listener created for HTTP on %s (%s)", protoAddrParts[0], protoAddrParts[1])
 		logrus.Debugf("Listener created for HTTP on %s (%s)", protoAddrParts[0], protoAddrParts[1])
-		api.Accept(protoAddrParts[1], l...)
+		api.Accept(protoAddrParts[1], ls...)
 	}
 	}
 
 
 	if err := migrateKey(); err != nil {
 	if err := migrateKey(); err != nil {

+ 15 - 0
cmd/dockerd/daemon_unix.go

@@ -11,6 +11,7 @@ import (
 	"strconv"
 	"strconv"
 	"syscall"
 	"syscall"
 
 
+	"github.com/docker/docker/cmd/dockerd/hack"
 	"github.com/docker/docker/daemon"
 	"github.com/docker/docker/daemon"
 	"github.com/docker/docker/libcontainerd"
 	"github.com/docker/docker/libcontainerd"
 	"github.com/docker/docker/pkg/system"
 	"github.com/docker/docker/pkg/system"
@@ -111,3 +112,17 @@ func allocateDaemonPort(addr string) error {
 // notifyShutdown is called after the daemon shuts down but before the process exits.
 // notifyShutdown is called after the daemon shuts down but before the process exits.
 func notifyShutdown(err error) {
 func notifyShutdown(err error) {
 }
 }
+
+func wrapListeners(proto string, ls []net.Listener) []net.Listener {
+	if os.Getenv("DOCKER_HTTP_HOST_COMPAT") != "" {
+		switch proto {
+		case "unix":
+			ls[0] = &hack.MalformedHostHeaderOverride{ls[0]}
+		case "fd":
+			for i := range ls {
+				ls[i] = &hack.MalformedHostHeaderOverride{ls[i]}
+			}
+		}
+	}
+	return ls
+}

+ 5 - 0
cmd/dockerd/daemon_windows.go

@@ -2,6 +2,7 @@ package main
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"net"
 	"os"
 	"os"
 	"syscall"
 	"syscall"
 
 
@@ -75,3 +76,7 @@ func (cli *DaemonCli) getLibcontainerdRoot() string {
 func allocateDaemonPort(addr string) error {
 func allocateDaemonPort(addr string) error {
 	return nil
 	return nil
 }
 }
+
+func wrapListeners(proto string, ls []net.Listener) []net.Listener {
+	return ls
+}

+ 116 - 0
cmd/dockerd/hack/malformed_host_override.go

@@ -0,0 +1,116 @@
+// +build !windows
+
+package hack
+
+import "net"
+
+// MalformedHostHeaderOverride is a wrapper to be able
+// to overcome the 400 Bad request coming from old docker
+// clients that send an invalid Host header.
+type MalformedHostHeaderOverride struct {
+	net.Listener
+}
+
+// MalformedHostHeaderOverrideConn wraps the underlying unix
+// connection and keeps track of the first read from http.Server
+// which just reads the headers.
+type MalformedHostHeaderOverrideConn struct {
+	net.Conn
+	first bool
+}
+
+var closeConnHeader = []byte("\r\nConnection: close\r")
+
+// Read reads the first *read* request from http.Server to inspect
+// the Host header. If the Host starts with / then we're talking to
+// an old docker client which send an invalid Host header. To not
+// error out in http.Server we rewrite the first bytes of the request
+// to sanitize the Host header itself.
+// In case we're not dealing with old docker clients the data is just passed
+// to the server w/o modification.
+func (l *MalformedHostHeaderOverrideConn) Read(b []byte) (n int, err error) {
+	// http.Server uses a 4k buffer
+	if l.first && len(b) == 4096 {
+		// This keeps track of the first read from http.Server which just reads
+		// the headers
+		l.first = false
+		// The first read of the connection by http.Server is done limited to
+		// DefaultMaxHeaderBytes (usually 1 << 20) + 4096.
+		// Here we do the first read which gets us all the http headers to
+		// be inspected and modified below.
+		c, err := l.Conn.Read(b)
+		if err != nil {
+			return c, err
+		}
+
+		var (
+			start, end    int
+			firstLineFeed = -1
+			buf           []byte
+		)
+		for i, bb := range b[:c] {
+			if bb == '\n' && firstLineFeed == -1 {
+				firstLineFeed = i
+			}
+			if bb != '\n' {
+				continue
+			}
+			if b[i+1] != 'H' {
+				continue
+			}
+			if b[i+2] != 'o' {
+				continue
+			}
+			if b[i+3] != 's' {
+				continue
+			}
+			if b[i+4] != 't' {
+				continue
+			}
+			if b[i+5] != ':' {
+				continue
+			}
+			if b[i+6] != ' ' {
+				continue
+			}
+			if b[i+7] != '/' {
+				continue
+			}
+			// ensure clients other than the docker clients do not get this hack
+			if i != firstLineFeed {
+				return c, nil
+			}
+			start = i + 7
+			// now find where the value ends
+			for ii, bbb := range b[start:c] {
+				if bbb == '\n' {
+					end = start + ii
+					break
+				}
+			}
+			buf = make([]byte, 0, c+len(closeConnHeader)-(end-start))
+			// strip the value of the host header and
+			// inject `Connection: close` to ensure we don't reuse this connection
+			buf = append(buf, b[:start]...)
+			buf = append(buf, closeConnHeader...)
+			buf = append(buf, b[end:c]...)
+			copy(b, buf)
+			break
+		}
+		if len(buf) == 0 {
+			return c, nil
+		}
+		return len(buf), nil
+	}
+	return l.Conn.Read(b)
+}
+
+// Accept makes the listener accepts connections and wraps the connection
+// in a MalformedHostHeaderOverrideConn initilizing first to true.
+func (l *MalformedHostHeaderOverride) Accept() (net.Conn, error) {
+	c, err := l.Listener.Accept()
+	if err != nil {
+		return c, err
+	}
+	return &MalformedHostHeaderOverrideConn{c, true}, nil
+}

+ 115 - 0
cmd/dockerd/hack/malformed_host_override_test.go

@@ -0,0 +1,115 @@
+// +build !windows
+
+package hack
+
+import (
+	"bytes"
+	"io"
+	"net"
+	"strings"
+	"testing"
+)
+
+func TestHeaderOverrideHack(t *testing.T) {
+	client, srv := net.Pipe()
+	tests := [][2][]byte{
+		{
+			[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\n"),
+			[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\r\n\r\n"),
+		},
+		{
+			[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\nFoo: Bar\r\n"),
+			[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\nFoo: Bar\r\n"),
+		},
+		{
+			[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\ntest something!"),
+			[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\r\n\r\ntest something!"),
+		},
+		{
+			[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\ntest something! " + strings.Repeat("test", 15000)),
+			[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\r\n\r\ntest something! " + strings.Repeat("test", 15000)),
+		},
+		{
+			[]byte("GET /foo\nFoo: Bar\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\n"),
+			[]byte("GET /foo\nFoo: Bar\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\n"),
+		},
+	}
+	l := MalformedHostHeaderOverrideConn{client, true}
+	read := make([]byte, 4096)
+
+	for _, pair := range tests {
+		go func() {
+			srv.Write(pair[0])
+		}()
+		n, err := l.Read(read)
+		if err != nil && err != io.EOF {
+			t.Fatalf("read: %d - %d, err: %v\n%s", n, len(pair[0]), err, string(read[:n]))
+		}
+		if !bytes.Equal(read[:n], pair[1][:n]) {
+			t.Fatalf("\n%s\n%s\n", read[:n], pair[1][:n])
+		}
+		l.first = true
+		// clean out the slice
+		read = read[:0]
+	}
+	srv.Close()
+	l.Close()
+}
+
+func BenchmarkWithHack(b *testing.B) {
+	client, srv := net.Pipe()
+	done := make(chan struct{})
+	req := []byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\n")
+	read := make([]byte, 4096)
+	b.SetBytes(int64(len(req) * 30))
+
+	l := MalformedHostHeaderOverrideConn{client, true}
+	go func() {
+		for {
+			if _, err := srv.Write(req); err != nil {
+				srv.Close()
+				break
+			}
+			l.first = true // make sure each subsequent run uses the hack parsing
+		}
+		close(done)
+	}()
+
+	for i := 0; i < b.N; i++ {
+		for i := 0; i < 30; i++ {
+			if n, err := l.Read(read); err != nil && err != io.EOF {
+				b.Fatalf("read: %d - %d, err: %v\n%s", n, len(req), err, string(read[:n]))
+			}
+		}
+	}
+	l.Close()
+	<-done
+}
+
+func BenchmarkNoHack(b *testing.B) {
+	client, srv := net.Pipe()
+	done := make(chan struct{})
+	req := []byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\n")
+	read := make([]byte, 4096)
+	b.SetBytes(int64(len(req) * 30))
+
+	go func() {
+		for {
+			if _, err := srv.Write(req); err != nil {
+				srv.Close()
+				break
+			}
+		}
+		close(done)
+	}()
+
+	for i := 0; i < b.N; i++ {
+		for i := 0; i < 30; i++ {
+			if _, err := client.Read(read); err != nil && err != io.EOF {
+				b.Fatal(err)
+			}
+		}
+	}
+	client.Close()
+	<-done
+}

+ 7 - 0
docs/breaking_changes.md

@@ -22,6 +22,13 @@ Unfortunately, Docker is a fast moving project, and newly introduced features
 may sometime introduce breaking changes and/or incompatibilities. This page
 may sometime introduce breaking changes and/or incompatibilities. This page
 documents these by Engine version.
 documents these by Engine version.
 
 
+# Engine 1.12
+
+Docker clients <= 1.9.2 used an invalid Host header when making request to the
+daemon. Docker 1.12 is built using golang 1.6 which is now checking the validity
+of the Host header and as such clients <= 1.9.2 can't talk anymore to the daemon. 
+[An environment variable was added to overcome this issue.](reference/commandline/dockerd.md#miscellaneous-options)
+
 # Engine 1.10
 # Engine 1.10
 
 
 There were two breaking changes in the 1.10 release.
 There were two breaking changes in the 1.10 release.

+ 13 - 0
docs/reference/commandline/dockerd.md

@@ -849,6 +849,19 @@ set like this:
     export DOCKER_TMPDIR=/mnt/disk2/tmp
     export DOCKER_TMPDIR=/mnt/disk2/tmp
     /usr/local/bin/dockerd -D -g /var/lib/docker -H unix:// > /var/lib/docker-machine/docker.log 2>&1
     /usr/local/bin/dockerd -D -g /var/lib/docker -H unix:// > /var/lib/docker-machine/docker.log 2>&1
 
 
+Docker clients <= 1.9.2 used an invalid Host header when making request to the
+daemon. Docker 1.12 is built using golang 1.6 which is now checking the validity
+of the Host header and as such clients <= 1.9.2 can't talk anymore to the daemon.
+Docker supports overcoming this issue via a Docker daemon
+environment variable. In case you are seeing this error when contacting the
+daemon:
+
+    Error response from daemon: 400 Bad Request: malformed Host header
+
+The `DOCKER_HTTP_HOST_COMPAT` can be set like this:
+
+    DOCKER_HTTP_HOST_COMPAT=1 /usr/local/bin/dockerd ...
+
 
 
 ## Default cgroup parent
 ## Default cgroup parent