Run your own relay
Tealhand Relay passes encrypted traffic between your phone and your computers when they cannot reach each other directly, for example when you are away from home. It is an iroh relay that only serves the devices on your own device list (roster). It holds no Tealhand key, stores only public data (owner public keys and signed device lists), and cannot sign or read anything.
This page is the reference for setting one up and running it. For a step-by-step install with commands, start at Install Tealhand Relay. For what a relay sees and keeps, read Relays.
What you need
Section titled “What you need”- A server with a public IP address (here
203.0.113.10, a documentation address) and, for a normal certificate, a DNS name pointing at it (hererelay.example.com). - A TLS certificate, as files. Either a certificate for that name, or, with no name, a self-signed certificate that your devices pin (below). ACME (Let’s Encrypt) is not built in: get the certificate with any ACME client and copy the files for the relay.
- Open ports: TCP 443 (the relay) and TCP 8443 (device list uploads), and UDP 7842 if you turn on QUIC address discovery. These are the numbers in the example; you choose them in the config.
1. Prepare the server
Section titled “1. Prepare the server”- Run the relay as a normal user, not root. Copy the certificate chain and the key into a folder that user
owns. Make the key readable by that user only:
chmod 600 privkey.pem. The relay refuses a key that group or others can read: at start, again every hour while running, and every 15 seconds while it is refused. While the key is refused it turns away TLS handshakes. - Renewal: let your ACME client’s deploy hook copy the renewed files to the same paths. The relay re-reads the certificate and key once a day, with no restart.
- The data folder (
data_dir) is created if it is missing, with mode0700. It holds one file per owner, named after the owner key and ending in.roster.
2. Write the config
Section titled “2. Write the config”Save this as /etc/tealhand-relay/config.toml and replace every placeholder:
# Listener addresses: required, no defaults, and never 0.0.0.0 or [::]. Write the public IP of this machine.relay_bind = "203.0.113.10:443" # HTTPS: the relay itselfroster_bind = "203.0.113.10:8443" # HTTPS: POST /tealhand/v1/roster# http_bind = "203.0.113.10:80" # optional: plain-HTTP captive-portal probe (absent: loopback only)# quic_bind = "203.0.113.10:7842" # optional: UDP QUIC address discovery (absent: off)
# Roster owner keys (the phone's `device` key). An empty list allows nobody.allowed_owners = ["thhw:REPLACE_WITH_THE_OWNER_KEY_66_HEX_DIGITS"]
data_dir = "/var/lib/tealhand-relay"
# max_connections_per_endpoint = 4 # optional# client_bytes_per_second = 8000000 # optional, per client connection
[tls]cert = "/etc/tealhand-relay/fullchain.pem"key = "/etc/tealhand-relay/privkey.pem" # must not be readable by group or others (0600)What each key does (types and defaults are in Relay config):
| Key | Required | What it does |
|---|---|---|
relay_bind |
Yes | Where the relay itself listens for HTTPS. Devices use the address https://relay.example.com |
roster_bind |
Yes | A separate HTTPS listener for POST /tealhand/v1/roster, where the phone uploads its device list. The relay serves uploads only here, not on relay_bind |
http_bind |
No | A plain-HTTP captive-portal probe. Leave it out (it then listens on loopback, on a port the system picks) or use a loopback address |
quic_bind |
No | UDP QUIC address discovery, which helps devices learn their public address. Absent: off. Devices expect port 7842 |
allowed_owners |
Yes | The owner keys whose device lists the relay accepts, each thhw: and 66 hex digits. An empty list allows nobody |
data_dir |
Yes | Where the stored device lists live |
max_connections_per_endpoint |
No | Most relay connections one device may hold at once. Default 4, at least 1 |
client_bytes_per_second |
No | Receive rate limit per client connection, in bytes per second. Default 8,000,000 |
[tls] cert |
Yes | The certificate chain file (PEM) |
[tls] key |
Yes | The private key file (PEM), mode 0600 |
Every listener address must be a real IP of the machine: 0.0.0.0, :: and their IPv4-mapped form are
refused, because they would listen on every interface. On a cloud server where the provider forwards the public
address to the machine, write the machine’s private IP. Two TCP listeners on the same address are refused.
Unknown keys are refused, with their name in the error.
The owner key is your phone’s device public key in its text form: thhw: followed by 66 lowercase hex
digits. The phone shows it in Settings, then Network, as Owner key, with a Copy button.
Certificates: a name, or a pinned fingerprint
Section titled “Certificates: a name, or a pinned fingerprint”With a DNS name, get a certificate with any ACME client (certbot’s standalone mode works) and let its deploy
hook copy fullchain.pem and privkey.pem to the paths in [tls]. Nothing else is needed on the phone or the
computers: they check the certificate against the public roots.
Without a DNS name, make a self-signed certificate for the IP address and pin it. Pinning means the phone and the computers accept only this one certificate, recognised by its SHA-256 fingerprint, with no name check (its dates are still checked), and refuse every other:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 825 \ -subj "/CN=tealhand-relay" -addext "subjectAltName=IP:203.0.113.10" \ -keyout privkey.pem -out cert.pemchmod 600 privkey.pemtealhand-relay fingerprint --config /etc/tealhand-relay/config.tomlThe last command prints sha256: and 64 hex digits (the relay also logs it at every start). Give it to the phone
in Certificate fingerprint, and to each computer with --relay-pin. A new certificate has a new fingerprint, so
pin the new one everywhere. Don’t pin a certificate that renews by itself. There is no switch that turns the
check off. The full steps, with the firewall and a service unit, are in
Install Tealhand Relay.
3. Start it
Section titled “3. Start it”tealhand-relay --config /etc/tealhand-relay/config.tomlAt start it logs where the source code of this version lives and the addresses it listens on. SIGTERM and
Ctrl-C stop it cleanly. A config error exits with code 2 and names the field, for example “relay_bind =
0.0.0.0:443 is an unspecified address (it would listen on every interface); write the public IP address of this
machine”. With an empty allowed_owners it starts, but logs “allowed_owners is empty: the relay will deny every
endpoint”.
Logs go to standard error. There is no access log. Set RUST_LOG to change the level (the default is
error,iroh_relay=error,tealhand_relay=info). When a device list loads, the log shows only the first characters
of the owner key and the version.
4. Point your devices at it
Section titled “4. Point your devices at it”- Computers: connect with
tealhand-host connect --relay https://relay.example.com. The choice is saved, andtealhand-host runuses it (the Tealhand app will get the same setting). Relay addresses must start withhttps://. With a self-signed certificate, add--relay-pin sha256:<64 hex digits>;connectsaves it too, andrunlogs “relay pinned”. - Phone: Settings, then Network, then My own relay. Fill in two addresses, both starting with
https://: the relay addresshttps://relay.example.comand the device list upload addresshttps://relay.example.com:8443/tealhand/v1/roster(theroster_bindaddress plus the path). With a self-signed certificate, also fill in Certificate fingerprint; otherwise leave it empty.
With your own relay, the phone and the computers publish nothing to public services, and the relay you configured always wins over a relay address from a QR code.
How your device list gets to the relay
Section titled “How your device list gets to the relay”The relay lets a device through only if a stored device list of an allowed owner lists it and its entry has not expired. Your phone keeps that list up to date:
- it uploads the signed device list on every app start, on a reconnect to a computer (at most every 10 minutes), and after every change (connecting, rename, removal);
- after a failed upload it does not upload again at the next reconnect. It tries again on one retry timer: after 1 minute, then 2, 4, and so on up to 10 minutes.
The relay stores an upload when its signature checks out, its owner is in allowed_owners, and its version is
higher than the stored one. Every 15 seconds, and after each upload, it disconnects devices that are no longer
listed or whose entry has expired.
| Answer | Body | Meaning |
|---|---|---|
| 200 | {"status":"stored"} |
Stored |
| 200 | {"status":"unchanged"} |
The same device list was already stored |
| 409 | {"status":"rollback","stored_version":N} |
The relay has version N, which is not lower. If N is at most 8 above the phone’s own version, the phone signs the current list again at N+1 and retries once (at most once per change or app run). If N is further ahead, the phone does not believe it: it keeps its list, uploads nothing more, and says the relay claims a newer device list (“RelayAhead”: check the relay, or connect again) |
| 403 | {"error":"unknown_owner"} |
The owner key is not in allowed_owners |
| 400 | {"error":"malformed"} or {"error":"bad_signature"} |
Not a valid signed device list |
| 413 | {"error":"too_large"} |
Over the size cap |
| 429 | {"error":"rate_limited"} |
Too many requests from this address |
| 404 | {"error":"not_found"} |
Wrong path |
| 405 | {"error":"method"} |
Not a POST |
| 500 | {"error":"storage"} |
The relay could not write the file |
Limits on the upload listener: 30 requests a minute per source (an IPv4 address, or an IPv6 /64), at most 6 open connections per source, 24 per IPv6 /48 and 256 in total, 10 seconds per connection, and a body of at most 32,896 bytes. The answers are fixed texts: nothing from the request is echoed.
The relay does not check the chain between versions (the devices check it themselves). So an old, validly signed device list could be replayed onto an empty relay until your phone uploads its current one, which it does on every start. The phone also sends every new device list to each computer directly, so computers never depend on the relay for it.
A reverse proxy in front
Section titled “A reverse proxy in front”The relay does TLS itself and is meant to face the internet directly. If you put a reverse proxy in front of the upload listener, its rate limit sees only the proxy’s address, so all uploads share one budget.
Removing an owner
Section titled “Removing an owner”Remove the key from allowed_owners and restart the relay. On start it ignores stored device lists of owners
that are no longer allowed, so their devices are no longer let through.
Licence
Section titled “Licence”Tealhand Relay is free software under the GNU AGPL-3.0-only, with no warranty. If you run a modified version
for other people over a network, section 13 of the licence requires you to offer them its source code. The relay
logs where the source of the running version lives when it starts, and tealhand-relay --help prints it.