Relay Client

link-relay-client is the one outbound, authenticated connection from this machine to the relay, and the Service interface the tunnel and remote plugins implement to answer its requests. It knows nothing about ports or models: a plugin hands it a Service. Its wire is the Protocol.

The connection

run exposes a service through the relay: it dials out, so no inbound port is needed, keeps the one connection alive, and turns every request stream the relay sends into a call to the service. It calls on_ready each time a connection is made, and returns only if the relay rejects the agent, such as for a bad token or an unsupported version.

Reconnecting

If the connection drops, it reconnects with backoff, at most 30 seconds apart, and asks for the same tunnel name, so the public URL survives. A connection's DNS, TCP, TLS and WebSocket upgrade take 10 seconds at most together, and each of a relay's addresses gets a 250 ms head start before the next is tried (RFC 8305).

AgentConfig

What a connection asks for: the relay's URL, its token and a TokenSource asked for a new one before a reconnect when it's near expiry or was refused, the app name, the client's version, the tunnel's password (basic_auth), the visitors it lets in (allow), a custom domain, and whether it waits as a standby. The agent refuses to serve if the relay doesn't confirm it enforces the password or the visitors, and stops if the relay doesn't confirm the domain.

Connected

What the relay said when the connection was made: the public URL, a newer client release if the relay knows of one, the operator's notice, whether the relay requires a password or lets in only allow's visitors, and where it serves the custom domain.

How much it serves at once

It serves up to 512 of the relay's requests at once. It writes up to 64 messages before a flush, passing them to the socket every 16 KiB, so on a slow uplink a small response isn't held behind a batch's worth of others.

The tunnel's Service interface

A plugin implements Service to answer what the relay sends: call takes an http::Request<Body> and answers an http::Response<Body>, and websocket, which answers 501 unless implemented, takes a visitor's WebSocket.

pub trait Service: Send + Sync + 'static {
    fn call(&self, req: http::Request<Body>) -> ResponseFuture<'_>; // -> Result<http::Response<Body>, BoxError>
    fn websocket(&self, req: http::Request<()>) -> WsFuture<'_> { /* 501 by default */ }
}

Requests and bodies

link-relay-client turns each relay stream into an http::Request and streams the returned body back chunk by chunk. The request body streams in as the visitor uploads it (read_body collects it); reading slowly is fine, since flow control makes the upload wait. A service never sees stream ids, the relay, or its WebSocket, and an Err becomes a 502 for the visitor.

Visitors' WebSockets

For a visitor's WebSocket, websocket returns WsUpgrade::Accepted with a WsSocket, a pair of message connections, or a normal response. websocket::connect bridges to a local ws:// server. exposes_an_in_process_service in tests/e2e is the smallest example.

What else it asks the relay

accounts asks a relay which account service it takes logins from (GET /_link/accounts), and the audience it takes relay tokens for: where lnk auth login signs this machine in. status asks it which of your tunnels are connected, from every machine, for lnk tunnel list. share is the relay's share API, for lnk bucket link create and a share's files for lnk bucket pull --link <url>.