tesera

tesera

tesera sends data between computers through relays. it encrypts the data, breaks it into pieces called tesserae, and sends them across more than one path. the receiver rebuilds the data once enough pieces arrive

tesera is experimental and hasn't been audited. don't rely on tesera yet for sensitive or critical data

these docs are a guide to using tesera. the README in the repository covers the same ground on one page

how it works

a transfer is split into blocks. each block is encrypted, then coded into redundant pieces called tesserae

by default tesera uses 2-of-3 coding. each block becomes 3 tesserae, and the receiver needs any 2 of them to rebuild the block

sender
  │
  ├── tessera 1 ── relay A ──┐
  ├── tessera 2 ── relay B ──┼── any 2 ── receiver
  └── tessera 3 ── relay C ──┘

if one tessera is lost or delayed, the receiver can continue once the other two arrive

a single coded piece is a tessera. the plural is tesserae

relays

a relay forwards encrypted tesserae between the sender and the receiver

anyone can run a relay. tesera's bootstrap relay at relay.tesera.net is a seed: other relays join it, and senders and receivers ask it which relays it knows. discovery covers how that works

why

erasure coding and multipath transmission already exist. tesera is experimenting with what happens when they become the normal way to move a stream of data

using several paths at once can raise useful throughput when the paths have independent capacity, and it lets a transfer continue when one path stops working

more paths aren't always better. paths can share a bottleneck, fail together, or be too slow to help. part of the work is learning when a path is worth using

install

the CLI and ordinary relays need Node.js 20 or newer. an attachment relay needs Node.js 20.17 or newer. nothing is published to npm yet, so you run tesera from a checkout of the repository

build

clone the repository, install the dependencies, and build the CLI

git clone https://github.com/teseralabs/tesera.git
cd tesera
npm ci
npm run build

check it works

this lists every command

npm run tesera -- --help

the examples in these docs run each command from the checkout with npm run tesera --

when an example saves a command's output in a shell variable, it uses npm run -s tesera --. -s keeps npm's own lines out of the variable

run the tests

you don't need the tests to use tesera. they're for working on tesera itself

npm test

this compiles the code, then runs the tests

send and receive

a transfer needs a sender, a receiver, and at least one relay. tesera doesn't traverse NAT yet, so the relays have to reach both computers on the UDP ports they listen on

by default, public relays only forward to public IPv4 addresses. destinations covers the rest

create a session secret

SESSION=$(npm run -s tesera -- session)

share it with the other side through a channel you already trust. tesera doesn't exchange it for you

anyone with the secret may be able to join the session, so treat it as private

start the receiver

replace HOST with the sender's IPv4 address

npm run tesera -- recv \
  --listen 0.0.0.0:4200 \
  --sender HOST:4300 \
  --discover relay.tesera.net \
  --session "$SESSION" \
  --output out.bin

start the sender

replace HOST with the receiver's IPv4 address

npm run tesera -- send \
  --listen 0.0.0.0:4300 \
  --receiver HOST:4200 \
  --discover relay.tesera.net \
  --session "$SESSION" \
  --input in.bin

choose relays

--discover asks one reachable seed for relays. use --relays instead to list every relay yourself

a pinned relay looks like this

relay:ID@host:port

tesera asks a pinned relay to prove its id before the transfer starts. --discover takes the same form, which checks the seed's id

--discover relay:es36gsfwxo2mcrmzl2neokxl6svtc4oowkb3j5xhcjgnxxyzkykq@relay.tesera.net

browser transfers

browsers run tesera through @tesera/client. the data is encrypted in the sending browser and decrypted in the receiving one. in between, relays carry it as tesserae

a browser can't send UDP, so it attaches to an attachment relay over WebTransport. that relay passes its tesserae on to ordinary UDP relays, which carry the coded paths as usual

sender's browser
  │ WebTransport
attachment relay
  ├── relay ──┐
  ├── relay ──┼── attachment relay
  └── relay ──┘     │ WebTransport
                  receiver's browser

both ends can use the same attachment relay or different ones. a transfer can lose one of its 3 coded paths, but if an end's attachment relay goes away, that end's transfer stops. tesera doesn't choose relays by operator or network yet, so the coded paths may share either

finding each other

the 2 browsers find relays and each other through a control plane, tesera api

  • the sender leaves an offer in a room. the offer names the relays and the transfer but holds no key
  • the receiver reads the offer, attaches to a relay, and answers. the first answer wins
  • the transfer then runs between the 2 browsers through the relays. the control plane never carries file data or the secret

the secret has to reach the receiver some other way. a link can carry it in its fragment, after #, which browsers never send to a server. anyone with the whole link can receive, so share it through a channel you already trust

both browsers have to stay open until the transfer finishes. closing either one stops it

browsers

browser transfers need WebTransport, and the page has to be served over HTTPS or from localhost

  • Chrome: tested on Chrome 154
  • Edge: uses the same engine as Chrome, but hasn't been tested
  • Firefox: tested on Firefox 130
  • Safari: tested on macOS and iOS, with a compatible attachment relay

tesera.net/transfer

the transfer page is one interface built this way. it sends files with a link that works once, for 10 minutes. its limits of 250 MB a file and 20 files a link belong to that page, not to tesera

getting started

help grow the tesera network by running your own relay

a relay listens for tesera traffic and forwards encrypted tesserae between senders and receivers

you can run a relay on your own computer or on a server. the steps are the same; only the networking differs

how a relay gets connected

for a relay you want tesera to discover and use automatically, you'll normally move through these stages

  1. running. the relay is listening on its machine
  2. joined. tesera's bootstrap relay knows about it and can introduce it through discovery. it also appears on the relays page
  3. reachable. computers elsewhere on the internet can send UDP to it, so transfers can use it

each stage is separate. a running relay may not have joined, and a joined relay that shows as online may still be unreachable from the internet. online only means the relay keeps reporting to the bootstrap. those reports leave from your relay, so they get through even when nothing outside can reach it

joining the bootstrap is optional. anyone can use a relay directly by its address with --relays

1. install

install tesera on the machine that will run the relay. install takes a few minutes

2. create an identity

npm run tesera -- id --out relay.secret

this prints your relay id, which starts with relay:. the id comes from the key saved in relay.secret

keep relay.secret private. anyone with it can act as your relay

start the relay with the same file every time so it keeps the same id. joining the bootstrap needs an identity. identity covers the details

3. start the relay

replace PUBLIC_IP with the public IPv4 address of the network this machine is on. networking shows how to find it

npm run tesera -- relay \
  --listen 0.0.0.0:4101 \
  --allow-remote \
  --identity relay.secret \
  --advertise PUBLIC_IP:4101 \
  --join relay:es36gsfwxo2mcrmzl2neokxl6svtc4oowkb3j5xhcjgnxxyzkykq@relay.tesera.net

the relay keeps running in this terminal. stop it with ctrl+c

  • --listen 0.0.0.0:4101 listens for UDP on port 4101 on every network interface of this machine. 4101 is tesera's usual relay port
  • --allow-remote lets the relay forward to other machines. without it, the relay only forwards on this machine and can't join a remote seed
  • --identity relay.secret gives the relay its id and signs its record
  • --advertise PUBLIC_IP:4101 is the address your relay claims in its signed record. the advertised address covers what it does and doesn't do
  • --join relay:ID@relay.tesera.net joins tesera's bootstrap relay. the id before @ pins it, so your relay refuses anything at that name with a different id

--name TEXT is optional. it adds a name of up to 64 bytes that the relays page shows

you don't need --access open to forward transfers. add it only if you want other relays to be able to join yours as a seed. operator policy covers access and limits

4. check that it joined

within a few seconds you should see

15:44:31.377 role=relay event=record seq=1
15:44:31.379 role=relay event=listen addr=0.0.0.0:4101
15:44:31.379 role=relay event=id id=relay:...
15:44:31.384 role=relay event=joined addr=104.207.95.64:4101 seed=relay:es36gsfwxo2mcrmzl2neokxl6svtc4oowkb3j5xhcjgnxxyzkykq

event=joined means the bootstrap now lists your relay, and it appears on the relays page

if the relay can't join within about a minute, it prints why and exits. troubleshooting covers each message

5. make it reachable

joining shows that your relay can reach the bootstrap. senders and receivers also have to reach your relay on UDP port 4101. allow it through the firewall, and forward it on your router if you're at home. networking covers each case, including when port forwarding isn't possible

networking

tesera relays use UDP. the usual port is 4101

a relay that starts without errors is listening on its machine. that doesn't show that another computer on the internet can reach it

home or local machine

  • allow UDP 4101 through the operating system's firewall. macOS and Windows may ask whether Node.js can accept incoming connections
  • most home networks sit behind a router that does NAT. forward UDP port 4101 on the router to the computer running tesera
  • give that computer a fixed LAN address, or a DHCP reservation on the router, so the forward keeps pointing at it
  • some internet providers use CGNAT or don't give you a public IPv4 address. router port forwarding won't work there. a cloud server or a plan with a public IPv4 address will

your router's status page shows its internet address. that's usually your PUBLIC_IP

on Linux with ufw

sudo ufw allow 4101/udp

on Linux with firewalld

sudo firewall-cmd --permanent --add-port=4101/udp
sudo firewall-cmd --reload

check for CGNAT

check the internet or WAN address on your router's status page. if it falls in a private or shared range such as 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, or 100.64.0.0/10, your provider may be using CGNAT or another upstream NAT

VPS or cloud server

  • allow inbound UDP 4101 in the provider's firewall or security group
  • allow it in the server's own firewall too, if one is on
  • use the public IPv4 address from the provider's dashboard, even when the server itself only shows a private one

only the UDP relay port needs to be public. the relay's HTTP API is off unless you add --api. if you add it, keep it on 127.0.0.1

the advertised address

--advertise PUBLIC_IP:4101 puts an address in your relay's signed record. it's a claim your relay makes about where it can be reached

  • it doesn't open a port or make the relay reachable
  • it doesn't choose the address discovery gives out. the bootstrap uses the address your relay's packets arrive from
  • it takes an IPv4 address and a port. hostnames aren't accepted
  • you can repeat it to claim more than one address
  • tesera info checks that the address you test is one of them

if your public address changes, update --advertise and restart the relay

the listed address

your relay's card on the relays page shows 2 addresses. address is where the bootstrap hears your relay from, and it's the one discovery gives to senders and receivers. advertised address is what your record claims

if the listed and advertised addresses differ, check your network configuration and --advertise value

attachment relays

a browser can't send UDP, so it attaches to an attachment relay over WebTransport, and that relay passes its tesserae on to ordinary relays. an ordinary relay needs none of this

npm run tesera -- relay \
  --listen 0.0.0.0:4101 \
  --allow-remote \
  --identity relay.secret \
  --advertise PUBLIC_IP:4101 \
  --webtransport :4433 \
  --api 127.0.0.1:4180
  • --webtransport :4433 accepts attachments on UDP port 4433. open it in the firewall too
  • --identity is required, because the relay signs a statement of its WebTransport certificate with it
  • --advertise is required when the relay listens on every interface
  • --api serves that statement at /v1/transports for a control plane to read. keep it on 127.0.0.1 or a private address
  • an attachment relay forwards everything its browsers send, so give it a higher --bandwidth than the default

certificates

without certificate flags, the relay makes its own short-lived self-signed certificates with openssl and rotates them

to use your own certificate, pass both

--webtransport-cert cert.pem \
--webtransport-key key.pem

requirements

  • Node.js 20.17 or newer
  • the optional WebTransport packages, which npm ci installs

those packages have prebuilt binaries for Linux with glibc 2.38 or newer, such as Ubuntu 24.04 or Debian 13, macOS 26 or newer on arm64, macOS 15 or newer on x64, and Windows on x64. on an older glibc, the binary installs but can't load. anywhere else they build from source, which needs git, CMake, and a C++ compiler

safari needs an attachment relay whose WebTransport implementation advertises one session per connection. the published @fails-components/webtransport-transport-http3-quiche 1.6.8 binaries don't support this yet

if the packages are missing or the listener can't start, the relay exits with an error

getting listed

an attachment relay only serves browsers once a control plane lists it. tesera api --entries FILE takes the list, and the README shows the file

verify your relay

these checks start on the relay's machine and move outward. only the last one tests whether the internet can reach your relay

  1. running. the relay printed event=listen and is still running
  2. identity. it printed event=id with your relay id
  3. joined. it printed event=joined with seed=relay:es36gsfwxo2mcrmzl2neokxl6svtc4oowkb3j5xhcjgnxxyzkykq
  4. listed. your id appears on the relays page as online
  5. record. your relay's card shows its software and advertised address. the advertised address should be your public address
  6. reachable. tesera info passes from a computer on another network

online on the relays page isn't the same as reachable. how a relay gets connected explains the difference

check from another network

npm run tesera -- info relay:YOUR_ID@PUBLIC_IP:4101

run this on a computer on a different network, such as a cloud server or a laptop on a phone hotspot. a check from the same machine or the same LAN can pass even when the internet can't reach you

a relay that passes prints

id=relay:...
signature=valid
pinned=match
fresh=yes
reachable=yes
advertised=yes

followed by the fields from its record

  • signature means the record is signed by the key it names
  • pinned means that key is the id you asked for
  • fresh means the record hasn't expired. the relay renews it while it runs
  • reachable means the relay answered a challenge from that address and proved it holds the key
  • advertised means that address is in the record

tesera info exits with 1 if any check fails. a relay started without --advertise, or checked at an address it doesn't advertise, fails advertised even when it's reachable

if nothing answers at that address, it prints relay PUBLIC_IP:4101 did not return a record instead

your relay is ready when it has joined the bootstrap and tesera info passes from another network. a pass from one network is a strong sign, but it doesn't promise that every network can reach you

the relay API

a relay started with --api HOST:PORT serves information about itself over HTTP

GET /v1/stats
GET /v1/relay
GET /v1/peers
GET /v1/record
GET /v1/transports

they are for you and a control plane, not for browsers, so they send no CORS headers. keep --api on 127.0.0.1 or a private address

keep it running

this is optional. on Linux, systemd can start the relay at boot and restart it after a failure

the repository has 2 sample units. use deploy/tesera-relay.service. it runs as user tesera from /opt/tesera, keeps its files in /var/lib/tesera, and joins tesera's bootstrap relay with its id pinned

deploy/tesera-seed.service is for running a seed that other relays join. it accepts joins from any relay. don't install it for an ordinary relay

install tesera

the checkout belongs to your own user, so npm never runs as root. the relay runs as tesera, which only reads it

sudo useradd --system --home-dir /var/lib/tesera --shell /usr/sbin/nologin tesera
sudo install -d -o tesera -g tesera -m 700 /var/lib/tesera
sudo install -d -o "$USER" -m 755 /opt/tesera
git clone https://github.com/teseralabs/tesera.git /opt/tesera
cd /opt/tesera
npm ci
npm run build

create its identity

sudo -u tesera node /opt/tesera/dist/src/cli.js id --out /var/lib/tesera/relay.secret

to keep the id you already have, copy your relay.secret there instead, along with relay.secret.record if it exists

sudo cp relay.secret relay.secret.record /var/lib/tesera/
sudo chown tesera:tesera /var/lib/tesera/relay.secret /var/lib/tesera/relay.secret.record
sudo chmod 600 /var/lib/tesera/relay.secret

install the unit

sudo cp /opt/tesera/deploy/tesera-relay.service /etc/systemd/system/
sudo nano /etc/systemd/system/tesera-relay.service

add --advertise PUBLIC_IP:4101 to the end of the ExecStart line, then save

the unit runs /usr/bin/node. if command -v node prints another path, change it in ExecStart too

sudo systemctl daemon-reload
sudo systemctl enable --now tesera-relay
journalctl -u tesera-relay -f

what it does

  • if the relay stops with an error, systemd starts it again 15 seconds later. that includes a startup join that didn't finish within about a minute
  • --metrics-file keeps the bytes and transfers your relay reports across restarts

update

cd /opt/tesera
git pull
npm ci
npm run build
sudo systemctl restart tesera-relay

on macOS, Windows, or a system without systemd, keep the relay running in a terminal or use your system's own service manager

troubleshooting

the relay exits right after it starts

the last line says why

  • --join needs --identity means you need to create an identity and pass --identity relay.secret
  • --join needs --allow-remote, because a relay without it cannot answer a remote seed means you need to add --allow-remote
  • a signed record needs --identity means --advertise and --name need an identity
  • tesera is IPv4 only, got ... means --advertise needs an IPv4 address. a hostname won't work
  • bind EADDRINUSE 0.0.0.0:4101 means something else is using port 4101
  • relay did not join relay.tesera.net:4101 within 60s: the seed did not answer means this machine couldn't reach the bootstrap over UDP. check that outgoing UDP is allowed
  • ... the seed answered but did not list this relay means the bootstrap heard your relay but didn't add it. try again later, and open an issue if it keeps happening
  • seed ... is relay:..., not the pinned relay:... means the id in --join doesn't match. copy the command from run a relay again

it doesn't appear in discovery

  • look for event=joined in its output. without it, the relay hasn't joined
  • check that the command has --allow-remote, --identity, and the exact --join value from run a relay

it shows online but transfers can't use it

online doesn't mean reachable. how a relay gets connected explains why

  • check the firewall and port forward in networking
  • check for CGNAT
  • compare address and advertised address on its card
  • run tesera info from another network

tesera info fails

  • did not return a record means nothing answered. check that the relay is running with --allow-remote and that the firewall and port forward let UDP 4101 in
  • advertised=no means the address you checked isn't in the record. start the relay with --advertise set to that address
  • pinned=mismatch means a different relay is at that address
  • reachable=no means the record came back but the key proof didn't. check the id and try again
  • fresh=no means the record looks expired. check the clock on both computers, then restart the relay
  • signature=invalid means the record is damaged or forged. don't rely on that relay

it disappeared after the bootstrap restarted

your relay joins again on its own and keeps running meanwhile. its output shows event=rejoin and then event=joined

an attachment relay won't start

  • --webtransport needs --identity means you need to pass --identity relay.secret
  • --webtransport on a relay listening on every interface needs --advertise HOST:PORT means you need to add --advertise
  • --webtransport needs the optional ... package, which failed to load means the WebTransport packages are missing or can't load. check the requirements, then run npm ci again and read its output
  • could not generate a self-signed certificate means openssl isn't installed. install it, or pass --webtransport-cert and --webtransport-key

transfers through it are slow

a new relay forwards up to 5 megabits a second. event=limited in its output means a cap is dropping traffic. operator policy shows how to raise it

a test between computers on my LAN fails

with --allow-remote, a relay refuses to forward to private addresses like 192.168.x.x. add the range for a local test

--allow-dest 192.168.0.0/16

@tesera/client

@tesera/client is the tesera client for applications. it sends and receives files and streams over tesera from browsers and Node.js, and encrypts on the device

transports sit beneath the client. in a browser, the current transport is WebTransport to an attachment relay. Node.js 20 or newer needs a WebTransport implementation passed in, such as @fails-components/webtransport

it finds relays and the other endpoint through a control plane, tesera api

install

it isn't on npm yet. build it from a checkout of the repository

git clone https://github.com/teseralabs/tesera.git
cd tesera
npm ci
npm run build
cd packages/client
npm ci
npm run build
npm pack

then install the file npm pack made, from your application's directory

npm install /path/to/tesera/packages/client/tesera-client-0.1.1-beta.tgz

the README covers sending, receiving, sinks, transports, and errors

api

tesera api is the control plane. applications using @tesera/client use it for 2 things: relay discovery, and letting the 2 endpoints of a transfer find each other

run it

npm run tesera -- api \
  --listen 127.0.0.1:4190 \
  --discover relay.tesera.net \
  --entries entries.json
  • --discover is the seed whose relays it lists. relay.tesera.net is the default
  • --entries FILE lists the attachment relays that /v1/relays offers. without it, browsers have nothing to attach to. the README shows the file

keep the API on 127.0.0.1 unless you mean to expose it. behind a proxy on the same machine, --trust-proxy limits by the forwarded address

endpoints

GET    /
GET    /v1/relays
POST   /v1/rooms
GET    /v1/rooms/ROOM
DELETE /v1/rooms/ROOM
POST   /v1/rooms/ROOM/answer
GET    /v1/rooms/ROOM/answer

the control plane never carries file data, and it never gets the secret. the data goes between the 2 devices and the relays. an application that wants to send uses @tesera/client, which encrypts on the device. it calls these endpoints for you

public instance

tesera.net runs a public instance at api.tesera.net, as a beta with no availability promise. it doesn't need an account or an API key. the examples below run against it from this page. nothing is sent until you press run

GET /

describes the API, its version, and its endpoints

GET https://api.tesera.net/

GET /v1/relays

discovery: the UDP relays it has confirmed, and the attachment relays it lists, each with a WebTransport statement the relay signed. verify each relay's statement before you rely on it

GET https://api.tesera.net/v1/relays

rooms

a room holds one offer and one answer, so the 2 endpoints of a transfer can find each other. it lasts 10 minutes. the offer and the answer are JSON objects of up to 4 KB, with v of 1 or more and a 32-hex sessionId. the rest is up to the endpoints, and the control plane keeps the exact text it was sent

the examples below share one room. open it first, and the rest fill in its id and token. try them in order, then run them again out of order to see the errors

POST /v1/rooms

the sender opens a room with its offer. it returns 201 with the room, a token, and expiresAt in unix seconds. keep the token on the sender. it reads the answer and closes the room

POST https://api.tesera.net/v1/rooms

{"v":1,"sessionId":"SESSION"}

GET /v1/rooms/ROOM

the receiver reads the offer. once someone has answered, it returns 409 answered

GET https://api.tesera.net/v1/rooms/ROOM

GET /v1/rooms/ROOM/answer

the sender reads the answer. wait holds the request open for up to that many seconds, at most 25. it returns the answer with 200, or 204 if none came in time. the answer stays readable until the sender closes the room

GET https://api.tesera.net/v1/rooms/ROOM/answer?wait=5
Authorization: Bearer TOKEN

POST /v1/rooms/ROOM/answer

the receiver answers with the offer's sessionId. it returns 204. the first answer wins. sending the same answer again is accepted, so a receiver that lost the reply can retry. a different answer gets 409 answered, and one for another session gets 409 session

POST https://api.tesera.net/v1/rooms/ROOM/answer

{"v":1,"sessionId":"SESSION"}

DELETE /v1/rooms/ROOM

the sender closes the room. it returns 204

DELETE https://api.tesera.net/v1/rooms/ROOM
Authorization: Bearer TOKEN

errors

a failed request returns JSON with an error code

{"error":"not_found"}
  • not_found: the room is unknown, closed, or expired
  • answered: another receiver answered first
  • session: the answer is for a different session
  • token: the bearer token is missing or wrong
  • document: the body isn't a valid offer or answer
  • size: the body is too large
  • busy: the control plane holds as many rooms as it allows
  • rate_limit: the caller is over a limit

limits

tesera api limits each IP

  • 120 requests a minute
  • 10 new rooms a minute

a caller over a limit gets HTTP 429 with {"error":"rate_limit"}, and Retry-After when the wait is known

identity

a relay identity is a key pair. the relay id starts with relay: and is made from the public key. the secret stays in the identity file

npm run tesera -- id --out relay.secret

--out writes the secret so only your user can read it, then prints the id. without --out, the secret is printed to the terminal

--identity takes that file. a raw 64-character secret also works, but it can show up in shell history or the process list

what needs one

  • --join
  • --access open
  • --advertise, --name, --record-file, and --record-ttl, which make up a signed record
  • --webtransport, which signs a statement of its certificate
  • --allow, --block, and --policy-file
  • checking a relay with tesera info, or pinning it as relay:ID@host:port

a relay without an identity still forwards. others can use it by address with --relays HOST:4101, without a pinned id

keeping it

the same file keeps the same id. a new file makes a new id, and the bootstrap and the relays page treat it as a different relay

anyone with your identity file can act as your relay. if it leaks, stop the relay and create a new identity

discovery

a seed is a relay that other relays join. senders and receivers ask it which relays it knows. tesera's bootstrap relay at relay.tesera.net is a public seed

join a seed

--join takes a name, an address, or either one with a pinned id

--join relay:ID@relay.tesera.net
--join relay.tesera.net
--join 203.0.113.10:4101

a name without a port uses 4101. relay:ID@ pins the seed: your relay checks the id that signed the seed's relay table and refuses any other. without it, your relay joins whichever seed answers at that address

a seed accepts the join when it runs with --access open, or when it has allowed your relay's id

staying joined

  • at startup the relay keeps trying to join. after about a minute without success, it exits with 1
  • a pinned seed that answers with a different id stops startup at once
  • once joined, the relay keeps reporting its traffic to the seed. if the seed stops listing it, it joins again

how a seed lists relays

a seed lists each relay at the address its join came from. a relay counts as online while it keeps reporting. when the reports stop, it's offline and leaves discovery

a seed remembers an offline relay, so it's listed again as soon as it reports. --peer-ttl sets how long, 30 days by default. a seed keeps up to 31 joined relays

find relays

--discover asks a seed for itself and the relays joined to it. discovery goes one hop

npm run tesera -- stats --via relay.tesera.net

the relays page lists the relays tesera's bootstrap relay knows right now, including offline ones it still remembers

relay records

a relay with an identity signs a small record about itself: its id, advertised addresses, implementation, software version, wire version, capabilities, an optional name, a sequence number, when it was signed, and how long it stays fresh

what the signature proves

a valid signature proves which relay identity produced the record

it doesn't prove that the claims inside are true. these are all reported by the relay itself

  • software version
  • implementation
  • name
  • advertised addresses
  • capabilities

it also doesn't prove the relay runs an official or unmodified tesera build. don't base compatibility or security decisions on a reported software version alone

name

--name is optional, up to 64 bytes, and isn't checked for uniqueness. the relay id remains the identity

sequence and freshness

  • each new record gets a higher sequence number. the relay keeps the last record beside its identity as relay.secret.record, and --record-file picks another path
  • a record stays fresh for 1 day by default, which --record-ttl SECONDS changes. the relay renews it while it runs
  • deleting the record file starts the sequence over at 1. anyone who already saw a higher number keeps the old record, so the fix is a new identity

inspect a record

tesera info fetches a record and checks it. verify your relay explains each check. --json prints the signed packet and its decoded fields

a relay started with --api also returns its record at GET /v1/record. a program that relies on it should verify packet and use the fields decoded from it

operator policy

policy decides which relays may join yours and how much your relay will forward

who may join

--access controls which other relays may join this relay. it doesn't decide who can send transfers through it

  • --access private is the default. only relays you've allowed may join
  • --access open accepts any relay that proves its identity, unless you've blocked it. it needs --identity

a relay only needs --access open if you want other relays to be able to join it

--access open doesn't turn off any of the limits below

npm run tesera -- allow relay:ID --policy-file policy.json
npm run tesera -- block relay:ID --policy-file policy.json
npm run tesera -- unblock relay:ID --policy-file policy.json
npm run tesera -- forget relay:ID --policy-file policy.json
  • allow lets a relay join a private relay
  • block refuses its joins and stops forwarding traffic from where it was last seen
  • unblock removes a block
  • forget drops a remembered relay without blocking it

these apply to relay ids, wherever the relay shows up. --policy-file keeps them across restarts, and a running relay picks up changes within about a second. --allow relay:ID and --block relay:ID do the same at startup

a relay refuses --allow, --block, and --policy-file without --identity, because they decide who may join, and only a relay with an identity can answer joins

limits

every relay starts with these limits

bandwidth        5 megabits per second
sessions         8
new peers        6 per minute
datagrams        2000 per second

they're resource and safety caps. they stay on for open and private relays alike until you change them with flags, and the policy file doesn't hold them

  • bandwidth counts forwarded data and control traffic alike. it's one limit shared by every transfer through the relay. a bare number is megabits, and 500kbps or 50mbps also work
  • sessions is how many transfers can use the relay at once. a session that's idle for 30 seconds frees its slot
  • new peers is how many new relays may join per minute
  • datagrams counts every forwarded datagram, including acknowledgements
--bandwidth 50mbps \
--max-sessions 32 \
--datagram-rate 0

0 removes a limit

when a cap drops traffic, senders treat it as loss and slow down. restrictive limits can make transfers slower. the relay prints event=limited while it happens, and logs explains it

destinations

with --allow-remote, a relay forwards to public IPv4 addresses. private, local, reserved, and other special-use ranges are refused

0.0.0.0/8         10.0.0.0/8        100.64.0.0/10
127.0.0.0/8       169.254.0.0/16    172.16.0.0/12
192.0.0.0/24      192.0.2.0/24      192.168.0.0/16
198.18.0.0/15     198.51.100.0/24   203.0.113.0/24
224.0.0.0/4       240.0.0.0/4       255.255.255.255/32

--allow-dest allows one of those ranges, for example a tailnet. it can be repeated

--allow-dest 100.64.0.0/10

the --join address is chosen by you and isn't filtered

logs

a relay prints one line per event

HH:MM:SS.mmm role=relay event=<event> key=value

--log-level info is the default. error prints only failures, and debug adds a line for each forwarded block

eventmeaning
listenthe relay is listening
idthe relay's id
joinedthe seed lists this relay. printed again after each rejoin
rejoin reason=missingthe seed answered without this relay, so it's joining again
rejoin reason=unreachablethe seed didn't answer, so it's joining again
error reason=joinone join attempt failed
error reason=pinnedthe seed answered with a different id than the pinned one
limiteda cap dropped traffic since the last line
join, online, offlineon a seed, a relay joined, came back, or went quiet
donetotals when the relay stops

limited

event=limited appears while a cap drops traffic. each field counts drops since the last line

12:00:31.000 role=relay event=limited session=0 datagram=412 bandwidth=0 destination=0 table=0
  • session means --max-sessions was full
  • datagram means --datagram-rate ran out
  • bandwidth means --bandwidth ran out
  • destination means an address that hasn't replied used up its allowance
  • table means too many relay-table lookups were waiting

with --api 127.0.0.1:4180, GET /v1/relay returns the same split as limitedBy, alongside this process's other counters

curl http://127.0.0.1:4180/v1/relay
↑↓ move enter open