Private Tunnels

Private Tunnels

Connect to services without publishing a public endpoint.


A private tunnel is a tunnel that is not published as an Internet-reachable endpoint. The engine still maintains tunnel state and forwards traffic, but connections require an rstream client or SDK that can dial the tunnel directly by id or name.

Private tunnels are useful when a service should remain reachable only through rstream-aware clients, when the upstream protocol should not be exposed as a public endpoint, or when the application needs direct control over connection behavior such as retries, timeouts, and multiplexing. Both sides still use outbound sessions to the engine, so the private environment does not need inbound firewall openings.

The model is intentionally different from a hidden public hostname. A private tunnel has no standard Internet socket for browsers, SSH clients, or custom clients to discover. Access happens through the rstream runtime, which means authentication, project selection, resources.tunnels boundaries, and the selected tunnel transport are part of the connection path.

Creating a private tunnel

In the CLI, private tunnels are created by disabling publishing. A name is optional, but it is usually useful because clients can dial the name instead of copying a generated tunnel id.

rstream forward 8080 --no-publish --name internal-api

Private bytestream tunnels are the default. Datagram semantics can be selected when the private workload is UDP-like and the client uses the packet dialing API.

rstream forward 5300 --datagram --no-publish --name internal-dns

In the engine model, private tunnels reject public exposure options such as HTTP version configuration and edge HTTP authentication settings. The tunnel remains accessible through rstream dialing APIs.

Connecting to a private tunnel

Private tunnels are dialed by tunnel id or name from an SDK client. Bytestream tunnels use Dial in the Go SDK:

conn, err := client.Dial(ctx, rstream.Addr{IdOrName: "internal-api"})

Datagram tunnels use the packet API:

pc, err := client.PacketDial(ctx, rstream.Addr{IdOrName: "internal-dns"})

Netcat over rstream

The CLI includes a netcat-style utility for private bytestream and datagram sessions. The command is available as rstream netcat, rstream ncat, and rstream nc. The C++ SDK also ships rstream-ncat for native bytestream workflows.

rstrm://<name-or-id> selects a private rstream endpoint. In listen mode, rstream nc can create a private unpublished tunnel and proxy it to a local service:

rstream nc -L rstrm://ssh-server -R 127.0.0.1:22

A client can then connect to that private tunnel:

rstream nc rstrm://ssh-server

The same command is useful as an SSH ProxyCommand because SSH still performs its normal host-key verification and user authentication while rstream carries the TCP stream:

ssh -o 'ProxyCommand rstream nc rstrm://ssh-server' admin@ssh-server hostname

The same local TCP adapter pattern works for database tooling. Access a Private PostgreSQL Database Without a VPN using rstream shows a private PostgreSQL workflow using rstream nc, a local client port, and standard PostgreSQL URLs.

In client mode, --exec and --sh-exec run a local command and bridge its stdin and stdout to the connection, which gives the command a bidirectional path without shell pipe plumbing:

rstream nc rstrm://internal-api -c "my-local-client"

Netcat datagram mode

rstream nc --datagram (-u) carries packets instead of a byte stream. Datagram mode requires rstrm:// endpoints on both sides: --listen rstrm://[name] creates a private unpublished datagram tunnel, and rstream nc -u rstrm://<name-or-id> dials one. --remote is not supported in datagram mode; datagram listen mode runs a command per accepted session with --exec or --sh-exec.

Because stdin and stdout are byte streams, packet boundaries on stdio are preserved with explicit framing, selected with --framing. The default and currently only supported framing is rfc4571, where each datagram is prefixed with a 2-byte big-endian length as defined by RFC 4571. One frame on stdio equals one datagram on the tunnel, so any program that reads and writes this framing exchanges packets through the tunnel without further adaptation.

# Producer side: one child process per accepted session
rstream nc -u -L rstrm://media -c "media-producer"
 
# Consumer side: stdio carries RFC 4571 frames
rstream nc -u rstrm://media
 
# Or run a local consumer with bidirectional framed stdio
rstream nc -u rstrm://media -c "media-consumer" --idle-timeout 60s

Datagram tunnels can also bridge local UDP sockets instead of stdio, which connects UDP-native applications without any framing concern. On the side that owns the tunnel, -L rstrm://<name> -R udp://host:port opens one connected UDP socket per accepted session toward a local service. On the dialing side, -L udp://host:port -R rstrm://<name> binds a local UDP socket and opens one tunnel session per local peer, with --udp-peer pinning the peer eagerly for receive-only applications that never send first. One UDP packet equals one tunnel datagram, and --framing does not apply to udp endpoints.

The Go runtime defaults to automatic transport selection. When it selects QUIC, datagram tunnels can use QUIC datagrams on the publishing leg and on the dialing leg. QUIC datagrams are congestion-controlled but never retransmitted, so delivery is not guaranteed and each datagram must fit the path MTU budget. A payload around 1200 bytes is a safe target, and oversized datagrams are dropped and logged without terminating the session. These are the semantics expected by loss-tolerant protocols such as RTP or SRT, where recovery belongs to the application layer. If automatic selection falls back to TLS, datagrams keep their message boundaries through stream framing.

Applications that need packet boundaries and guaranteed delivery can request it when creating the datagram tunnel. In the CLI, --datagram-guaranteed-delivery is valid with --datagram on commands that create a tunnel:

rstream forward 5300 --datagram --datagram-guaranteed-delivery --no-publish --name internal-dns
rstream nc -u -L rstrm://reliable-packets --datagram-guaranteed-delivery -c "packet-app"

With guaranteed delivery enabled, a QUIC tunnel transport does not use the unreliable QUIC datagram fast path for that tunnel. Datagram message boundaries are still preserved, but packets ride reliable streams so loss can increase latency. Leave the option unset for media protocols that already implement their own recovery.

Closing either side of a datagram channel closes the peer session as well. --idle-timeout handles a different case: it closes a session after no datagram has been received for the given duration. It is appropriate only when that side expects inbound packets. A send-only producer receives no traffic to refresh the deadline and must not set it. In datagram exec sessions the child's stderr goes to the local stderr rather than the connection, which keeps process logs out of the framed packet path.

SSH as a motivating example

SSH is a common example of a protocol that is not exposed as a first-class published tunnel endpoint. A private tunnel allows the SSH client to remain local while the rstream dialer carries traffic to the private environment.

A complete walkthrough for SSH, including ProxyCommand, rstream nc, and rstream run, is available in Access Remote Machines over SSH with rstream.

Transport and security notes

For private tunnels, transport configuration matters on both sides. The publishing client must maintain its outbound session to the engine, and each dialing client must also reach the engine under its own network constraints. Proxy settings, DNS override, address-family selection, and QUIC transport are documented in Tunnel Transports.

Private tunnels do not use public HTTP edge authentication because there is no public HTTP request path. Runtime access is controlled by the token or credential used by the dialing client and by any resources.tunnels boundaries attached to that credential. Project policies that forbid public tunnels still allow private tunnels when the project plan and token permissions allow tunnel creation.