Skip to main content

Security model

FREEHOP / NETWORK MODELSealed by the client
◈Seal
◇Pass ciphertext
◈Open

HKDF + AES-256-GCM protect signalling envelopes from the gate.

Everything the gate handles is sealed. Only the people in the room hold the key.

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:

ValueDerivationUsed for
Room tagHKDF(secret, salt, info "room-tag"), 32 bytes, base64urlThe only room identifier a gate ever sees
Envelope keyHKDF(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​

PartyLearnsCannot
GateSocket IP addresses, opaque room tags, random peer ids, envelope sizes and timingRead 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 muchRead the media: it stays DTLS-SRTP encrypted end to end, and the gateway relays ciphertext
Forwarding participantThe media it forwardsNothing 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 carriesRead the media: it stays DTLS-SRTP encrypted end to end
Room memberNetwork addresses on direct paths, the room's shared key and, with classifyNat, each member's NAT typeAuthenticate 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:

  1. Your backend calls authority.kick(roomId, member). The room gets a new secret and a higher epoch, and the authority returns fresh tickets for every remaining member.
  2. Your backend delivers those tickets, together with the removed peer's id.
  3. 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.
  4. 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.