Tunnel Runtime

Go SDK Tunnel Runtime

Create, serve, and dial rstream tunnels from Go.


The tunnel runtime is the part of the Go SDK that creates upstream sessions, publishes services, accepts inbound streams, and dials private tunnels. It uses standard Go interfaces whenever possible, so existing net/http, net.Conn, and net.PacketConn code can stay close to its normal shape.

Create a Tunnel

Create an authenticated control channel, then create the tunnel with the properties that describe how it should be exposed.

ctrl, err := client.Connect(ctx, nil)
if err != nil {
  log.Fatal(err)
}
defer ctrl.Close()
 
tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:        rstream.StringPtr("api"),
  Publish:     rstream.BoolPtr(true),
  Protocol:    rstream.ProtocolPtr(rstream.ProtocolHTTP),
  HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP1_1),
})
if err != nil {
  log.Fatal(err)
}
defer tunnel.Close()

For published tunnels, read the forwarding address after the tunnel is created.

forwardingAddress, err := tunnel.ForwardingAddress()
if err != nil {
  log.Fatal(err)
}
 
fmt.Println(forwardingAddress)

Serve HTTP

An HTTP tunnel can be used as a net.Listener, which lets a Go HTTP server accept requests directly from rstream.

func handler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/plain")
  fmt.Fprintln(w, "Hello from rstream-go")
}
 
listener, ok := tunnel.(net.Listener)
if !ok {
  log.Fatal("tunnel does not implement net.Listener")
}
 
server := &http.Server{
  Handler: http.HandlerFunc(handler),
}
 
log.Fatal(server.Serve(listener))

This pattern works well for API services, callback receivers, admin tools, and other HTTP applications where the Go process should own the upstream server.

WebSocket

WebSocket works on HTTP tunnels across the supported upstream HTTP versions. For H1 and H2C upstreams, use any standard WebSocket library with an HTTP tunnel and http.Server.

tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:        rstream.StringPtr("ws-example"),
  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 with the WebSocket library already used by your service.
})
 
log.Fatal(http.Serve(listener, nil))

For HTTP/3 upstreams, WebSocket uses Extended CONNECT and requires an HTTP/3-aware server. See Connection Upgrades.

Plain CONNECT

Plain authority-form CONNECT is available on HTTP tunnels for upstream forward proxy services. The upstream service receives CONNECT host:port, applies its own target policy, and returns 2xx before the engine starts relaying opaque TCP bytes.

Use a bytestream HTTP tunnel for H1 or H2C upstreams.

props := rstream.TunnelProperties{
  Name:        rstream.StringPtr("connect-egress"),
  Type:        rstream.TunnelTypePtr(rstream.TunnelTypeBytestream),
  Publish:     rstream.BoolPtr(true),
  Protocol:    rstream.ProtocolPtr(rstream.ProtocolHTTP),
  HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP2),
}

Use a datagram HTTP tunnel for H3 upstreams.

props := rstream.TunnelProperties{
  Name:        rstream.StringPtr("connect-egress-h3"),
  Type:        rstream.TunnelTypePtr(rstream.TunnelTypeDatagram),
  Publish:     rstream.BoolPtr(true),
  Protocol:    rstream.ProtocolPtr(rstream.ProtocolHTTP),
  HTTPVersion: rstream.HTTPVersionPtr(rstream.HTTP3),
}

The engine binds CONNECT routing to the selected tunnel connection instead of using the CONNECT authority as a tunnel hostname. This prevents a proxy request from escaping the tunnel that was already selected by the downstream connection.

WebTransport

WebTransport runs over HTTP/3 tunnels. Create a datagram HTTP tunnel, then pass the tunnel packet listener to a webtransport.Server.

tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:        rstream.StringPtr("wt-example"),
  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()
 
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)
 
log.Fatal(wtServer.Serve(rstream.PacketConnFromPacketListener(packetListener)))

CONNECT-UDP and CONNECT-IP

CONNECT-UDP and CONNECT-IP are MASQUE protocols carried over HTTP/3 Extended CONNECT. They use the same published tunnel shape as WebTransport: Type: TunnelTypeDatagram, Protocol: ProtocolHTTP, and HTTPVersion: HTTP3.

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

The upstream application implements the actual UDP or IP proxy behavior. The Go examples use github.com/quic-go/masque-go for CONNECT-UDP and github.com/quic-go/connect-ip-go for CONNECT-IP. Private-mode samples also show a client dialing the datagram tunnel with PacketDial and running HTTP/3 over the returned packet connection.

Bytestream and Datagram Tunnels

Private bytestream tunnels use net.Listener on the server side and client.Dial on the client side.

tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:    rstream.StringPtr("stream-echo"),
  Type:    rstream.TunnelTypePtr(rstream.TunnelTypeBytestream),
  Publish: rstream.BoolPtr(false),
})
if err != nil {
  log.Fatal(err)
}
 
listener, ok := tunnel.(net.Listener)
if !ok {
  log.Fatal("tunnel does not implement net.Listener")
}

Datagram tunnels use packet-oriented APIs.

tunnel, err := ctrl.CreateTunnel(ctx, rstream.TunnelProperties{
  Name:     rstream.StringPtr("datagram-echo"),
  Publish:  rstream.BoolPtr(true),
  Protocol: rstream.ProtocolPtr(rstream.ProtocolDTLS),
})
if err != nil {
  log.Fatal(err)
}
 
packetListener, ok := tunnel.(rstream.PacketListener)
if !ok {
  log.Fatal("tunnel does not implement rstream.PacketListener")
}

Dial Private Tunnels

The same SDK can act as a client and dial an existing private tunnel by id or name.

conn, err := client.Dial(ctx, rstream.Addr{IdOrName: "my-tunnel"})
if err != nil {
  log.Fatal(err)
}
defer conn.Close()

Datagram tunnels use the packet API.

pc, err := client.PacketDial(ctx, rstream.Addr{IdOrName: "my-dgram-tunnel"})
if err != nil {
  log.Fatal(err)
}
defer pc.Close()

The native Go HTTP client can reuse private tunnel dialing by wiring DialContext.

httpClient := &http.Client{
  Transport: &http.Transport{
    DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
      host, _, err := net.SplitHostPort(addr)
      if err != nil || host == "" {
        return nil, fmt.Errorf("failed to extract host from address: %v", err)
      }
      return client.Dial(ctx, rstream.Addr{IdOrName: host})
    },
  },
}
 
resp, err := httpClient.Get("http://h1-example/")
if err != nil {
  log.Fatal(err)
}
defer resp.Body.Close()