Summary
Upgrading a HTTP connection to TCP no long work for versions greater than v2.3.
This is NOT about upgrading a connection in a TCP frontend using switch-mode http
, nor is it about WebSocket. It’s about the Connection: Upgrade
and Upgrade: tcp
HTTP headers.
With HAProxy 2.2: client and server could upgrade the connection and converse bidirectionnaly using a proprietary binary format on the connection.
With HAProxy 3.0: proxy returns HTTP 200 (instead of expected 101) and connection upgrade fails.
Looking at traces, the problem seems to originate from the parsing of the HTTP request. The Upgrade
header seems ignored and is not passed to the backend, so no upgrade takes place.
Looking at the code, the h1_parse_upgrade_header seems to ignore the Upgrade
header unless it’s set to websocket
.
Do you think this is the intended behavior? Or is it a shortcoming?
Context
The docker-socket-proxy project uses HAproxy to filter requests to the Docker daemon using ACLs (like here). It worked well with HAproxy v2.2 and broke when I updated HAproxy to v3.0.
The proxied protocol is the Docker Engine API. It’s mostly HTTP REST but for some endpoints, the connection is “hijacked”: upgraded to TCP, then used for streaming a proprietary binary format in both directions. Hijacking is detailed here in Docker doc.
An example of problematic call is the POST to /v1.46/exec/{id}/start
.
Observations
Note: output of various commands were redacted for brevity
When working (HAproxy 2.2)
Proxy logs
dockerhttp dockerhttp/dockersocket 0/0/0/0/29 101 316 - - ---- 2/2/0/0/0 0/0 "POST /v1.46/exec/a84c8fa7af83d2d6fb8438e107f82fc569284db962ba8f57f0794686c5cb7068/start HTTP/1.1"
Wireshark capture, between client and proxy
Hypertext Transfer Protocol
POST /v1.46/exec/a84c8fa7af83d2d6fb8438e107f82fc569284db962ba8f57f0794686c5cb7068/start HTTP/1.1\r\n
Connection: Upgrade\r\n
Content-Type: application/json\r\n
Upgrade: tcp\r\n
Hypertext Transfer Protocol
HTTP/1.1 101 UPGRADED\r\n
content-type: application/vnd.docker.multiplexed-stream\r\n
connection: Upgrade\r\n
upgrade: tcp\r\n
Transmission Control Protocol
# A paquet with binary data that respect the docker.multiplexed-stream content-type
Traces
[04|h1|0|mux_h1.c:1534] H1 request fully rcvd : [F] [MSG_DONE, MSG_RPBEFORE] - "POST /v1.46/exec/a84c8fa7af83d2d6fb8438e107f82fc569284db962ba8f57f0794686c5cb7068/start HTTP/1.1" - h1c=0x7551d2ecf090(0x00000000) h1s=0x7551d2ecf7b0(0x00000410) ibuf=280@0x7551d2a02f00+48/16384 obuf=0@0+0/0
htx=0x7551d2a07810(size=16336,data=276,used=10,wrap=NO,flags=0x00000010,extra=0,first=0,head=0,tail=9,tail_addr=276,head_addr=0,end_addr=0)
[0] type=HTX_BLK_REQ_SL - size=118 - addr=0 POST /v1.46/exec/a84c8fa7af83d2d6fb8438e107f82fc569284db962ba8f57f0794686c5cb7068/start HTTP/1.1
[1] type=HTX_BLK_HDR - size=18 - addr=118 host: 127.0.0.1:2375
[2] type=HTX_BLK_HDR - size=38 - addr=136 user-agent: Docker-Client/27.1.1 (linux)
[3] type=HTX_BLK_HDR - size=16 - addr=174 content-length: 29
[4] type=HTX_BLK_HDR - size=17 - addr=190 connection: Upgrade
[5] type=HTX_BLK_HDR - size=28 - addr=207 content-type: application/json
[6] type=HTX_BLK_HDR - size=10 - addr=235 upgrade: tcp
[7] type=HTX_BLK_EOH - size=1 - addr=245 <empty>
[8] type=HTX_BLK_DATA - size=29 - addr=246
[9] type=HTX_BLK_EOM - size=1 - addr=275 <empty>
# snip
[04|h1|0|mux_h1.c:1739] sending request headers : [B] [MSG_RQBEFORE, MSG_RPBEFORE] - "POST /v1.46/exec/a84c8fa7af83d2d6fb8438e107f82fc569284db962ba8f57f0794686c5cb7068/start HTTP/1.1" - h1c=0x7551d2ecf680(0x00000000) h1s=0x7551d2ecf9a0(0x00000010) ibuf=0@0+0/0 obuf=0@0x7551d2a10a50+0/16384
htx=0x7551d2a07810(size=16336,data=276,used=10,wrap=NO,flags=0x00000010,extra=0,first=0,head=0,tail=9,tail_addr=276,head_addr=0,end_addr=0)
[0] type=HTX_BLK_REQ_SL - size=118 - addr=0 POST /v1.46/exec/a84c8fa7af83d2d6fb8438e107f82fc569284db962ba8f57f0794686c5cb7068/start HTTP/1.1
[1] type=HTX_BLK_HDR - size=18 - addr=118 host: 127.0.0.1:2375
[2] type=HTX_BLK_HDR - size=38 - addr=136 user-agent: Docker-Client/27.1.1 (linux)
[3] type=HTX_BLK_HDR - size=16 - addr=174 content-length: 29
[4] type=HTX_BLK_HDR - size=17 - addr=190 connection: Upgrade
[5] type=HTX_BLK_HDR - size=28 - addr=207 content-type: application/json
[6] type=HTX_BLK_HDR - size=10 - addr=235 upgrade: tcp
[7] type=HTX_BLK_EOH - size=1 - addr=245 <empty>
[8] type=HTX_BLK_DATA - size=29 - addr=246
[9] type=HTX_BLK_EOM - size=1 - addr=275 <empty>
# snip
[04|h1|0|mux_h1.c:1501] rcvd H1 response headers : [B] [MSG_TUNNEL, MSG_TUNNEL] - VAL=210 - "HTTP/1.1 101 UPGRADED" - h1c=0x7551d2ecf680(0x00000000) h1s=0x7551d2ecf9a0(0x00004020) ibuf=210@0x7551d2a02f00+0/16384 obuf=0@0+0/0
htx=0x7551d2a07810(size=16336,data=201,used=9,wrap=NO,flags=0x00000000,extra=18446744073709551615,first=0,head=0,tail=8,tail_addr=201,head_addr=0,end_addr=0)
[0] type=HTX_BLK_RES_SL - size=43 - addr=0 HTTP/1.1 101 UPGRADED
[1] type=HTX_BLK_HDR - size=53 - addr=43 content-type: application/vnd.docker.multiplexed-stream
[2] type=HTX_BLK_HDR - size=17 - addr=96 connection: Upgrade
[3] type=HTX_BLK_HDR - size=10 - addr=113 upgrade: tcp
[4] type=HTX_BLK_HDR - size=15 - addr=123 api-version: 1.46
[5] type=HTX_BLK_HDR - size=24 - addr=138 docker-experimental: false
[6] type=HTX_BLK_HDR - size=11 - addr=162 ostype: linux
[7] type=HTX_BLK_HDR - size=27 - addr=173 server: Docker/27.1.1 (linux)
[8] type=HTX_BLK_EOH - size=1 - addr=200 <empty>
When broken (HAproxy 3.0)
Proxy logs
dockerhttp dockerhttp/dockersocket 0/0/0/0/20 200 156 - - ---- 1/1/0/0/0 0/0 "POST /v1.46/exec/239ed0d2f465f6fca17c9d8a68e72b8b41e396d34516e910d810bbc8fbc2b160/start HTTP/1.1"
Wireshark capture, between client and proxy
Hypertext Transfer Protocol
POST /v1.46/exec/239ed0d2f465f6fca17c9d8a68e72b8b41e396d34516e910d810bbc8fbc2b160/start HTTP/1.1\r\n
Connection: Upgrade\r\n
Content-Type: application/json\r\n
Upgrade: tcp\r\n
Hypertext Transfer Protocol
HTTP/1.1 200 OK\r\n
content-type: application/vnd.docker.raw-stream\r\n
connection: close\r\n
# No TCP packet with binary data
Traces
[02|h1|1|mux_h1.c:2087] H1 request fully rcvd : [F,EMB] [MSG_DONE, MSG_RPBEFORE] - req=(.fl=0x00001511 .curr_len=0 .body_len=29) res=(.fl=0x00001404 .curr_len=0 .body_len=0) - "POST /v1.46/exec/239ed0d2f465f6fca17c9d8a68e72b8b41e396d34516e910d810bbc8fbc2b160/start HTTP/1.1" - h1c=0x7341fce5e1f0(0x00000000) conn=0x7341fce5e100(0x80000300) h1s=0x7341fce5e2e0(0x00000010) sd=0x7341fd80ec90(0x00500021) ibuf=280@0x7341fce8a530+48/16384 obuf=0@0+0/0
htx=0x7341fce7ca00(size=16336,data=261,used=8,wrap=NO,flags=0x00000010,extra=0,first=0,head=0,tail=7,tail_addr=261,head_addr=0,end_addr=0)
[0] type=HTX_BLK_REQ_SL - size=114 - addr=0 POST /v1.46/exec/239ed0d2f465f6fca17c9d8a68e72b8b41e396d34516e910d810bbc8fbc2b160/start HTTP/1.1
[1] type=HTX_BLK_HDR - size=18 - addr=114 host: 127.0.0.1:2375
[2] type=HTX_BLK_HDR - size=38 - addr=132 user-agent: Docker-Client/27.1.1 (linux)
[3] type=HTX_BLK_HDR - size=16 - addr=170 content-length: 29
[4] type=HTX_BLK_HDR - size=17 - addr=186 connection: Upgrade
[5] type=HTX_BLK_HDR - size=28 - addr=203 content-type: application/json
[6] type=HTX_BLK_EOH - size=1 - addr=231 <empty>
[7] type=HTX_BLK_DATA - size=29 - addr=232
# snip
[02|h1|1|mux_h1.c:2291] sending request headers : [B,RUN] [MSG_RQBEFORE, MSG_RPBEFORE] - req=(.fl=0x00001400 .curr_len=0 .body_len=0) res=(.fl=0x00001404 .curr_len=0 .body_len=0) - "POST /v1.46/exec/239ed0d2f465f6fca17c9d8a68e72b8b41e396d34516e910d810bbc8fbc2b160/start HTTP/1.1" - h1c=0x7341fce5e4c0(0x80000000) conn=0x7341fce5e3d0(0x00000300) h1s=0x7341fce5e5b0(0x00100010) sd=0x7341fd80ec10(0x40400001) sc=0x7341fd515f00(0x00001c31) ibuf=0@0+0/0 obuf=0@0x7341fce85c20+0/16384
htx=0x7341fce7ca00(size=16336,data=261,used=8,wrap=NO,flags=0x00000010,extra=0,first=0,head=0,tail=7,tail_addr=261,head_addr=0,end_addr=0)
[0] type=HTX_BLK_REQ_SL - size=114 - addr=0 POST /v1.46/exec/239ed0d2f465f6fca17c9d8a68e72b8b41e396d34516e910d810bbc8fbc2b160/start HTTP/1.1
[1] type=HTX_BLK_HDR - size=18 - addr=114 host: 127.0.0.1:2375
[2] type=HTX_BLK_HDR - size=38 - addr=132 user-agent: Docker-Client/27.1.1 (linux)
[3] type=HTX_BLK_HDR - size=16 - addr=170 content-length: 29
[4] type=HTX_BLK_HDR - size=17 - addr=186 connection: Upgrade
[5] type=HTX_BLK_HDR - size=28 - addr=203 content-type: application/json
[6] type=HTX_BLK_EOH - size=1 - addr=231 <empty>
[7] type=HTX_BLK_DATA - size=29 - addr=232
# snip
[02|h1|1|mux_h1.c:2376] sending response headers : [F,RUN] [MSG_DONE, MSG_RPBEFORE] - req=(.fl=0x00001511 .curr_len=0 .body_len=29) res=(.fl=0x00001404 .curr_len=0 .body_len=0) - "HTTP/1.1 200 OK" - h1c=0x7341fce5e1f0(0x00000000) conn=0x7341fce5e100(0x80000300) h1s=0x7341fce5e2e0(0x00100010) sd=0x7341fd80ec90(0x50404001) sc=0x7341fd515ea0(0x00001422) ibuf=0@0+0/0 obuf=0@0x7341fce8a530+0/16384
htx=0x7341fce7ca00(size=16336,data=156,used=7,wrap=NO,flags=0x00000000,extra=18446744073709551615,first=0,head=0,tail=6,tail_addr=156,head_addr=0,end_addr=0)
[0] type=HTX_BLK_RES_SL - size=33 - addr=0 HTTP/1.1 200 OK
[1] type=HTX_BLK_HDR - size=45 - addr=33 content-type: application/vnd.docker.raw-stream
[2] type=HTX_BLK_HDR - size=15 - addr=78 api-version: 1.46
[3] type=HTX_BLK_HDR - size=24 - addr=93 docker-experimental: false
[4] type=HTX_BLK_HDR - size=11 - addr=117 ostype: linux
[5] type=HTX_BLK_HDR - size=27 - addr=128 server: Docker/27.1.1 (linux)
[6] type=HTX_BLK_EOH - size=1 - addr=155 <empty>
How to reproduce
haproxy.cfg:
global
log stdout len 4096 format raw daemon debug
stats socket ipv4@:10000 level admin
maxconn 100
defaults
log global
timeout connect 10s
timeout client 10m
timeout server 10m
errorfile 400 /usr/local/etc/haproxy/errors/400.http
errorfile 403 /usr/local/etc/haproxy/errors/403.http
errorfile 408 /usr/local/etc/haproxy/errors/408.http
errorfile 500 /usr/local/etc/haproxy/errors/500.http
errorfile 502 /usr/local/etc/haproxy/errors/502.http
errorfile 503 /usr/local/etc/haproxy/errors/503.http
errorfile 504 /usr/local/etc/haproxy/errors/504.http
frontend dockerhttp
bind :2375
mode http
option httplog
default_backend dockerhttp
backend dockerhttp
mode http
server dockersocket /var/run/docker.sock
As an user that has access to /var/run/docker.sock
(upgrade version as needed and make sure Docker daemon is up)
docker run --rm --name haproxy \
--user root --privileged \
-v ./haproxy.cfg:/haproxy.cfg \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 127.0.0.1:2375:2375 \
-p 127.0.0.1:10000:10000 \
haproxy:2.2-alpine haproxy -f /haproxy.cfg
In another terminal:
export DOCKER_HOST=tcp://127.0.0.1:2375
docker exec haproxy ls