CONNECT-UDP in Firefox

        flowchart LR
    Client[Client]
    Proxy[Proxy]
    Target[Target]

    subgraph OuterConn["Outer HTTP/3 Connection"]
        subgraph InnerConnClientToProxy["Inner HTTP/3 Connection"]
            InnerConnClientToProxy1["Inner Stream 1"]
            InnerConnClientToProxy2["Inner Stream 2"]
        end
    end

    subgraph UDPTunnel["UDPTunnel"]
        subgraph InnerConnProxyToTarget["Inner HTTP/3 Connection"]
            InnerConnProxyToTarget1["Inner Stream 1"]
            InnerConnProxyToTarget2["Inner Stream 2"]
        end
    end

    Client --> OuterConn
    OuterConn --> Proxy

    Proxy --> UDPTunnel
    UDPTunnel --> Target
    

Terminology and Concepts

To clearly describe the design, we define two key terms that appear frequently throughout this document:

Outer Connection

The outer connection refers to the HTTP/3 connection between Firefox and the proxy. This connection is established once and can be reused for multiple HTTP/3 requests.

Inner Connection

The inner connection refers to the logical UDP tunnel established over an HTTP/3 stream via the CONNECT method.

Connection Establishment

Note: Firefox only supports CONNECT-UDP over HTTP/3. Support over HTTP/2 or HTTP/1.1 is not implemented.

To establish a UDP tunnel over HTTP/3, Firefox initiates a request using the CONNECT method with the :protocol set to connect-udp. This request negotiates a stream that serves as the logical tunnel for encapsulated UDP datagrams between the client and the proxy.

The tunnel setup process involves sending a HEADERS frame on a new HTTP/3 stream with the following pseudo-header and header fields:

:method = CONNECT
:protocol = connect-udp
:scheme = https
:path = /.well-known/masque/udp/example.com/443/
:authority = proxy.org
capsule-protocol = ?1

Http3ConnectUDPStream

Http3ConnectUDPStream is the core implementation of the CONNECT-UDP in Firefox. It behaves like a regular nsIUDPSocket, allowing the inner HTTP/3 connection to send and receive UDP datagrams.

Internally, it encapsulates outgoing UDP datagrams into HTTP datagrams, which are then sent over the outer HTTP/3 connection to the proxy. Incoming HTTP datagrams from the proxy are decoded and delivered as UDP packets to the consumer.

Data sending flow

        sequenceDiagram
    participant HttpConnectionUDP (inner)
    participant Http3Session (inner)
    participant Http3Stream (inner)
    participant nsHttpTransaction
    participant Http3Session (outer)
    participant Http3ConnectUDPStream
    participant NeqoHttp3Conn (outer)

    HttpConnectionUDP (inner)->>HttpConnectionUDP (inner): SendData()
    HttpConnectionUDP (inner)->>Http3Session (inner): SendData()
    Http3Session (inner)->>Http3Stream (inner): ReadSegments()
    Http3Stream (inner)->>nsHttpTransaction: ReadSegmentsAgain()
    nsHttpTransaction->>Http3Stream (inner): ReadRequestSegment()
    Http3Stream (inner)->>Http3Session (inner): TryActivating/SendRequestBody()
    Http3Session (inner)->>Http3ConnectUDPStream: SendWithAddress()
    Http3ConnectUDPStream->>Http3ConnectUDPStream: QueueDatagram()
    Http3ConnectUDPStream->>Http3Session (outer): StreamHasDataToWrite()
    Http3Session (outer)->>Http3ConnectUDPStream: ReadSegments()
    Http3ConnectUDPStream->>Http3Session (outer): SendHTTPDatagram()
    Http3Session (outer)->>NeqoHttp3Conn: NeqoProcessOutputAndSend()
    

Data reading flow

        sequenceDiagram
    participant NeqoHttp3Conn (outer)
    participant Http3Session (outer)
    participant Http3ConnectUDPStream
    participant Http3Session (inner)
    participant Http3Stream (inner)
    participant HttpConnectionUDP (inner)
    participant nsHttpTransaction

    NeqoHttp3Conn (outer)->>Http3Session (outer): Receive HTTP Datagram
    Http3Session (outer)->>Http3ConnectUDPStream: OnDatagramReceived()
    Http3ConnectUDPStream->>HttpConnectionUDP (inner): OnPacketReceived()
    HttpConnectionUDP (inner)->>Http3Session (inner): RecvData()
    Http3Session (inner)->>Http3ConnectUDPStream: RecvWithAddr()
    Http3Session (inner)->>Http3Session (inner): ProcessTransactionRead()
    Http3Session (inner)->>Http3Stream (inner): WriteSegments()
    Http3Stream (inner)->>nsHttpTransaction: WriteSegmentsAgain()
    

Fallback Mechanism

This section describes the fallback mechanism for establishing proxy connections when using HTTP/3. The process is divided into two layers: the outer connection (between the browser and the proxy) and the inner connection (between the browser and the target server through the proxy).

Outer Connection

  • The browser first attempts to establish this connection using HTTP/3.

  • If the HTTP/3 attempt fails, the connection falls back to HTTP/2 or HTTP/1.

Current implementation:

  • The fallback logic is implemented in nsHttpTransaction (subject to future changes under the Happy Eyeballs project).

  • When a transaction is inserted into the pending queue:

    • A HTTP/3 backup timer is started.

    • An attempt to establish an HTTP/3 connection to the proxy is initiated.

  • If the backup timer fires before the HTTP/3 connection succeeds, a TCP connection to the proxy server is created.

  • If the TCP connection succeeds while HTTP/3 is still pending, we fall back to the TCP-based connection.

  • At this point, the connection info of the inner connection is also updated to disable HTTP/3, since the we do not support CONNECT-UDP over HTTP/2 or HTTP/1.

Inner Connection

Even if the outer connection is established with HTTP/3, the inner connection may still need to fall back.

Current implementation:

  • This fallback logic is also implemented in nsHttpTransaction.

  • Once the proxy responds with 200 OK to the initial connect-udp setup, a HTTP/3 fallback timer for the inner connection is started.

  • If the fallback timer fires:

    • HTTP/3 for the inner connection is disabled.

    • The ongoing HTTP/3 inner connection is closed.

  • The inner connection falls back to HTTP/2 or HTTP/1 using a standard HTTP CONNECT tunnel.