Documentation
The telesh handbook
From install to a self-hosted server, this is the complete configuration and concept reference. The CLI is minimal — one data-directory flag — and everything else happens in the local UI.
Overview
telesh has two parts: an agent (telesh) that runs on the machine whose services you want to publish, and a self-hostable server (telesh-server). The agent dials out to the server, so machines behind NAT or a firewall do not need a public IP.
Publish local ports
Expose HTTP / TCP services on a public domain or port.
Mirror remote ports
Map a remote host's port to your machine over P2P.
Install
Both programs are single binaries with no runtime dependencies. Install from source with cargo, or download the build for your platform from Releases.
# Requires Rust 1.91+
cargo install --git https://github.com/zeaphoo/telesh telesh
cargo install --git https://github.com/zeaphoo/telesh telesh-serverQuick start
Set one line for the control server, start the agent, then pick the ports to publish in the local UI.
# 1. Set the control server (auto-generated if missing)
control_server = "tunnel.example.com:9009"
# 2. Start the long-running agent
$ telesh
info agent starting ui=127.0.0.1:16345
# 3. Open the local UI and publish the ports you wanthttp://127.0.0.1:16345. It only listens on the loopback interface and is never exposed.Data directory
Defaults to ~/.telesh and can be overridden with --dataroot. Identity, sessions and publish configuration all live here.
~/.telesh/
├── config.toml # control_server address
├── state # Ed25519 identity + cached session
├── publish.json # declarative publish config from the UI
└── traces/ # request logs, persisted per channelConfiguration
The only required field is the control server address. If the file is missing, a default one is generated (connecting to 127.0.0.1:9009); it only errors out when the file exists but lacks control_server.
# The only required field: the control server
control_server = "tunnel.example.com:9009"
# Host only default to port 9009
# A full ws://host:port/api/tunnel URL is also accepted
control_server = "ws://tunnel.example.com:9009/api/tunnel"Publish model
The publish configuration is declarative state: the process continuously reconciles actual runtime state toward it. Each entry has one of four states: pending, live, error, or target_gone (the local port disappeared).
[
{ "kind": "http", "local_port": 8000, "sub_domain": "app" },
{ "kind": "tcp", "local_port": 22, "remote_port": null },
{ "kind": "p2p", "local_port": 5432, "name": "db" },
{ "kind": "mirror", "local_port": 18000, "remote": "000012345/web" }
]httppublishes a local port on a fixed subdomain (automatic HTTPS).tcpmaps a local port to a public TCP port on the server.p2pexposes a local port directly over iroh for P2P peers.mirrormirrors a remoteaccount/nameto a local listening port.
Server deployment
A single TOML file starts the control plane, the data plane and an optional embedded iroh relay. The control port also serves the dashboard and /api/*.
# Data root: database, ACME cache, etc.
dataroot = "~/.telesh/server/"
# Where agents connect (also serves the dashboard)
control_addr = "0.0.0.0:9009"
# Public HTTP / TCP edge
ingress_addr = "0.0.0.0:8008"
# Data plane: agents connect to this
data_addr = "0.0.0.0:9008"
# Public suffix used to build tunnel URLs
public_host = "tunnel.example.com"
[relay]
# In-process iroh relay (optional); ACME issues certs
serve = "0.0.0.0:8081"
acme = ["relay.example.com"]
acme_email = "admin@example.com"# The only argument is the config file
$ telesh-server /etc/telesh/server.tomlArchitecture
Control plane
A WebSocket (JSON) connection carrying login, subdomain and TCP port registration, the P2P directory, keepalive and metering. Small messages only, no payload.
Data plane
A separate TCP connection made by the agent and multiplexed with yamux: a CBOR signalling stream plus one raw byte stream per public connection.
P2P data plane
Built on iroh (hole-punched QUIC). A peer resolves the directory, gets the address and connects directly, so the payload never touches the server; it falls back to an encrypted relay when a direct path fails.
Security
- Devices and accounts log in with an Ed25519 key pair via challenge–response, creating the account on first login (TOFU). Session tokens are stored as SHA-256 hashes with a fixed 12-hour lifetime.
- To reach a service, the server issues a short-lived opaque access token scoped to
account/name; the agent verifies it online, so the data plane holds no keys. - P2P connections use encrypted QUIC and authenticate the peer by its
EndpointId; relays are encrypted too.
Want more?
Source, design docs and issue tracking all live on GitHub.