Skip to content

Commit 3d6f598

Browse files
committed
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>
1 parent 376c15b commit 3d6f598

7 files changed

Lines changed: 274 additions & 2 deletions

File tree

cmd/dockerd/daemon.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,18 +228,19 @@ func (cli *DaemonCli) start() (err error) {
228228
if proto == "tcp" && (serverConfig.TLSConfig == nil || serverConfig.TLSConfig.ClientAuth != tls.RequireAndVerifyClientCert) {
229229
logrus.Warn("[!] DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING [!]")
230230
}
231-
l, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
231+
ls, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
232232
if err != nil {
233233
return err
234234
}
235+
ls = wrapListeners(proto, ls)
235236
// If we're binding to a TCP port, make sure that a container doesn't try to use it.
236237
if proto == "tcp" {
237238
if err := allocateDaemonPort(addr); err != nil {
238239
return err
239240
}
240241
}
241242
logrus.Debugf("Listener created for HTTP on %s (%s)", protoAddrParts[0], protoAddrParts[1])
242-
api.Accept(protoAddrParts[1], l...)
243+
api.Accept(protoAddrParts[1], ls...)
243244
}
244245

245246
if err := migrateKey(); err != nil {

cmd/dockerd/daemon_unix.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strconv"
1212
"syscall"
1313

14+
"github.com/docker/docker/cmd/dockerd/hack"
1415
"github.com/docker/docker/daemon"
1516
"github.com/docker/docker/libcontainerd"
1617
"github.com/docker/docker/pkg/system"
@@ -111,3 +112,17 @@ func allocateDaemonPort(addr string) error {
111112
// notifyShutdown is called after the daemon shuts down but before the process exits.
112113
func notifyShutdown(err error) {
113114
}
115+
116+
func wrapListeners(proto string, ls []net.Listener) []net.Listener {
117+
if os.Getenv("DOCKER_HTTP_HOST_COMPAT") != "" {
118+
switch proto {
119+
case "unix":
120+
ls[0] = &hack.MalformedHostHeaderOverride{ls[0]}
121+
case "fd":
122+
for i := range ls {
123+
ls[i] = &hack.MalformedHostHeaderOverride{ls[i]}
124+
}
125+
}
126+
}
127+
return ls
128+
}

cmd/dockerd/daemon_windows.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"fmt"
5+
"net"
56
"os"
67
"syscall"
78

@@ -75,3 +76,7 @@ func (cli *DaemonCli) getLibcontainerdRoot() string {
7576
func allocateDaemonPort(addr string) error {
7677
return nil
7778
}
79+
80+
func wrapListeners(proto string, ls []net.Listener) []net.Listener {
81+
return ls
82+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// +build !windows
2+
3+
package hack
4+
5+
import "net"
6+
7+
// MalformedHostHeaderOverride is a wrapper to be able
8+
// to overcome the 400 Bad request coming from old docker
9+
// clients that send an invalid Host header.
10+
type MalformedHostHeaderOverride struct {
11+
net.Listener
12+
}
13+
14+
// MalformedHostHeaderOverrideConn wraps the underlying unix
15+
// connection and keeps track of the first read from http.Server
16+
// which just reads the headers.
17+
type MalformedHostHeaderOverrideConn struct {
18+
net.Conn
19+
first bool
20+
}
21+
22+
var closeConnHeader = []byte("\r\nConnection: close\r")
23+
24+
// Read reads the first *read* request from http.Server to inspect
25+
// the Host header. If the Host starts with / then we're talking to
26+
// an old docker client which send an invalid Host header. To not
27+
// error out in http.Server we rewrite the first bytes of the request
28+
// to sanitize the Host header itself.
29+
// In case we're not dealing with old docker clients the data is just passed
30+
// to the server w/o modification.
31+
func (l *MalformedHostHeaderOverrideConn) Read(b []byte) (n int, err error) {
32+
// http.Server uses a 4k buffer
33+
if l.first && len(b) == 4096 {
34+
// This keeps track of the first read from http.Server which just reads
35+
// the headers
36+
l.first = false
37+
// The first read of the connection by http.Server is done limited to
38+
// DefaultMaxHeaderBytes (usually 1 << 20) + 4096.
39+
// Here we do the first read which gets us all the http headers to
40+
// be inspected and modified below.
41+
c, err := l.Conn.Read(b)
42+
if err != nil {
43+
return c, err
44+
}
45+
46+
var (
47+
start, end int
48+
firstLineFeed = -1
49+
buf []byte
50+
)
51+
for i, bb := range b[:c] {
52+
if bb == '\n' && firstLineFeed == -1 {
53+
firstLineFeed = i
54+
}
55+
if bb != '\n' {
56+
continue
57+
}
58+
if b[i+1] != 'H' {
59+
continue
60+
}
61+
if b[i+2] != 'o' {
62+
continue
63+
}
64+
if b[i+3] != 's' {
65+
continue
66+
}
67+
if b[i+4] != 't' {
68+
continue
69+
}
70+
if b[i+5] != ':' {
71+
continue
72+
}
73+
if b[i+6] != ' ' {
74+
continue
75+
}
76+
if b[i+7] != '/' {
77+
continue
78+
}
79+
// ensure clients other than the docker clients do not get this hack
80+
if i != firstLineFeed {
81+
return c, nil
82+
}
83+
start = i + 7
84+
// now find where the value ends
85+
for ii, bbb := range b[start:c] {
86+
if bbb == '\n' {
87+
end = start + ii
88+
break
89+
}
90+
}
91+
buf = make([]byte, 0, c+len(closeConnHeader)-(end-start))
92+
// strip the value of the host header and
93+
// inject `Connection: close` to ensure we don't reuse this connection
94+
buf = append(buf, b[:start]...)
95+
buf = append(buf, closeConnHeader...)
96+
buf = append(buf, b[end:c]...)
97+
copy(b, buf)
98+
break
99+
}
100+
if len(buf) == 0 {
101+
return c, nil
102+
}
103+
return len(buf), nil
104+
}
105+
return l.Conn.Read(b)
106+
}
107+
108+
// Accept makes the listener accepts connections and wraps the connection
109+
// in a MalformedHostHeaderOverrideConn initilizing first to true.
110+
func (l *MalformedHostHeaderOverride) Accept() (net.Conn, error) {
111+
c, err := l.Listener.Accept()
112+
if err != nil {
113+
return c, err
114+
}
115+
return &MalformedHostHeaderOverrideConn{c, true}, nil
116+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// +build !windows
2+
3+
package hack
4+
5+
import (
6+
"bytes"
7+
"io"
8+
"net"
9+
"strings"
10+
"testing"
11+
)
12+
13+
func TestHeaderOverrideHack(t *testing.T) {
14+
client, srv := net.Pipe()
15+
tests := [][2][]byte{
16+
{
17+
[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\n"),
18+
[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\r\n\r\n"),
19+
},
20+
{
21+
[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\nFoo: Bar\r\n"),
22+
[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\nFoo: Bar\r\n"),
23+
},
24+
{
25+
[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\ntest something!"),
26+
[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\r\n\r\ntest something!"),
27+
},
28+
{
29+
[]byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\ntest something! " + strings.Repeat("test", 15000)),
30+
[]byte("GET /foo\nHost: \r\nConnection: close\r\nUser-Agent: Docker\r\n\r\ntest something! " + strings.Repeat("test", 15000)),
31+
},
32+
{
33+
[]byte("GET /foo\nFoo: Bar\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\n"),
34+
[]byte("GET /foo\nFoo: Bar\nHost: /var/run/docker.sock\nUser-Agent: Docker\r\n\r\n"),
35+
},
36+
}
37+
l := MalformedHostHeaderOverrideConn{client, true}
38+
read := make([]byte, 4096)
39+
40+
for _, pair := range tests {
41+
go func() {
42+
srv.Write(pair[0])
43+
}()
44+
n, err := l.Read(read)
45+
if err != nil && err != io.EOF {
46+
t.Fatalf("read: %d - %d, err: %v\n%s", n, len(pair[0]), err, string(read[:n]))
47+
}
48+
if !bytes.Equal(read[:n], pair[1][:n]) {
49+
t.Fatalf("\n%s\n%s\n", read[:n], pair[1][:n])
50+
}
51+
l.first = true
52+
// clean out the slice
53+
read = read[:0]
54+
}
55+
srv.Close()
56+
l.Close()
57+
}
58+
59+
func BenchmarkWithHack(b *testing.B) {
60+
client, srv := net.Pipe()
61+
done := make(chan struct{})
62+
req := []byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\n")
63+
read := make([]byte, 4096)
64+
b.SetBytes(int64(len(req) * 30))
65+
66+
l := MalformedHostHeaderOverrideConn{client, true}
67+
go func() {
68+
for {
69+
if _, err := srv.Write(req); err != nil {
70+
srv.Close()
71+
break
72+
}
73+
l.first = true // make sure each subsequent run uses the hack parsing
74+
}
75+
close(done)
76+
}()
77+
78+
for i := 0; i < b.N; i++ {
79+
for i := 0; i < 30; i++ {
80+
if n, err := l.Read(read); err != nil && err != io.EOF {
81+
b.Fatalf("read: %d - %d, err: %v\n%s", n, len(req), err, string(read[:n]))
82+
}
83+
}
84+
}
85+
l.Close()
86+
<-done
87+
}
88+
89+
func BenchmarkNoHack(b *testing.B) {
90+
client, srv := net.Pipe()
91+
done := make(chan struct{})
92+
req := []byte("GET /foo\nHost: /var/run/docker.sock\nUser-Agent: Docker\n")
93+
read := make([]byte, 4096)
94+
b.SetBytes(int64(len(req) * 30))
95+
96+
go func() {
97+
for {
98+
if _, err := srv.Write(req); err != nil {
99+
srv.Close()
100+
break
101+
}
102+
}
103+
close(done)
104+
}()
105+
106+
for i := 0; i < b.N; i++ {
107+
for i := 0; i < 30; i++ {
108+
if _, err := client.Read(read); err != nil && err != io.EOF {
109+
b.Fatal(err)
110+
}
111+
}
112+
}
113+
client.Close()
114+
<-done
115+
}

docs/breaking_changes.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ Unfortunately, Docker is a fast moving project, and newly introduced features
2222
may sometime introduce breaking changes and/or incompatibilities. This page
2323
documents these by Engine version.
2424

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+
2532
# Engine 1.10
2633

2734
There were two breaking changes in the 1.10 release.

docs/reference/commandline/dockerd.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,19 @@ set like this:
849849
export DOCKER_TMPDIR=/mnt/disk2/tmp
850850
/usr/local/bin/dockerd -D -g /var/lib/docker -H unix:// > /var/lib/docker-machine/docker.log 2>&1
851851
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+
852865

853866
## Default cgroup parent
854867

0 commit comments

Comments
 (0)