Learning objective. See how CONNECT-IP builds a full VPN inside HTTP/3: how it differs from CONNECT-UDP, how the reliable capsule control plane assigns an address and advertises routes, and how the unreliable datagram data plane then carries whole IP packets.
From proxying UDP to proxying IP #
CONNECT-UDP (§12.3) tunnels UDP payloads to one target. CONNECT-IP ([RFC 9484]) goes a layer lower: it tunnels whole IP packets (headers and all, any IP protocol), which is exactly what a VPN is. RFC 9484 is blunt about the gap it fills: CONNECT and CONNECT-UDP "cannot tunnel other IP protocols… nor convey fields of the IP header," whereas CONNECT-IP enables "remote access VPN, site-to-site VPN… or general-purpose packet tunnelling" ([RFC 9484 §1]). Because it can carry arbitrary IP traffic, it "MUST be operated over TLS or QUIC encryption" ([RFC 9484 §4]).
The tunnel opens with the same Extended CONNECT mechanism, changing only the :protocol and path
([RFC 9484 §4.4]):
:method = CONNECT
:protocol = connect-ip
:path = /.well-known/masque/ip/*/*/ ← a full VPN (no scope restriction)
The template is /.well-known/masque/ip/{target}/{ipproto}/, and both variables are optional
([RFC 9484 §3]): a wildcard * for target and ipproto means "any destination, any IP protocol": a
general VPN tunnel. A concrete target/ipproto scopes the tunnel to specific destinations.
Two planes: reliable control, unreliable data #
CONNECT-IP's key structural idea is that it uses both carriers from §12.2, for two different jobs:
- The control plane rides reliable capsules. Assigning an address and advertising routes are configuration that must arrive and arrive in order; you cannot build a VPN on messages that might be dropped. So these travel as capsules on the request's data stream (§12.2).
- The data plane rides unreliable datagrams. The actual IP packets travel as HTTP Datagrams over QUIC DATAGRAM frames: unreliable, exactly like a real network path, so the tunneled traffic's own protocols handle any loss (§12.1).
This split is the whole design: the slow-changing configuration is reliable; the high-volume packet flow is not.
And it is the proxy itself that enforces the split, because the proxy is a full QUIC endpoint of the client↔proxy connection, not a passive wire. On that one connection it acknowledges and retransmits a lost STREAM frame carrying a capsule, exactly as Chapter 3 described — while a lost DATAGRAM frame is acknowledged but "not retransmitted upon loss detection" ([RFC 9221 §5.2]). Same connection, same congestion controller, opposite recovery: configuration is guaranteed to survive loss, and a stale tunneled packet is never resurrected. Recovery of the packets themselves, if any, belongs to whatever protocol runs inside the tunnel (§12.5).
The control plane: three capsules build the VPN #
Three capsule types ([RFC 9484 §4.7], constants.md §16) turn an open tunnel into a working network
interface:
- ADDRESS_REQUEST (
0x02), the client asking for an address: it "allows an endpoint to request assignment of IP addresses from its peer" ([RFC 9484 §4.7.2]). A client expecting an address "MUST send an ADDRESS_REQUEST capsule" ([RFC 9484 §4.7.1]). - ADDRESS_ASSIGN (
0x01), the proxy granting one: it "allows an endpoint to assign its peer a list of IP addresses or prefixes" ([RFC 9484 §4.7.1]), e.g.192.0.2.11/32. The client puts this address on a virtual interface; it is now the client's identity inside the tunnel. - ROUTE_ADVERTISEMENT (
0x03), the proxy saying what it will carry: it "communicate[s] to its peer that it is willing to route traffic to a set of IP address ranges" ([RFC 9484 §4.7.3]). Advertising the whole range0.0.0.0–255.255.255.255makes it a default route: a full-tunnel VPN.
Because these are capsules on the stream, they are reliable and ordered, and either side may send them; the client can also assign addresses and advertise routes for network-to-network routing ([RFC 9484 §4.1]).
The data plane: whole IP packets as datagrams #
Once addressed and routed, IP packets flow as HTTP Datagrams whose payload, like CONNECT-UDP, begins with a Context ID ([RFC 9484 §6]):
IP Proxying HTTP Datagram Payload {
Context ID (i),
Payload (..),
}
Context ID 0 means the payload is a complete IP packet: "when the Context ID is set to zero, the Payload field contains a full IP packet (from the IP Version field until the last byte of the IP payload)" ([RFC 9484 §6]). So the client takes an IP packet its OS wants to send, prepends the Quarter Stream ID (§12.2) and Context ID 0, and ships it as a QUIC DATAGRAM frame; the proxy strips the prefixes, recovers the raw IP packet, and routes it onward toward its real destination, then encapsulates return packets the same way. To the client's operating system, the tunnel looks like an ordinary network interface with the assigned address.
Worked example: a remote-access VPN #
Trace Figure 12.4-1. A laptop wants all its traffic to exit through the proxy. It sends Extended CONNECT
with :protocol = connect-ip and :path = /.well-known/masque/ip/*/*/; the proxy returns 200. On the
reliable control plane the client sends an ADDRESS_REQUEST; the proxy replies with an ADDRESS_ASSIGN
of 192.0.2.11/32 and a ROUTE_ADVERTISEMENT for 0.0.0.0–255.255.255.255. The laptop configures a
virtual interface with 192.0.2.11 and points its default route at the tunnel. Now every packet the OS
sends (a TCP SYN to a website, a QUIC Initial, an ICMP ping) is captured by that interface, wrapped as an
HTTP Datagram (Quarter Stream ID, Context ID 0, the full IP packet), and sent to the proxy, which routes it
to the real internet and returns the responses. The websites see the proxy's address; the local network
sees only HTTP/3 to the proxy. That is a VPN built entirely out of HTTP.
Here is that setup as frames on the wire, with the capsule bytes computed. Watch the two planes interleave on one tunnel: every control message is a capsule inside stream 8's DATA frames, and every packet is a DATAGRAM frame on no stream, tied back by Quarter Stream ID 2:
Two consequences of sharing one connection are worth pinning down. First, the planes can share more than the wire: a QUIC packet "MAY contain multiple frames and multiple frame types" ([RFC 9000 §12.4]), so a single 1-RTT packet can legally carry both a stream-8 STREAM frame holding a capsule and a DATAGRAM frame whose payload starts with Quarter Stream ID 2. Demultiplexing is layered: frame type first, then stream ID on one path and Quarter Stream ID on the other. Second, the planes share the tunnel's fate but not its ordering. Nothing sequences a datagram against a capsule, so a datagram can overtake control data it logically depends on; RFC 9298 notes exactly this for context registration — "it is possible for datagrams to be received with Context IDs that have not yet been registered," for instance "due to reordering" ([RFC 9298 §4]). Reliable-and-ordered is a property within the capsule stream, never across the planes.
Notice how CONNECT-IP reuses everything from earlier in the chapter rather than inventing new machinery. The tunnel is an Extended CONNECT request (§12.3); the data plane is HTTP Datagrams with a Context ID (§12.2); the control plane is capsules (§12.2). The only genuinely new pieces are three capsule types for addressing and routing. This is the payoff of the layered design: a full VPN is a thin addressing layer on top of the datagram/capsule foundation, which is itself a thin layer on QUIC's DATAGRAM frames (Chapter 9).
editorial Get the two-plane split right or nothing works: address and route capsules MUST go on the reliable stream, and the IP packets MUST go as datagrams; sending configuration as unreliable datagrams means a dropped ADDRESS_ASSIGN leaves the client with no address and a silently dead tunnel. Size the data plane to the outer QUIC datagram limit (§9.2) and have the client's virtual interface advertise a matching MTU, so the OS doesn't hand you IP packets too large to fit one QUIC DATAGRAM frame (which can't be fragmented, §9.3). And never run CONNECT-IP unencrypted: carrying raw IP for arbitrary destinations over a cleartext tunnel is a serious exposure, which is why the RFC makes TLS/QUIC mandatory.
Takeaways #
CONNECT-IP tunnels whole IP packets (a VPN inside HTTP/3) opened by an Extended CONNECT with
:protocol = connect-ip. It splits into a reliable capsule control plane (ADDRESS_REQUEST 0x02,
ADDRESS_ASSIGN 0x01, ROUTE_ADVERTISEMENT 0x03) that assigns the client an address and advertises routes,
and an unreliable datagram data plane carrying full IP packets with Context ID 0. It reuses the
Extended CONNECT, HTTP Datagram, and Capsule machinery of the previous sections, adding only three
addressing capsules. A byte-level worked example of a UDP packet's full journey through a MASQUE proxy is
§12.5.