Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/dockerd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,18 +228,19 @@ func (cli *DaemonCli) start() (err error) {
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 [!]")
}
l, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
ls, err := listeners.Init(proto, addr, serverConfig.SocketGroup, serverConfig.TLSConfig)
if err != nil {
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 proto == "tcp" {
if err := allocateDaemonPort(addr); err != nil {
return err
}
}
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 {
Expand Down
15 changes: 15 additions & 0 deletions cmd/dockerd/daemon_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strconv"
"syscall"

"github.com/docker/docker/cmd/dockerd/hack"
"github.com/docker/docker/daemon"
"github.com/docker/docker/libcontainerd"
"github.com/docker/docker/pkg/system"
Expand Down Expand Up @@ -111,3 +112,17 @@ func allocateDaemonPort(addr string) error {
// notifyShutdown is called after the daemon shuts down but before the process exits.
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 changes: 5 additions & 0 deletions cmd/dockerd/daemon_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"net"
"os"
"syscall"

Expand Down Expand Up @@ -75,3 +76,7 @@ func (cli *DaemonCli) getLibcontainerdRoot() string {
func allocateDaemonPort(addr string) error {
return nil
}

func wrapListeners(proto string, ls []net.Listener) []net.Listener {
return ls
}
116 changes: 116 additions & 0 deletions cmd/dockerd/hack/malformed_host_override.go
Original file line number Diff line number Diff line change
@@ -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 changes: 115 additions & 0 deletions cmd/dockerd/hack/malformed_host_override_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 7 additions & 0 deletions docs/breaking_changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

There were two breaking changes in the 1.10 release.
Expand Down
13 changes: 13 additions & 0 deletions docs/reference/commandline/dockerd.md
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,19 @@ set like this:
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

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

Expand Down