Go SDK
Integrate rstream in Go applications and agents.
The Go SDK is the reference SDK for rstream and covers the broadest surface today. It spans tunnel creation and lifecycle management, bytestream and datagram dialing, engine discovery, watch streams over SSE and WebSocket, TURN credential helpers, and the managed control-plane APIs. It is suitable both for backend services and for long-running agents or tools that operate close to machines and networks.
Repository: github.com/rstreamlabs/rstream-go
This page focuses on the Go SDK surface. For raw HTTP routes, see APIs. For watch semantics, see Signaling. For TURN-specific behavior, see STUN and TURN.
Creating a client
The usual entrypoint is the config bridge:
client, err := config.NewClientFromEnv()
if err != nil {
log.Fatal(err)
}config.NewClientFromEnv() reads the same runtime model as the CLI: explicit overrides first, then environment variables, then the selected context from ~/.rstream/config.yaml.
If you already know the engine and token explicitly, you can create the data-plane client directly:
client, err := rstream.NewClient(rstream.ClientOptions{
Engine: "project-endpoint.cluster.example.rstream.test:443",
Token: os.Getenv("RSTREAM_AUTHENTICATION_TOKEN"),
})
if err != nil {
log.Fatal(err)
}Transport selection
By default, the Go SDK reaches the edge over the standard TLS transport. Transport configuration is independent from tunnel properties: it controls how the client reaches the edge, not what the tunnel publishes.
This distinction matters when reading examples: QUICTransport changes the SDK-to-engine session, while ProtocolQUIC creates a published QUIC endpoint for downstream clients.
A custom transport can pin egress behavior or apply an explicit DNS policy:
client := &rstream.Client{
EngineURL: "project-endpoint.cluster.example.rstream.test:443",
Token: os.Getenv("RSTREAM_AUTHENTICATION_TOKEN"),
Transport: &rstream.Transport{
DNSOverride: rstream.StringPtr("1.1.1.1:853"),
DNSOverTLS: rstream.BoolPtr(true),
DNSServerName: rstream.StringPtr("cloudflare-dns.com"),
DNSSECEnabled: rstream.BoolPtr(true),
},
}The same DNS path is used for direct hostname resolution and for ECH discovery through DNS SVCB and HTTPS records.
When the edge supports it, the client-to-edge session can also use QUIC:
client := &rstream.Client{
EngineURL: "project-endpoint.cluster.example.rstream.test:443",
Token: os.Getenv("RSTREAM_AUTHENTICATION_TOKEN"),
Transport: &rstream.QUICTransport{
DNSOverride: rstream.StringPtr("1.1.1.1:853"),
DNSOverTLS: rstream.BoolPtr(true),
DNSServerName: rstream.StringPtr("cloudflare-dns.com"),
DNSSECEnabled: rstream.BoolPtr(true),
},
}When using the config bridge, RSTREAM_QUIC_TRANSPORT=1 is the quickest way to select QUIC transport for a process:
RSTREAM_QUIC_TRANSPORT=1 go run ./cmd/agentThat environment variable is honored by config.NewClientFromEnv() and the examples that use it. It does not affect clients built manually with rstream.NewClient; for those, pass &rstream.QUICTransport{} explicitly as shown above.
If you need QUIC together with explicit DNS or bind settings, prefer a config file context with transport.useQuic: true or construct rstream.QUICTransport directly so the full transport policy is visible in code.
For the transport model used by the CLI and SDKs, see Tunnel Transports.
Creating a tunnel
Tunnel creation is the core feature of the Go SDK. It happens over a control channel on the data plane.
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, the SDK can return the forwarding address:
forwardingAddress, err := tunnel.ForwardingAddress()
if err != nil {
log.Fatal(err)
}
fmt.Println(forwardingAddress)HTTP integration with the Go standard library
One of the most practical integration patterns is to use the tunnel directly as a net.Listener and hand it to http.Server.
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, "Hello from rstream-go")
}
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("h1-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")
}
server := &http.Server{
Handler: http.HandlerFunc(handler),
}
log.Fatal(server.Serve(listener))This lets the Go HTTP stack stay almost entirely unchanged while rstream provides the transport entrypoint.
WebSocket
WebSocket is supported across all three upstream HTTP versions. On H1 and H2C upstreams, any standard WebSocket library works without modification: create an HTTP tunnel, obtain it as a net.Listener, and pass it to http.Server. On H3 upstream the engine uses Extended CONNECT over QUIC and requires an HTTP/3-aware server; see Connection Upgrades for that path.
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 any WebSocket library. The engine relays the upgrade
// handshake and resulting byte stream to this handler transparently.
})
log.Fatal(http.Serve(listener, nil))All HTTP tunnel features apply to WebSocket connections: token authentication, rstream auth, and challenge mode gate the initial HTTP upgrade request.
WebTransport
WebTransport works over HTTP/3 tunnels. Create a datagram tunnel with Protocol: ProtocolHTTP and HTTPVersion: HTTP3, then use the tunnel as a PacketListener and pass it to a webtransport.Server.
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-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)
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
}
// sess exposes AcceptStream, OpenStreamSync, AcceptUniStream,
// OpenUniStreamSync, SendDatagram, ReceiveDatagram, and CloseWithError.
go handleSession(sess)
})
log.Fatal(wtServer.Serve(rstream.PacketConnFromPacketListener(packetListener)))To require token authentication, set TokenAuth: rstream.BoolPtr(true) on the tunnel properties. Browser clients supply the token as a query parameter:
const transport = new WebTransport(
"https://endpoint.example.rstream.test/app?rstream.token=<token>",
);
await transport.ready;For full session handling examples, a comparison with raw QUIC tunnels, and details on close code propagation, see Connection Upgrades.
Bytestream and datagram tunnels
The Go SDK supports both bytestream and datagram workloads.
Bytestream example
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")
}For published bytestream tunnels, set ProtocolTLS.
Datagram example
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")
}This is the path to take for UDP-style services where the Go process should directly manage packet-oriented sessions.
Dialing tunnels
The same SDK can also 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 HTTP stack can also reuse the native Go transport by wiring DialContext to client.Dial.
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()Listing tunnels and clients
The data-plane client exposes typed discovery APIs.
tunnels, err := client.ListTunnels(ctx, &rstream.ListTunnelsParams{
Filters: &rstream.ListTunnelsFilters{
Protocol: rstream.ProtocolPtr(rstream.ProtocolHTTP),
Labels: map[string]*string{
"service": rstream.StringPtr("api"),
"env": rstream.StringPtr("prod"),
},
},
})
if err != nil {
log.Fatal(err)
}clients, err := client.ListClients(ctx, &rstream.ListClientsParams{
Filters: &rstream.ListClientsFilters{
Labels: map[string]*string{
"role": rstream.StringPtr("edge-agent"),
},
},
})
if err != nil {
log.Fatal(err)
}Watching events
The watch APIs support SSE and WebSocket with the same filter model as the list APIs.
err := client.WatchWS(ctx, &rstream.WatchParams{
Clients: &rstream.ListClientsFilters{
Labels: map[string]*string{
"role": rstream.StringPtr("edge-agent"),
},
},
Tunnels: &rstream.ListTunnelsFilters{
Labels: map[string]*string{
"service": rstream.StringPtr("api"),
},
},
}, func(ev rstream.Event) error {
fmt.Println(ev.Type)
return nil
})
if err != nil {
log.Fatal(err)
}WatchSSE exposes the same contract over SSE.
TURN credentials
The Go SDK also exposes a config-aware TURN helper:
turn, err := config.CreateTURNCredentialsFromEnv(context.Background())
if err != nil {
log.Fatal(err)
}
_ = turnThis is the simplest path when the process already runs with a configured project context or appropriate environment variables.
Managed control-plane APIs
The Go SDK also exposes a separate control-plane client for managed project APIs. github.com/rstreamlabs/rstream-go covers the tunnels engine and data-plane APIs, while github.com/rstreamlabs/rstream-go/controlplane covers the managed control-plane APIs exposed by https://rstream.io. The controlplane package is the right entrypoint for whoami, project listing, endpoint resolution, and managed TURN issuance.
cp := controlplane.NewClient(
"https://rstream.io",
os.Getenv("RSTREAM_AUTHENTICATION_TOKEN"),
)
whoami, err := cp.Whoami(context.Background())
if err != nil {
log.Fatal(err)
}
projects, err := cp.ListProjects(context.Background(), controlplane.ListProjectsParams{})
if err != nil {
log.Fatal(err)
}
project, err := cp.ResolveProjectByEndpoint(context.Background(), "project-endpoint")
if err != nil {
log.Fatal(err)
}
_ = whoami
_ = projects
_ = projectEnvironment variables
The config bridge reads the same core environment variables as the CLI:
| Variable | Use |
|---|---|
RSTREAM_CONFIG | Select the configuration file path. |
RSTREAM_CONTEXT | Select a named context from the configuration file. |
RSTREAM_API_URL | Select the hosted control-plane API URL used for context resolution. |
RSTREAM_ENGINE | Override the engine endpoint used by data-plane clients. |
RSTREAM_AUTHENTICATION_TOKEN | Override the authentication token used by the SDK. |
RSTREAM_QUIC_TRANSPORT | Set to 1 to make config.NewClientFromEnv() use QUIC transport. |
This keeps the Go SDK aligned with the CLI context model while still allowing explicit SDK options for advanced network policy.
Examples
The repository includes examples for HTTP tunnel servers built on net/http, bytestream echo servers and clients, datagram echo servers and clients, HTTP client integration through the native Go transport, TURN credential creation, and managed control-plane usage.