Connection Upgrades

Connection Upgrades

WebSocket and WebTransport over HTTP tunnels.


HTTP tunnels support connection upgrades. This covers the two most common upgrade protocols: WebSocket and WebTransport.

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.1h1RFC 6455 upgrade
HTTP/1.1h2cRFC 6455 → RFC 8441 translation
HTTP/1.1h3RFC 6455 → RFC 9220 translation
HTTP/2h1RFC 8441 → RFC 6455 translation
HTTP/2h2cRFC 8441 Extended CONNECT
HTTP/2h3RFC 8441 → RFC 9220 translation
HTTP/3h1RFC 9220 → RFC 6455 translation
HTTP/3h2cRFC 9220 → RFC 8441 translation
HTTP/3h3RFC 9220 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 (RFC 8441)

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 (RFC 9220)

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 (RFC 8441 for H2C, RFC 9220 for H3). Libraries that only implement RFC 6455 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.

WebTransport

WebTransport is a browser API that provides multiplexed bidirectional streams and unreliable datagrams over an HTTP/3 session. 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 policiesTransport-level mTLS only
StreamsMultiplexed by session pathPer-connection
DatagramsYesYes
Browser clientYes (W3C WebTransport API)No

Use a WebTransport tunnel when the client is a browser 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.