Browse code

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 authored on 2016/04/13 00:21:20
Showing 7 changed files
... ...
@@ -228,10 +228,11 @@ func (cli *DaemonCli) start() (err error) {
228 228
 		if proto == "tcp" && (serverConfig.TLSConfig == nil || serverConfig.TLSConfig.ClientAuth != tls.RequireAndVerifyClientCert) {
229 229
 			logrus.Warn("[!] DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING [!]")
230 230
 		}
231
-		l, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
231
+		ls, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
232 232
 		if err != nil {
233 233
 			return err
234 234
 		}
235
+		ls = wrapListeners(proto, ls)
235 236
 		// If we're binding to a TCP port, make sure that a container doesn't try to use it.
236 237
 		if proto == "tcp" {
237 238
 			if err := allocateDaemonPort(addr); err != nil {
... ...
@@ -239,7 +240,7 @@ func (cli *DaemonCli) start() (err error) {
239 239
 			}
240 240
 		}
241 241
 		logrus.Debugf("Listener created for HTTP on %s (%s)", protoAddrParts[0], protoAddrParts[1])
242
-		api.Accept(protoAddrParts[1], l...)
242
+		api.Accept(protoAddrParts[1], ls...)
243 243
 	}
244 244
 
245 245
 	if err := migrateKey(); err != nil {
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	"strconv"
12 12
 	"syscall"
13 13
 
14
+	"github.com/docker/docker/cmd/dockerd/hack"
14 15
 	"github.com/docker/docker/daemon"
15 16
 	"github.com/docker/docker/libcontainerd"
16 17
 	"github.com/docker/docker/pkg/system"
... ...
@@ -111,3 +112,17 @@ func allocateDaemonPort(addr string) error {
111 111
 // notifyShutdown is called after the daemon shuts down but before the process exits.
112 112
 func notifyShutdown(err error) {
113 113
 }
114
+
115
+func wrapListeners(proto string, ls []net.Listener) []net.Listener {
116
+	if os.Getenv("DOCKER_HTTP_HOST_COMPAT") != "" {
117
+		switch proto {
118
+		case "unix":
119
+			ls[0] = &hack.MalformedHostHeaderOverride{ls[0]}
120
+		case "fd":
121
+			for i := range ls {
122
+				ls[i] = &hack.MalformedHostHeaderOverride{ls[i]}
123
+			}
124
+		}
125
+	}
126
+	return ls
127
+}
... ...
@@ -2,6 +2,7 @@ package main
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"net"
5 6
 	"os"
6 7
 	"syscall"
7 8
 
... ...
@@ -75,3 +76,7 @@ func (cli *DaemonCli) getLibcontainerdRoot() string {
75 75
 func allocateDaemonPort(addr string) error {
76 76
 	return nil
77 77
 }
78
+
79
+func wrapListeners(proto string, ls []net.Listener) []net.Listener {
80
+	return ls
81
+}
78 82
new file mode 100644
... ...
@@ -0,0 +1,116 @@
0
+// +build !windows
1
+
2
+package hack
3
+
4
+import "net"
5
+
6
+// MalformedHostHeaderOverride is a wrapper to be able
7
+// to overcome the 400 Bad request coming from old docker
8
+// clients that send an invalid Host header.
9
+type MalformedHostHeaderOverride struct {
10
+	net.Listener
11
+}
12
+
13
+// MalformedHostHeaderOverrideConn wraps the underlying unix
14
+// connection and keeps track of the first read from http.Server
15
+// which just reads the headers.
16
+type MalformedHostHeaderOverrideConn struct {
17
+	net.Conn
18
+	first bool
19
+}
20
+
21
+var closeConnHeader = []byte("\r\nConnection: close\r")
22
+
23
+// Read reads the first *read* request from http.Server to inspect
24
+// the Host header. If the Host starts with / then we're talking to
25
+// an old docker client which send an invalid Host header. To not
26
+// error out in http.Server we rewrite the first bytes of the request
27
+// to sanitize the Host header itself.
28
+// In case we're not dealing with old docker clients the data is just passed
29
+// to the server w/o modification.
30
+func (l *MalformedHostHeaderOverrideConn) Read(b []byte) (n int, err error) {
31
+	// http.Server uses a 4k buffer
32
+	if l.first && len(b) == 4096 {
33
+		// This keeps track of the first read from http.Server which just reads
34
+		// the headers
35
+		l.first = false
36
+		// The first read of the connection by http.Server is done limited to
37
+		// DefaultMaxHeaderBytes (usually 1 << 20) + 4096.
38
+		// Here we do the first read which gets us all the http headers to
39
+		// be inspected and modified below.
40
+		c, err := l.Conn.Read(b)
41
+		if err != nil {
42
+			return c, err
43
+		}
44
+
45
+		var (
46
+			start, end    int
47
+			firstLineFeed = -1
48
+			buf           []byte
49
+		)
50
+		for i, bb := range b[:c] {
51
+			if bb == '\n' && firstLineFeed == -1 {
52
+				firstLineFeed = i
53
+			}
54
+			if bb != '\n' {
55
+				continue
56
+			}
57
+			if b[i+1] != 'H' {
58
+				continue
59
+			}
60
+			if b[i+2] != 'o' {
61
+				continue
62
+			}
63
+			if b[i+3] != 's' {
64
+				continue
65
+			}
66
+			if b[i+4] != 't' {
67
+				continue
68
+			}
69
+			if b[i+5] != ':' {
70
+				continue
71
+			}
72
+			if b[i+6] != ' ' {
73
+				continue
74
+			}
75
+			if b[i+7] != '/' {
76
+				continue
77
+			}
78
+			// ensure clients other than the docker clients do not get this hack
79
+			if i != firstLineFeed {
80
+				return c, nil
81
+			}
82
+			start = i + 7
83
+			// now find where the value ends
84
+			for ii, bbb := range b[start:c] {
85
+				if bbb == '\n' {
86
+					end = start + ii
87
+					break
88
+				}
89
+			}
90
+			buf = make([]byte, 0, c+len(closeConnHeader)-(end-start))
91
+			// strip the value of the host header and
92
+			// inject `Connection: close` to ensure we don't reuse this connection
93
+			buf = append(buf, b[:start]...)
94
+			buf = append(buf, closeConnHeader...)
95
+			buf = append(buf, b[end:c]...)
96
+			copy(b, buf)
97
+			break
98
+		}
99
+		if len(buf) == 0 {
100
+			return c, nil
101
+		}
102
+		return len(buf), nil
103
+	}
104
+	return l.Conn.Read(b)
105
+}
106
+
107
+// Accept makes the listener accepts connections and wraps the connection
108
+// in a MalformedHostHeaderOverrideConn initilizing first to true.
109
+func (l *MalformedHostHeaderOverride) Accept() (net.Conn, error) {
110
+	c, err := l.Listener.Accept()
111
+	if err != nil {
112
+		return c, err
113
+	}
114
+	return &MalformedHostHeaderOverrideConn{c, true}, nil
115
+}
0 116
new file mode 100644
... ...
@@ -0,0 +1,115 @@
0
+// +build !windows
1
+
2
+package hack
3
+
4
+import (
5
+	"bytes"
6
+	"io"
7
+	"net"
8
+	"strings"
9
+	"testing"
10
+)
11
+
12
+func TestHeaderOverrideHack(t *testing.T) {
13
+	client, srv := net.Pipe()
14
+	tests := [][2][]byte{
15
+		{
16
+			[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\n"),
17
+			[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\r\n\r\n"),
18
+		},
19
+		{
20
+			[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\nFoo: Bar\r\n"),
21
+			[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\nFoo: Bar\r\n"),
22
+		},
23
+		{
24
+			[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\ntest something!"),
25
+			[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\r\n\r\ntest something!"),
26
+		},
27
+		{
28
+			[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\ntest something! " + strings.Repeat("test", 15000)),
29
+			[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\r\n\r\ntest something! " + strings.Repeat("test", 15000)),
30
+		},
31
+		{
32
+			[]byte("GET /foo\nFoo: Bar\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\n"),
33
+			[]byte("GET /foo\nFoo: Bar\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\n"),
34
+		},
35
+	}
36
+	l := MalformedHostHeaderOverrideConn{client, true}
37
+	read := make([]byte, 4096)
38
+
39
+	for _, pair := range tests {
40
+		go func() {
41
+			srv.Write(pair[0])
42
+		}()
43
+		n, err := l.Read(read)
44
+		if err != nil && err != io.EOF {
45
+			t.Fatalf("read: %d - %d, err: %v\n%s", n, len(pair[0]), err, string(read[:n]))
46
+		}
47
+		if !bytes.Equal(read[:n], pair[1][:n]) {
48
+			t.Fatalf("\n%s\n%s\n", read[:n], pair[1][:n])
49
+		}
50
+		l.first = true
51
+		// clean out the slice
52
+		read = read[:0]
53
+	}
54
+	srv.Close()
55
+	l.Close()
56
+}
57
+
58
+func BenchmarkWithHack(b *testing.B) {
59
+	client, srv := net.Pipe()
60
+	done := make(chan struct{})
61
+	req := []byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\n")
62
+	read := make([]byte, 4096)
63
+	b.SetBytes(int64(len(req) * 30))
64
+
65
+	l := MalformedHostHeaderOverrideConn{client, true}
66
+	go func() {
67
+		for {
68
+			if _, err := srv.Write(req); err != nil {
69
+				srv.Close()
70
+				break
71
+			}
72
+			l.first = true // make sure each subsequent run uses the hack parsing
73
+		}
74
+		close(done)
75
+	}()
76
+
77
+	for i := 0; i < b.N; i++ {
78
+		for i := 0; i < 30; i++ {
79
+			if n, err := l.Read(read); err != nil && err != io.EOF {
80
+				b.Fatalf("read: %d - %d, err: %v\n%s", n, len(req), err, string(read[:n]))
81
+			}
82
+		}
83
+	}
84
+	l.Close()
85
+	<-done
86
+}
87
+
88
+func BenchmarkNoHack(b *testing.B) {
89
+	client, srv := net.Pipe()
90
+	done := make(chan struct{})
91
+	req := []byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\n")
92
+	read := make([]byte, 4096)
93
+	b.SetBytes(int64(len(req) * 30))
94
+
95
+	go func() {
96
+		for {
97
+			if _, err := srv.Write(req); err != nil {
98
+				srv.Close()
99
+				break
100
+			}
101
+		}
102
+		close(done)
103
+	}()
104
+
105
+	for i := 0; i < b.N; i++ {
106
+		for i := 0; i < 30; i++ {
107
+			if _, err := client.Read(read); err != nil && err != io.EOF {
108
+				b.Fatal(err)
109
+			}
110
+		}
111
+	}
112
+	client.Close()
113
+	<-done
114
+}
... ...
@@ -22,6 +22,13 @@ Unfortunately, Docker is a fast moving project, and newly introduced features
22 22
 may sometime introduce breaking changes and/or incompatibilities. This page
23 23
 documents these by Engine version.
24 24
 
25
+# Engine 1.12
26
+
27
+Docker clients <= 1.9.2 used an invalid Host header when making request to the
28
+daemon. Docker 1.12 is built using golang 1.6 which is now checking the validity
29
+of the Host header and as such clients <= 1.9.2 can't talk anymore to the daemon. 
30
+[An environment variable was added to overcome this issue.](reference/commandline/dockerd.md#miscellaneous-options)
31
+
25 32
 # Engine 1.10
26 33
 
27 34
 There were two breaking changes in the 1.10 release.
... ...
@@ -849,6 +849,19 @@ set like this:
849 849
     export DOCKER_TMPDIR=/mnt/disk2/tmp
850 850
     /usr/local/bin/dockerd -D -g /var/lib/docker -H unix:// > /var/lib/docker-machine/docker.log 2>&1
851 851
 
852
+Docker clients <= 1.9.2 used an invalid Host header when making request to the
853
+daemon. Docker 1.12 is built using golang 1.6 which is now checking the validity
854
+of the Host header and as such clients <= 1.9.2 can't talk anymore to the daemon.
855
+Docker supports overcoming this issue via a Docker daemon
856
+environment variable. In case you are seeing this error when contacting the
857
+daemon:
858
+
859
+    Error response from daemon: 400 Bad Request: malformed Host header
860
+
861
+The `DOCKER_HTTP_HOST_COMPAT` can be set like this:
862
+
863
+    DOCKER_HTTP_HOST_COMPAT=1 /usr/local/bin/dockerd ...
864
+
852 865
 
853 866
 ## Default cgroup parent
854 867