Connection Upgrades

Connection Upgrades

WebSocket, plain CONNECT, WebTransport, CONNECT-UDP, and CONNECT-IP over HTTP tunnels.


HTTP tunnels support classic connection upgrades, plain authority-form CONNECT, and HTTP Extended CONNECT. This covers WebSocket, TCP proxying with CONNECT, WebTransport, CONNECT-UDP, and CONNECT-IP.

Plain HTTP CONNECT

Plain CONNECT is the standard HTTP forward proxy mechanism for TCP targets. A downstream client sends CONNECT host:port to the published rstream HTTP endpoint; the engine forwards that CONNECT request to the upstream service attached by the rstream client and relays opaque bytes after the upstream returns a 2xx response.

Plain CONNECT is not Extended CONNECT. It has no :protocol token. It follows the HTTP versions available on the tunnel: HTTP/1.1 and HTTP/2 where supported, plus HTTP/3 when the runtime and tunnel are configured for h3.

Use caseDownstream supportUpstream requirement
TCP forward proxyHTTP/1.1, HTTP/2, HTTP/3 where enabledA forward proxy service that handles CONNECT host:port

The engine does not resolve the CONNECT target or enforce destination allowlists itself. Target policy, outbound DNS behavior, proxy authentication, and audit logging belong to the upstream proxy service. rstream still applies the tunnel's edge authentication, access policies, routing, and observability before the CONNECT request reaches upstream.

Because a plain CONNECT request uses the target as its authority, the engine does not route it by Host. It binds the request to the tunnel selected by the downstream TLS or QUIC connection. On reused HTTP/2 and HTTP/3 connections, rstream also separates reverse HTTP traffic from proxy egress traffic: once a connection carries plain CONNECT, CONNECT-UDP, or CONNECT-IP, normal HTTP requests on that same connection are rejected with 421 Misdirected Request, and vice versa.

Proxy-Authorization and Proxy-Authenticate are preserved for plain CONNECT so the upstream forward proxy can authenticate clients. These credentials are redacted from logs.

References:

RFC 9110 §9.3.6.

Extended CONNECT

Extended CONNECT lets a client open a protocol-specific tunnel inside HTTP/2 or HTTP/3 instead of using the HTTP/1.1 Upgrade mechanism. In rstream, this keeps upgraded traffic on the HTTP tunnel path, so edge authentication and access policies still apply before the session reaches upstream.

ProtocolDownstream supportUpstream requirementPrimary use
WebSocketHTTP/1.1, HTTP/2, HTTP/3H1, h2c, or H3 depending on HTTPVersionBidirectional application streams
WebTransportHTTP/3H3 datagram tunnelBrowser and native real-time sessions with streams and datagrams
CONNECT-UDPHTTP/3H3 datagram tunnelUDP proxying over HTTP
CONNECT-IPHTTP/3H3 datagram tunnelIP packet proxying over HTTP

WebTransport over HTTP/3 is still tracked as an IETF draft rather than a published RFC; rstream supports the quic-go/webtransport-go implementation used by the Go examples.

References:

WebSocket over HTTP/2 RFC 8441, WebSocket over HTTP/3 RFC 9220, CONNECT-UDP RFC 9298, CONNECT-IP RFC 9484, HTTP Datagrams and capsules RFC 9297.

WebSocket

rstream supports WebSocket on both the downstream (client-to-engine) and upstream (engine-to-server) legs across all HTTP versions. The downstream and upstream versions are independent: the engine performs protocol translation automatically.

Supported combinations

Downstream (client → engine)Upstream (engine → server)Mechanism
HTTP/1.1h1Classic WebSocket upgrade
HTTP/1.1h2cClassic upgrade to H2 Extended CONNECT
HTTP/1.1h3Classic upgrade to H3 Extended CONNECT
HTTP/2h1H2 Extended CONNECT to classic upgrade
HTTP/2h2cH2 Extended CONNECT
HTTP/2h3H2 to H3 Extended CONNECT
HTTP/3h1H3 Extended CONNECT to classic upgrade
HTTP/3h2cH3 to H2 Extended CONNECT
HTTP/3h3H3 Extended CONNECT

The upstream HTTPVersion in TunnelProperties controls which protocol the engine uses toward the upstream server. The downstream version is independent — any HTTP client can connect regardless of the upstream setting.

HTTP/1.1 upstream

The standard WebSocket upgrade path. Any library that works over net.Listener is compatible on the server side.

tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:        rstream.StringPtr("ws-server"),
  Publish:     rstream.BoolPtr(true),
  Protocol:    rstream.ProtocolPtr(rstream.ProtocolHTTP),
  HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP1_1),
})
if err != nil {
  log.Fatal(err)
}
defer tunnel.Close()
 
listener, ok := tunnel.(net.Listener)
if !ok {
  log.Fatal("tunnel does not implement net.Listener")
}
 
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
  // upgrade and handle the connection
})
log.Fatal(http.Serve(listener, nil))

HTTP/2 cleartext upstream

Setting HTTPVersion to HTTP2 instructs the engine to reach the upstream server over HTTP/2 cleartext using Extended CONNECT. The upstream server receives a CONNECT request with :protocol: websocket and handles WebSocket frames over that stream.

tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:        rstream.StringPtr("ws-server"),
  Publish:     rstream.BoolPtr(true),
  Protocol:    rstream.ProtocolPtr(rstream.ProtocolHTTP),
  HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP2),
})

HTTP/3 upstream

Setting HTTPVersion to HTTP3 instructs the engine to reach the upstream server over HTTP/3 using Extended CONNECT. The tunnel type must be TunnelTypeDatagram because HTTP/3 runs over QUIC.

tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:        rstream.StringPtr("ws-server"),
  Type:        rstream.TunnelTypePtr(rstream.TunnelTypeDatagram),
  Publish:     rstream.BoolPtr(true),
  Protocol:    rstream.ProtocolPtr(rstream.ProtocolHTTP),
  HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP3),
})

Server-side considerations

For an H1 upstream, any standard WebSocket library works without modification — the engine forwards the standard Upgrade: websocket handshake unchanged.

For H2C and H3 upstreams, the upstream server receives WebSocket over Extended CONNECT. The server must handle a CONNECT request carrying :protocol: websocket. Libraries that only implement the classic HTTP/1.1 WebSocket opening handshake are not compatible with this handshake form; the upstream must either use an HTTP-version-agnostic library or handle the CONNECT method directly. The http-ws-h2c-server and http-ws-h3-server examples provide minimal reference implementations.

The downstream client is not affected by the upstream setting. Any WebSocket client connects using its native protocol; the engine translates to the configured upstream version transparently.

Token authentication, rstream Auth, and challenge mode apply to the initial WebSocket upgrade request.

References:

WebSocket RFC 6455, WebSocket over HTTP/2 RFC 8441, WebSocket over HTTP/3 RFC 9220.

WebTransport

WebTransport provides multiplexed bidirectional streams and unreliable datagrams over an HTTP/3 session. Browser and native clients can use it when they implement the WebTransport draft version supported by the runtime. rstream exposes WebTransport through a published HTTP/3 tunnel.

How it works

An HTTP/3 tunnel creates a datagram tunnel endpoint published on the edge network. The engine accepts WebTransport sessions from downstream clients using the Extended CONNECT mechanism (:protocol: webtransport), establishes a matching session to the upstream service over the tunnel, and relays bidirectional streams, unidirectional streams, and datagrams between the two sessions end-to-end.

Because WebTransport is carried over an HTTP/3 tunnel, all HTTP-level features are available: token authentication, rstream auth, and access policies apply to the initial session establishment. This is the key advantage of WebTransport over a raw QUIC tunnel, which has no HTTP-level authentication.

Server setup

import (
  "github.com/quic-go/quic-go/http3"
  "github.com/quic-go/webtransport-go"
  "github.com/rstreamlabs/rstream-go"
)
 
tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:        rstream.StringPtr("wt-server"),
  Type:        rstream.TunnelTypePtr(rstream.TunnelTypeDatagram),
  Publish:     rstream.BoolPtr(true),
  Protocol:    rstream.ProtocolPtr(rstream.ProtocolHTTP),
  HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP3),
})
if err != nil {
  log.Fatal(err)
}
defer tunnel.Close()
 
forwardingAddr, err := tunnel.ForwardingAddress()
if err != nil {
  log.Fatal(err)
}
fmt.Println("WebTransport endpoint:", forwardingAddr)
 
packetListener, ok := tunnel.(rstream.PacketListener)
if !ok {
  log.Fatal("tunnel does not implement rstream.PacketListener")
}
 
mux := http.NewServeMux()
wtServer := webtransport.Server{
  H3: &http3.Server{
    Handler:         mux,
    TLSConfig:       yourTLSConfig,
    EnableDatagrams: true,
  },
}
webtransport.ConfigureHTTP3Server(wtServer.H3)
 
mux.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) {
  sess, err := wtServer.Upgrade(w, r)
  if err != nil {
    http.Error(w, "upgrade failed", http.StatusBadRequest)
    return
  }
  go handleSession(sess)
})
 
log.Fatal(wtServer.Serve(rstream.PacketConnFromPacketListener(packetListener)))

Session handling

A WebTransport session exposes bidirectional streams, unidirectional streams, and datagrams. All four directions are relayed through the engine independently.

func handleSession(sess *webtransport.Session) {
  ctx := sess.Context()
 
  // Accept a client-opened bidirectional stream
  stream, err := sess.AcceptStream(ctx)
  if err != nil {
    return
  }
  defer stream.Close()
  io.Copy(stream, stream) // echo
 
  // Open a server-initiated bidirectional stream
  out, err := sess.OpenStreamSync(ctx)
  if err != nil {
    return
  }
  out.Write([]byte("hello from server"))
  out.Close()
 
  // Accept a client-opened unidirectional stream
  rs, err := sess.AcceptUniStream(ctx)
  if err != nil {
    return
  }
  io.ReadAll(rs)
 
  // Send and receive datagrams
  sess.SendDatagram([]byte("ping"))
  payload, err := sess.ReceiveDatagram(ctx)
 
  // Close the session with an application code and reason
  sess.CloseWithError(webtransport.SessionErrorCode(0), "done")
}

Application-level close codes

CloseWithError takes an error code and a reason string. Both propagate through the relay to the other side. Downstream clients receive the exact code and reason set by the upstream application, and vice versa. This lets applications signal termination intent without relying on stream-level errors.

Token authentication

When a tunnel is created with TokenAuth: true, the engine validates the downstream client's token before upgrading the WebTransport session. The token is supplied by the client in the Authorization: Bearer header or the rstream.token query parameter, identical to any other HTTP tunnel request.

tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:        rstream.StringPtr("wt-server"),
  Type:        rstream.TunnelTypePtr(rstream.TunnelTypeDatagram),
  Publish:     rstream.BoolPtr(true),
  Protocol:    rstream.ProtocolPtr(rstream.ProtocolHTTP),
  HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP3),
  TokenAuth:   rstream.BoolPtr(true),
})

A WebTransport client connecting to a token-protected tunnel supplies the token in the initial HTTP/3 CONNECT request:

// Browser — add the token as a query parameter
const transport = new WebTransport(
  "https://endpoint.example.rstream.test/app?rstream.token=<token>",
);
await transport.ready;

WebTransport vs raw QUIC tunnels

Both WebTransport tunnels (Protocol: HTTP, HTTPVersion: h3) and raw QUIC tunnels (Protocol: QUIC) carry QUIC-based traffic, but they serve different needs.

WebTransport tunnelQUIC tunnel
ProtocolHTTP/3 + Extended CONNECTRaw QUIC
AuthenticationToken auth, rstream auth, access policiesEdge TLS policy, mTLS, and project access policies
StreamsMultiplexed by session pathPer-connection
DatagramsYesYes
Browser clientVersion-dependent WebTransport APINo

Use a WebTransport tunnel when the client stack speaks the supported WebTransport version or when HTTP-level authentication and access policies matter. Use a raw QUIC tunnel for custom protocols that speak QUIC natively and do not need HTTP framing.

MASQUE: CONNECT-UDP and CONNECT-IP

CONNECT-UDP and CONNECT-IP are MASQUE protocols carried over HTTP/3 Extended CONNECT. They are useful when the upstream service should expose a UDP or IP proxy through the normal rstream HTTP tunnel model instead of publishing a raw QUIC or DTLS endpoint.

Create the tunnel as a published HTTP/3 datagram tunnel:

tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:        rstream.StringPtr("masque-server"),
  Type:        rstream.TunnelTypePtr(rstream.TunnelTypeDatagram),
  Publish:     rstream.BoolPtr(true),
  Protocol:    rstream.ProtocolPtr(rstream.ProtocolHTTP),
  HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP3),
})
if err != nil {
  log.Fatal(err)
}
defer tunnel.Close()

The engine accepts the downstream HTTP/3 Extended CONNECT session, opens a matching upstream HTTP/3 Extended CONNECT session to the service attached by the rstream client, and relays the request stream plus HTTP datagrams. It does not perform UDP socket handling, IP routing, DNS resolution, route assignment, or target policy itself; those semantics belong to the upstream MASQUE service.

CONNECT-UDP

CONNECT-UDP uses :protocol: connect-udp and the URI template model to identify the target host and port. The default interoperable shape is:

https://proxy.example/.well-known/masque/udp/{target_host}/{target_port}/

The upstream service can use quic-go/masque-go to parse the request and proxy datagrams to the requested UDP target.

References:

RFC 9298.

CONNECT-IP

CONNECT-IP uses :protocol: connect-ip and a URI template chosen by the IP proxy. Full-tunnel style proxies often use a fixed path, while scoped proxies can include optional template variables for target prefix and IP protocol.

The upstream service can use quic-go/connect-ip-go to assign client addresses, advertise routes, and exchange IP packets over HTTP datagrams.

References:

RFC 9484.

Client support

CONNECT-UDP and CONNECT-IP are not exposed as general-purpose browser JavaScript APIs. Use native MASQUE clients, system proxy integrations, VPN-like clients, or an application runtime that can open HTTP/3 Extended CONNECT sessions directly.

The Go SDK repository includes masque-server and masque-client examples for both protocols. They run private by default through the rstream SDK datagram dialer and use published HTTP/3 endpoints when started with --publish.