Skip to content

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.

  • 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 (here relay.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.
  • 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 mode 0700. It holds one file per owner, named after the owner key and ending in .roster.

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 itself
roster_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:

Terminal window
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.pem
chmod 600 privkey.pem
tealhand-relay fingerprint --config /etc/tealhand-relay/config.toml

The 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.

Terminal window
tealhand-relay --config /etc/tealhand-relay/config.toml

At 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.

  • Computers: connect with tealhand-host connect --relay https://relay.example.com. The choice is saved, and tealhand-host run uses it (the Tealhand app will get the same setting). Relay addresses must start with https://. With a self-signed certificate, add --relay-pin sha256:<64 hex digits>; connect saves it too, and run logs “relay pinned”.
  • Phone: Settings, then Network, then My own relay. Fill in two addresses, both starting with https://: the relay address https://relay.example.com and the device list upload address https://relay.example.com:8443/tealhand/v1/roster (the roster_bind address 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.

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.

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.

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.

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.