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>
This commit is contained in:
Antonio Murdaca 2016-04-12 17:21:20 +02:00
parent 376c15bbaa
commit 3d6f5984f5
7 changed files with 274 additions and 2 deletions

View File

@ -228,10 +228,11 @@ 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 {
@ -239,7 +240,7 @@ func (cli *DaemonCli) start() (err error) {
}
}
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 {

View File

@ -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"
@ -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
}

View File

@ -2,6 +2,7 @@ package main
import (
"fmt"
"net"
"os"
"syscall"
@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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.

View File

@ -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