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
- running. the relay is listening on its machine
- joined. tesera's bootstrap relay knows about it and can introduce it through discovery. it also appears on the relays page
- 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:4101listens for UDP on port 4101 on every network interface of this machine. 4101 is tesera's usual relay port--allow-remotelets the relay forward to other machines. without it, the relay only forwards on this machine and can't join a remote seed--identity relay.secretgives the relay its id and signs its record--advertise PUBLIC_IP:4101is 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.netjoins 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 infochecks 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 :4433accepts attachments on UDP port 4433. open it in the firewall too--identityis required, because the relay signs a statement of its WebTransport certificate with it--advertiseis required when the relay listens on every interface--apiserves that statement at/v1/transportsfor a control plane to read. keep it on127.0.0.1or a private address- an attachment relay forwards everything its browsers send, so give it a higher
--bandwidththan 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 ciinstalls
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
- running. the relay printed
event=listenand is still running - identity. it printed
event=idwith your relay id - joined. it printed
event=joinedwithseed=relay:es36gsfwxo2mcrmzl2neokxl6svtc4oowkb3j5xhcjgnxxyzkykq - listed. your id appears on the relays page as online
- record. your relay's card shows its software and advertised address. the advertised address should be your public address
- reachable.
tesera infopasses 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
signaturemeans the record is signed by the key it namespinnedmeans that key is the id you asked forfreshmeans the record hasn't expired. the relay renews it while it runsreachablemeans the relay answered a challenge from that address and proved it holds the keyadvertisedmeans 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-filekeeps 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 --identitymeans you need to create an identity and pass--identity relay.secret--join needs --allow-remote, because a relay without it cannot answer a remote seedmeans you need to add--allow-remotea signed record needs --identitymeans--advertiseand--nameneed an identitytesera is IPv4 only, got ...means--advertiseneeds an IPv4 address. a hostname won't workbind EADDRINUSE 0.0.0.0:4101means something else is using port 4101relay did not join relay.tesera.net:4101 within 60s: the seed did not answermeans this machine couldn't reach the bootstrap over UDP. check that outgoing UDP is allowed... the seed answered but did not list this relaymeans the bootstrap heard your relay but didn't add it. try again later, and open an issue if it keeps happeningseed ... is relay:..., not the pinned relay:...means the id in--joindoesn't match. copy the command from run a relay again
it doesn't appear in discovery
- look for
event=joinedin its output. without it, the relay hasn't joined - check that the command has
--allow-remote,--identity, and the exact--joinvalue 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 recordmeans nothing answered. check that the relay is running with--allow-remoteand that the firewall and port forward let UDP 4101 inadvertised=nomeans the address you checked isn't in the record. start the relay with--advertiseset to that addresspinned=mismatchmeans a different relay is at that addressreachable=nomeans the record came back but the key proof didn't. check the id and try againfresh=nomeans the record looks expired. check the clock on both computers, then restart the relaysignature=invalidmeans 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 --identitymeans you need to pass--identity relay.secret--webtransport on a relay listening on every interface needs --advertise HOST:PORTmeans you need to add--advertise--webtransport needs the optional ... package, which failed to loadmeans the WebTransport packages are missing or can't load. check the requirements, then runnpm ciagain and read its outputcould not generate a self-signed certificatemeansopensslisn't installed. install it, or pass--webtransport-certand--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
--discoveris the seed whose relays it lists. relay.tesera.net is the default--entries FILElists the attachment relays that/v1/relaysoffers. 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 expiredanswered: another receiver answered firstsession: the answer is for a different sessiontoken: the bearer token is missing or wrongdocument: the body isn't a valid offer or answersize: the body is too largebusy: the control plane holds as many rooms as it allowsrate_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 asrelay: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-filepicks another path - a record stays fresh for 1 day by default, which
--record-ttl SECONDSchanges. 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 privateis the default. only relays you've allowed may join--access openaccepts 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
allowlets a relay join a private relayblockrefuses its joins and stops forwarding traffic from where it was last seenunblockremoves a blockforgetdrops 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
500kbpsor50mbpsalso 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
| event | meaning |
|---|---|
listen | the relay is listening |
id | the relay's id |
joined | the seed lists this relay. printed again after each rejoin |
rejoin reason=missing | the seed answered without this relay, so it's joining again |
rejoin reason=unreachable | the seed didn't answer, so it's joining again |
error reason=join | one join attempt failed |
error reason=pinned | the seed answered with a different id than the pinned one |
limited | a cap dropped traffic since the last line |
join, online, offline | on a seed, a relay joined, came back, or went quiet |
done | totals 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
sessionmeans--max-sessionswas fulldatagrammeans--datagram-rateran outbandwidthmeans--bandwidthran outdestinationmeans an address that hasn't replied used up its allowancetablemeans 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