Fundamentals

HTTP CONNECT tunnelling explained

CONNECT turns an HTTP proxy into a blind TCP relay. The exact wire exchange, status codes, port policy, HTTP/2 extended CONNECT, MASQUE and failure modes.

· 11 min read · How we verify this

Key points

  • CONNECT asks a proxy to open a TCP connection to host:port and then relay raw bytes in both directions; after the 2xx response the proxy stops parsing HTTP.
  • A 2xx response to CONNECT must not carry Content-Length or Transfer-Encoding; any bytes after the blank line already belong to the tunnel.
  • The CONNECT authority and the TLS SNI are two separate client-supplied values, so a policy engine that checks only one is trivially bypassed.
  • HTTP/2 and HTTP/3 reuse CONNECT per stream, and extended CONNECT (RFC 8441, RFC 9220) adds a :protocol pseudo-header for WebSockets and CONNECT-UDP.

CONNECT is the HTTP method a client uses to ask a forward proxy to open a TCP connection to a named host:port and then relay raw octets in both directions without interpreting them (RFC 9110, section 9.3.6). It exists because TLS cannot be proxied at the HTTP layer: the client needs an end-to-end byte pipe so it can run its own handshake with the origin. Once the proxy answers with any 2xx status, the HTTP conversation on that connection is over and everything that follows is opaque payload.

The request-target of a CONNECT is in authority-form: a host and an explicit port, with no scheme and no path. CONNECT example.com:443 HTTP/1.1 is valid; CONNECT https://example.com/ HTTP/1.1 is not. A CONNECT request has no body, and the proxy is expected to hold the request open rather than treat the absence of a body as end-of-message.

The exact wire exchange#

The client writes a request, the proxy dials the target, and the proxy writes a status line plus a blank line. Nothing else.

http
CONNECT example.com:443 HTTP/1.1
Host: example.com:443
Proxy-Connection: keep-alive
User-Agent: curl/8.5.0
http
HTTP/1.1 200 Connection established

Four properties of that response matter and are frequently got wrong by home-grown proxies:

  • Any 2xx means success. Clients must not require exactly 200, and the reason phrase is free text. 200 OK, 200 Connection established and 200 Connection Established are all seen in the wild.
  • No Content-Length, no Transfer-Encoding. RFC 9110 states a server must not send either field in a 2xx response to CONNECT, because the tunnel payload is not a message body and has no framing.
  • No body. The blank line ends the response header section, and the very next octet on the socket is tunnel data.
  • Headers on the 2xx are hop-by-hop by nature. They are addressed to the client from the proxy, not from the origin. Via, Proxy-Agent and similar are the only things sensibly placed there.

A non-2xx response does behave like a normal HTTP response: it may carry a body and Content-Length, and the connection is not a tunnel. That asymmetry is the source of a lot of client bugs.

Byte level: what actually crosses the wire#

Here is a full exchange with the CR LF octets made explicit, followed by the first bytes the client sends once the tunnel is up.

text
C -> P  43 4f 4e 4e 45 43 54 20 65 78 61 6d 70 6c 65 2e   CONNECT example.
C -> P  63 6f 6d 3a 34 34 33 20 48 54 54 50 2f 31 2e 31   com:443 HTTP/1.1
C -> P  0d 0a 48 6f 73 74 3a 20 65 78 61 6d 70 6c 65 2e   ..Host: example.
C -> P  63 6f 6d 3a 34 34 33 0d 0a 0d 0a                  com:443....

P -> C  48 54 54 50 2f 31 2e 31 20 32 30 30 20 43 6f 6e   HTTP/1.1 200 Con
P -> C  6e 65 63 74 69 6f 6e 20 65 73 74 61 62 6c 69 73   nection establis
P -> C  68 65 64 0d 0a 0d 0a                              hed....

C -> P  16 03 01 02 00 01 00 01 fc 03 03 ...              TLS ClientHello
P -> S  16 03 01 02 00 01 00 01 fc 03 03 ...              (byte identical)

The last two lines are the whole point: after 0d 0a 0d 0a the proxy is a splice() loop. It copies 16 03 01 ... to the origin unchanged, and copies the ServerHello back, and it never looks at either again except to count them.

What the proxy can and cannot see after the 200#

Post-tunnel, a plain (non-intercepting) proxy observes exactly four things:

ObservableSourceNotes
Target authorityThe CONNECT line itselfClient-asserted, not verified against DNS or the certificate
TLS SNIFirst bytes of the ClientHelloCleartext today; Encrypted Client Hello removes it where both peers support it
Byte counts and timingIts own relay loopEnough for volume accounting and for traffic-analysis style classification
Connection lifetimeSocket close or idle timeoutThe only "end of transaction" signal available

It cannot see the request path, method, headers, cookies, status codes or response sizes at HTTP granularity. That is why access logs for CONNECT traffic have one line per tunnel rather than one line per request, and why URL-category filtering degrades to domain-category filtering unless the proxy performs TLS interception with a corporate root CA.

Status codes a proxy returns to CONNECT#

StatusMeaning at the proxyTypical trigger
200Tunnel establishedTCP connect to origin succeeded
403 ForbiddenPolicy denialDestination or port not permitted by ACL; Squid serves ERR_ACCESS_DENIED
407 Proxy Authentication RequiredCredentials neededSent with Proxy-Authenticate; see proxy authentication
502 Bad GatewayUpstream failureDNS failure, connection refused, upstream proxy error
503 Service UnavailableProxy-side failureSquid uses this with ERR_CONNECT_FAIL where other proxies choose 502
504 Gateway TimeoutOrigin did not answerTCP connect timed out; distinguish from a stalled tunnel, which is a socket close, not a 504

There is no way to send a status code after a tunnel is open. If the origin dies mid-session the proxy can only close the socket, which is why 502-versus-504 reasoning applies to tunnel setup only.

Why proxies restrict CONNECT to port 443#

CONNECT to an arbitrary port turns the proxy into a general purpose TCP relay, which is the definition of an open relay risk: spam via port 25, SSH pivoting via port 22, database exfiltration via 3306 or 5432, and internal port scanning that appears to originate from the proxy's trusted network position.

Squid's shipped configuration encodes the standard answer. It defines acl SSL_ports port 443, a Safe_ports list (80, 21, 443, 70, 210, 1025-65535, 280, 488, 591, 777), and then denies CONNECT to anything outside SSL_ports. The result is that a plain curl -x proxy:3128 https://host:8443/ fails with a 403 out of the box even though nothing about it is malicious. Adding 8443 (and often 563, 9443) to the SSL_ports ACL is the intended fix; widening Safe_ports is not, because that ACL governs ordinary GETs rather than tunnels.

The practical rule: allow CONNECT to the ports you have a business reason for, and never allow 1025-65535 on CONNECT, because the whole ephemeral range is where anything interesting listens.

HTTP/2, HTTP/3 and extended CONNECT#

In HTTP/2 (RFC 9113, section 8.5) CONNECT is per stream, not per connection. The client sends a HEADERS frame with :method = CONNECT and :authority set, and omits :scheme and :path. DATA frames on that stream carry tunnel octets, and END_STREAM in each direction plays the role of TCP FIN. Two consequences follow that do not exist in HTTP/1.1: many tunnels multiplex over one connection, and each tunnel is subject to HTTP/2 flow control, so a small SETTINGS_INITIAL_WINDOW_SIZE throttles bulk transfers inside the tunnel even when the network is idle. That interaction is covered further in HTTP/2 and HTTP/3 through proxies.

RFC 8441 defines extended CONNECT. A server advertises SETTINGS_ENABLE_CONNECT_PROTOCOL with value 1, after which a client may send CONNECT with a :protocol pseudo-header, and, unusually for CONNECT, with :scheme and :path present. :protocol = websocket is how WebSockets are carried over HTTP/2; RFC 9220 ports the same mechanism to HTTP/3. This is why WebSockets through a reverse proxy behave differently on HTTP/1.1 (an Upgrade: websocket handshake) and on HTTP/2 (an extended CONNECT stream).

RFC 9298 (connect-udp) uses the same slot: :protocol = connect-udp with a URI template such as /.well-known/masque/udp/{target_host}/{target_port}/. Datagrams travel as HTTP Datagrams (RFC 9297), mapped onto QUIC DATAGRAM frames on HTTP/3 or onto the Capsule Protocol over a stream when the transport has no datagram support. RFC 9484 extends the idea to whole IP packets (connect-ip), which is what makes full-tunnel VPN-like behaviour expressible in HTTP.

MASQUE is the IETF working group that produced this family. Its aim is to let proxied traffic of any kind (TCP via CONNECT, UDP via CONNECT-UDP, IP via CONNECT-IP) ride inside ordinary HTTP/3 to a proxy, so that the proxy connection is indistinguishable from other HTTPS traffic and so that a client can chain two independent proxies such that neither one holds both the client identity and the destination. For proxy operators the operational headline is that MASQUE tunnels are UDP/443 QUIC, not TCP CONNECT, so a firewall policy that assumes "tunnels look like CONNECT" no longer holds.

Failure modes#

"Proxy CONNECT aborted" (curl). curl emits this when the proxy closes the connection, or the response ends, before a complete CONNECT response is read. It is not an HTTP status; it means the proxy hung up mid-handshake. Common causes: an upstream ACL that resets rather than replying, an idle timeout shorter than the origin's TCP connect time, or a TLS-inspecting appliance that failed to build a certificate. Distinguish it from curl's other message, Received HTTP code 403 from proxy after CONNECT, which means the proxy answered properly and denied you.

A proxy that buffers the 200. Some middleboxes hold the CONNECT response until they have "enough" data. The client is waiting to send its ClientHello, the proxy is waiting to send the 200, and the session stalls until a timeout fires. Symptom: connections that succeed after exactly the proxy's read timeout, or never. Reproduce with openssl s_client -proxy host:3128 -connect example.com:443 and watch whether the 200 arrives immediately.

Buffering in the tunnel itself. A relay that accumulates a full buffer before forwarding destroys interactive protocols run over CONNECT (SSH via ProxyCommand, terminal sessions, streaming APIs). The tunnel must be byte-transparent and flush-on-read. This is the forward-proxy twin of response buffering in a reverse proxy.

Content-Length: 0 on the 200. A proxy that adds it is violating RFC 9110. Tolerant clients ignore it; strict ones treat the tunnel's first bytes as the start of a new HTTP message and fail with a parse error.

Auth required on CONNECT but not on plain requests. Many enterprise proxies allow GET http://... anonymously while demanding credentials on CONNECT, because tunnels are what they cannot inspect. Symptom: http:// URLs work and every https:// URL returns 407. The client must handle 407 during tunnel setup, which not every HTTP library does.

Credentials sent only after a challenge. Because a 407 during CONNECT arrives before any tunnel exists, the client must be able to retry the CONNECT on a fresh or reused connection with Proxy-Authorization attached. Connection-oriented schemes make this much worse; see the NTLM section of proxy authentication.

Idle tunnels reaped silently. A tunnel with no traffic is indistinguishable from a dead one. Proxies close them on an idle timer, and the application sees a truncated stream rather than an error. Enable TCP keepalives or application-level pings, and check the whole chain with the timeout ladder checker.

Frequently asked questions#

What is the HTTP CONNECT method used for?#

CONNECT asks a forward proxy to open a TCP connection to a specified host and port and then relay bytes verbatim in both directions. Its dominant use is carrying HTTPS through a proxy, because the client must perform the TLS handshake with the origin itself. It is also used for SSH, SMTP and other TCP protocols where policy permits.

Does a 200 response to CONNECT have a body?#

No. RFC 9110 forbids Content-Length and Transfer-Encoding in a 2xx response to CONNECT, and there is no body. The first octet after the blank line that terminates the response header section is tunnel payload. Non-2xx responses are ordinary HTTP responses and may have a body.

Can a proxy see the URL inside a CONNECT tunnel?#

Not without intercepting TLS. It sees the authority in the CONNECT request line, the SNI in the ClientHello, byte counts and timing. Full URLs, headers and status codes are inside the encrypted stream. Recovering them requires terminating TLS at the proxy with a trusted root CA installed on clients.

Why does my proxy return 403 for CONNECT on port 8443?#

Because the port is not in the proxy's allowed CONNECT port list. Squid's default configuration permits CONNECT only to port 443 via its SSL_ports ACL and denies everything else. Add the port to SSL_ports, not to Safe_ports, since only the former governs tunnels.

What does "Proxy CONNECT aborted" mean in curl?#

It means the proxy closed the connection before returning a complete response to the CONNECT request. The failure is at tunnel setup, not inside TLS. Look at proxy logs for an ACL reset, an authentication requirement, or a TLS-inspection appliance that failed to reach the origin.

How is CONNECT different from a SOCKS5 proxy?#

CONNECT is an HTTP request that the proxy must parse, so it carries HTTP headers such as Proxy-Authorization and can be logged and filtered by an HTTP engine. SOCKS5 uses a compact binary handshake, supports UDP association and BIND, and is protocol-agnostic. The trade-offs are set out in SOCKS5 versus HTTP proxy.

Does HTTP/2 still use CONNECT?#

Yes, per stream. :method = CONNECT with :authority set and no :scheme or :path turns one stream into a tunnel carried in DATA frames. Extended CONNECT (RFC 8441 for HTTP/2, RFC 9220 for HTTP/3) adds a :protocol pseudo-header for WebSockets and for CONNECT-UDP.

What is CONNECT-UDP and how does it relate to MASQUE?#

CONNECT-UDP (RFC 9298) is extended CONNECT with :protocol = connect-udp, which proxies UDP flows inside HTTP using HTTP Datagrams (RFC 9297). It is one output of the IETF MASQUE working group, alongside CONNECT-IP (RFC 9484) for whole IP packets. Both typically ride over HTTP/3, so they appear as QUIC on UDP/443 rather than as TCP CONNECT.

Primary sources#

Every normative claim on this page is checked against the specification or the vendor documentation listed here. Where behaviour is version dependent, the version is named in the text.

  1. RFC 9110 HTTP Semantics, section 9.3.6 CONNECT
  2. RFC 9113 HTTP/2, section 8.5 The CONNECT Method
  3. RFC 8441 Bootstrapping WebSockets with HTTP/2
  4. RFC 9220 Bootstrapping WebSockets with HTTP/3
  5. RFC 9298 Proxying UDP in HTTP
  6. RFC 9484 Proxying IP in HTTP
  7. RFC 9297 HTTP Datagrams and the Capsule Protocol
  8. Squid default configuration and CONNECT port ACLs
  9. curl HTTP proxy handling (lib/http_proxy.c)

Found something wrong, or behaviour that differs on your version? Report it with the version number and a primary source. Anything substantive is fixed in the page and logged on the corrections page. See editorial standards for how pages are researched, sourced and reviewed.

More in proxy fundamentals#