Security model
HKDF + AES-256-GCM protect signalling envelopes from the gate.
Freehop assumes that every server in the middle is curious and some are hostile. Gates only ever see sealed envelopes, gateways only relay encrypted media for people they issued keys to, and membership comes from your backend, not from any server Freehop talks to.
Keys
Each room starts with a room secret of at least 128 bits, created by your backend's authority (32 random bytes by default). Members receive it inside their ticket, over your own authenticated channel. The app does not display it, but every member can extract and share their ticket. Treat room members as trusted.
From the secret, every member derives two values with HKDF-SHA256:
| Value | Derivation | Used for |
|---|---|---|
| Room tag | HKDF(secret, salt, info "room-tag"), 32 bytes, base64url | The only room identifier a gate ever sees |
| Envelope key | HKDF(secret, salt, info "envelope-key") | AES-256-GCM key for every signalling envelope |
The salt is peerlane/v1/ followed by your app name, so two applications never share tags or keys. "Peerlane" is Freehop's codename, which is why it appears in wire identifiers.
Sealed envelopes
Every signalling message is sealed before it leaves the browser:
box = base64url( iv[12] || AES-256-GCM(envelopeKey, iv, plaintext, aad) )
aad = "peerlane/v1|" + roomTag + "|" + from + "|" + to
The additional data binds the room, the sender and the recipient. If a gate re-labels the sender or re-routes an envelope to someone else, it fails authentication and is dropped. Each sender also numbers its envelopes, and receivers keep a 1024-wide replay window per sender, so replayed envelopes are discarded. Try tampering in the gate demo.
Who learns what
| Party | Learns | Cannot |
|---|---|---|
| Gate | Socket IP addresses, opaque room tags, random peer ids, envelope sizes and timing | Read SDP, candidates, capabilities or credentials; forge or re-route envelopes undetected; add members to a room |
| Gateway owner (a desktop participant or host node) | Which peers relay through it, and how much | Read the media: it stays DTLS-SRTP encrypted end to end, and the gateway relays ciphertext |
| Forwarding participant | The media it forwards | Nothing new: it is in the call and already receives both streams |
Application TURN relay (only with the opt-in turn) | IP addresses, timing and sizes of the encrypted packets of the pairs it carries | Read the media: it stays DTLS-SRTP encrypted end to end |
| Room member | Network addresses on direct paths, the room's shared key and, with classifyNat, each member's NAT type | Authenticate to the new epoch after a kick, provided every remaining member and host updates its ticket |
A hostile gate can drop or delay traffic. Several gates in each ticket mitigate that: clients use all of them, and envelopes arrive through any route that works.
Membership comes from envelopes, not rosters
A gate's list of peers in a room is only a hint. A hint makes a client greet that peer with its sealed capabilities, at most once every 20 seconds and for at most 64 hints. The peer becomes a member, with peer events, a link and a slot, only after an envelope from it authenticates. A gate cannot authenticate an invented member. It can still prevent discovery or disrupt recovery by hiding or dropping traffic; roster presence alone is not proof of membership.
Tickets
A ticket is { v, app, roomId, epoch, gates, secret, auth?, stun?, turn?, expires }. It carries the room secret, so it is a bearer credential: anyone holding it can join the room. With the opt-in turn it also carries credentials for your TURN relay; mint short-lived ones per ticket. Deliver it only to the member it was issued for, over your application's own authenticated channel, and never put it in a listing or a shareable link. A ticket's expires is 6 hours after issue by default, and, when configured, the gate token inside it expires at the same moment. The SDK refuses expired tickets at connect and update; token-gated sockets reject further requests at expiry and close on the next heartbeat. Expiry does not erase a member's copy of the shared secret or terminate established media. Use key rotation to revoke membership, including on public trackers.
Kicks rotate the room
Removing someone is a key rotation, not a request to a server:
- Your backend calls
authority.kick(roomId, member). The room gets a new secret and a higherepoch, and the authority returns fresh tickets for every remaining member. - Your backend delivers those tickets, together with the removed peer's id.
- Each client calls
session.update(newTicket, { dropped: [peerId] }). It closes every old-key link, including unreported aliases, rejects the old key immediately and reconnects approved members on the new room tag. Media briefly pauses while new links form. - Host nodes and Electron desktop gateways revoke the entire old room epoch and all its allocations on update. This includes credentials issued to aliases the backend never learned. Update every host and remaining member; a machine left on the old key can still communicate with old-key holders.
A voluntary departure also needs rotation: await authority.leave(roomId, member) returns replacement tickets just like kick().
Gateways
Gateway credentials are minted per peer and per room, are valid for 2 hours by default (configurable from 1 to 86400 seconds) and are only issued to authenticated members. A gateway relays only inside its session: traffic goes between allocations on the gateway itself, and every other destination is refused. Nobody can use a participant's gateway to send traffic to other hosts on the internet, or into the participant's home network. Services on the gateway's own machine stay unreachable, and quotas apply (6 allocations per username, 4 Mbit/s per allocation, 32 allocations and 20 Mbit/s per room, 40 Mbit/s in total).
Credential expiry blocks authenticated requests; existing allocations can continue until their granted lifetime ends. Revocation ends them immediately: a rotation revokes the whole old room, a participant's desktop gateway releases a room when the participant leaves or the page navigates away, and credentials minted before a revocation never work again for that room.
On the desktop, configure the same exact HTTPS origins (HTTP is allowed only on loopback) in installFreehopGateway({ ipcMain, allowedOrigins }) and the preload argument --freehop-origins=. The main process rejects IPC from other origins and subframes.
Trust between members
Room members are mutually trusted for signalling: any member can forge envelopes in the room's name, because they share one key. Per-peer signatures are a possible addition to a future protocol version. Removing a member always requires a secret rotation.
IP addresses
Freehop's direct paths expose network addresses to the other participants. Gate and tracker operators also see connecting IP addresses. The SDK does not offer a mode that guarantees IP anonymity; encrypted signalling does not hide connection metadata.
Audit and limits
The 2 October 2026 source review added regression checks for key rotation, aliases, ticket expiry, authority races, forwarding authorization, gate input handling, UDP reflection budgets and Electron IPC authorization. The follow-up review added gateway key isolation, gate audiences, application-pinned STUN, room quotas, bounded revocations and both-endpoint forwarding consent. The current trust boundaries are described above and in the protocol. This is a source review with local tests, not an independent cryptographic certification.
Next
The cost model shows where bandwidth and compute costs fall and who carries it instead.