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 case | Downstream support | Upstream requirement |
|---|---|---|
| TCP forward proxy | HTTP/1.1, HTTP/2, HTTP/3 where enabled | A 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:
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.
| Protocol | Downstream support | Upstream requirement | Primary use |
|---|---|---|---|
| WebSocket | HTTP/1.1, HTTP/2, HTTP/3 | H1, h2c, or H3 depending on HTTPVersion | Bidirectional application streams |
| WebTransport | HTTP/3 | H3 datagram tunnel | Browser and native real-time sessions with streams and datagrams |
| CONNECT-UDP | HTTP/3 | H3 datagram tunnel | UDP proxying over HTTP |
| CONNECT-IP | HTTP/3 | H3 datagram tunnel | IP 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.1 | h1 | Classic WebSocket upgrade |
| HTTP/1.1 | h2c | Classic upgrade to H2 Extended CONNECT |
| HTTP/1.1 | h3 | Classic upgrade to H3 Extended CONNECT |
| HTTP/2 | h1 | H2 Extended CONNECT to classic upgrade |
| HTTP/2 | h2c | H2 Extended CONNECT |
| HTTP/2 | h3 | H2 to H3 Extended CONNECT |
| HTTP/3 | h1 | H3 Extended CONNECT to classic upgrade |
| HTTP/3 | h2c | H3 to H2 Extended CONNECT |
| HTTP/3 | h3 | H3 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 tunnel | QUIC tunnel | |
|---|---|---|
| Protocol | HTTP/3 + Extended CONNECT | Raw QUIC |
| Authentication | Token auth, rstream auth, access policies | Edge TLS policy, mTLS, and project access policies |
| Streams | Multiplexed by session path | Per-connection |
| Datagrams | Yes | Yes |
| Browser client | Version-dependent WebTransport API | No |
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:
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:
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.