Skip to content
telesh

Theme

telesh
FeaturesHow it worksUse casesQuick startCompareDocsGitHub

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.

terminal
# Requires Rust 1.91+
cargo install --git https://github.com/zeaphoo/telesh telesh
cargo install --git https://github.com/zeaphoo/telesh telesh-server

Quick start

Set one line for the control server, start the agent, then pick the ports to publish in the local UI.

terminal
# 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 want
The local UI is fixed at http://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.

layout
~/.telesh/
├── config.toml   # control_server address
├── state         # Ed25519 identity + cached session
├── publish.json  # declarative publish config from the UI
└── traces/       # request logs, persisted per channel

Configuration

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.

~/.telesh/config.toml
# 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).

~/.telesh/publish.json
[
  { "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" }
]
  • http publishes a local port on a fixed subdomain (automatic HTTPS).
  • tcp maps a local port to a public TCP port on the server.
  • p2p exposes a local port directly over iroh for P2P peers.
  • mirror mirrors a remote account/name to 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/*.

/etc/telesh/server.toml
# 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"
terminal
# The only argument is the config file
$ telesh-server /etc/telesh/server.toml

Architecture

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.

Open the repository