Senior software engineer blogging about software systems, computing history, and practical engineering.

FROG Protocol Draft

FROG/1 — Federated Rendezvous Overlay Gateway

0. Introduction

FROG is a small federated rendezvous protocol for peer-to-peer applications.

FROG provides:

  • public-key-derived peer identities
  • temporary peer presence
  • random peer discovery
  • exact peer lookup
  • WebRTC signaling relay
  • rendezvous server discovery
  • bounded server federation

FROG is not an application protocol.

After a direct WebRTC data channel is established, FROG is no longer in the data path.

A static application supporting WebRTC ships with:

  • a network name
  • a list of bootstrap rendezvous servers
  • a FROG client library

The application can then register, discover peers, exchange WebRTC signaling, and establish direct peer connections without operating a private signaling backend.

A network is a public application namespace.

Examples:

FOO
BLUTELLA
CHECKERS

A network is not a session, room, channel, or connection.

The WebRTC connection established between peers is the session.

FROG/1 favors:

  • a small surface area to promote independent interoperable implementations
  • ergonomics that make the protocol easy to use
  • temporary in-memory state
  • federation of relay servers
  • best-effort lookup rather than guaranteed lookup
  • opaque signaling payloads
  • simple wire semantics
  • one canonical text encoding for keys, signatures, and protocol identifiers

1. Conformance

The key words:

MUST
MUST NOT
REQUIRED
SHOULD
SHOULD NOT
RECOMMENDED
MAY
OPTIONAL

are used in their normal protocol-specification sense.

A FROG/1 implementation is compliant only if it follows the wire format, state machines, validation rules, command semantics, limits, timers, and authentication rules in this document.

Independent implementations must be able to interoperate without implementation-specific behavior.

2. Scope

This document defines the interoperable FROG/1 wire protocol.

FROG/1 supports:

  • WebSocket transport
  • client-to-server rendezvous
  • server-to-server federation
  • temporary peer registration
  • exact peer lookup
  • random peer discovery
  • opaque signaling relay
  • server gossip

FROG/1 does not define:

  • application data formats
  • WebRTC SDP format
  • ICE candidate format
  • application-level authentication
  • application-level encryption
  • persistent server storage
  • global federation membership policy
  • application rooms, sessions, channels, or groups

Federation is an authenticated and authorized server graph.

Federated random discovery and exact lookup use bounded recursive flooding.

Federated signaling follows hop-by-hop route state created by successful lookup.

3. Terminology

3.1 Client

A client is an endpoint that uses FROG to establish peer-to-peer connections.

Examples:

  • browser application
  • desktop application
  • mobile application
  • command-line tool
  • headless service

3.2 Rendezvous server

A rendezvous server helps clients:

  • register temporary presence
  • discover peers
  • locate peers
  • exchange signaling messages
  • discover additional rendezvous servers

A rendezvous server does not carry application traffic after WebRTC connection setup.

A rendezvous server is infrastructure.

A rendezvous server is not a participant in a network merely because it stores or relays presence for that network.

A rendezvous server MAY support many networks at the same time.

3.3 Network

A network identifies an interoperable application namespace.

A network is public.

A network is not secret.

A network is not an access-control mechanism.

A network is not a WebRTC session.

A network is not a room.

A client registered in one network MUST NOT discover, look up, or signal peers in another network through the same registration.

Applications that need rooms, sessions, channels, or access control must define them above FROG.

3.4 Peer fingerprint

A peer fingerprint is the public-key-derived identifier on the right side of a peer key.

A peer fingerprint is derived from a raw Ed25519 public key.

A peer fingerprint is not network-qualified.

The same public key always produces the same peer fingerprint.

3.5 Peer key

A peer key is the complete network-qualified peer identity used on the FROG wire.

A peer key has this form:

<network>:<peer_fingerprint>

Example:

BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW

The same peer fingerprint MAY be used in multiple networks.

These are distinct peer keys:

BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW
CHECKERS:AS3NN9TMCD3MR0M5VXEVYAYAPW

A peer key identifies a peer registration within one public application namespace.

3.6 Server ID

A server ID is the public-key-derived global identity of a rendezvous server.

A server ID is not network-qualified.

A server ID is used for sister-server authentication, federation trust policy, server gossip, duplicate sister connection handling, and route sides that point at adjacent servers.

3.7 Sister server

A sister server is another rendezvous server participating in the FROG federation.

A sister connection is a WebSocket connection between two rendezvous servers using FROG sister commands.

The FROG protocol aims to create a single global mesh of rendezvous servers rather than isolated private meshes.

3.8 Authenticated sister

An authenticated sister is a sister server that has proven possession of the private key corresponding to its claimed server ID on the current sister connection.

Authentication proves key possession on that connection.

Authentication does not prove that the claimed public URI is reachable.

Authentication does not by itself authorize the server to participate in federation.

3.9 Verified sister record

A verified sister record is a (server_id, server_uri) pair that this server has verified by opening a WebSocket connection to exactly server_uri and authenticating a server whose derived server ID is exactly server_id.

A server MUST NOT mark a server URI as verified merely because it was received in inbound @HELLO.

3.10 Authorized sister

An authorized sister is a sister server that local policy permits to participate in federation.

Local policy MAY authorize:

  • operator-configured servers
  • verified discovered servers
  • allowlisted server IDs
  • other implementation-defined trust rules

A public FROG server SHOULD use only verified sister records for automatic federation.

3.11 Live sister

A live sister is an authenticated, authorized sister server with a currently usable WebSocket connection.

Federated lookup, discovery, and signaling use live sisters.

3.12 Route

A route is temporary hop-by-hop signaling state created after successful peer lookup.

Routes are identified by a route_id.

A route binds:

  • route_id
  • peer_a_key
  • peer_b_key
  • side_a
  • side_b
  • created_at
  • expires_at

peer_a_key is the peer key of the peer that initiated lookup.

peer_b_key is the peer key of the target peer found by lookup.

Both peer keys in a route MUST be in the same network.

For each server that stores the route:

side_a is the local peer connection for peer_a_key, or the adjacent sister server ID used to reach peer_a_key.

side_b is the local peer connection for peer_b_key, or the adjacent sister server ID used to reach peer_b_key.

For a local route where both peers are registered on the same server:

side_a = local peer_a connection
side_b = local peer_b connection

For a route crossing multiple servers, each intermediate server stores only the adjacent hop toward each peer.

A route exists only for signaling setup.

A route does not imply that WebRTC connection establishment will succeed.

4. WebSocket transport

FROG/1 uses WebSocket.

Supported URI schemes are:

ws://
wss://

Public production servers SHOULD use:

wss://

Clients and sister servers MUST offer the WebSocket subprotocol:

frog.v1

Servers implementing FROG/1 MUST select:

frog.v1

If the WebSocket subprotocol cannot be selected, the connection MUST be closed.

All FROG messages are sent as WebSocket binary messages.

A receiver of a WebSocket text message MUST close the WebSocket connection.

Each WebSocket binary message contains exactly one complete FROG message.

WebSocket fragmentation is invisible to FROG. A fragmented WebSocket message is still one FROG message after reassembly.

WebSocket Ping/Pong MAY be used for transport liveness.

Peer presence is bound to the registered WebSocket connection.

A successful WebSocket Ping/Pong exchange does not imply that any route still exists.

5. FROG message format

A FROG message is either:

<header>\n

or:

<header>\n<payload bytes>

The header is UTF-8 text containing only ASCII protocol fields.

The newline is exactly one byte:

0x0A

Carriage return is not allowed in the header.

This is invalid:

<header>\r\n

The payload, when present, begins immediately after the newline.

The payload length MUST exactly match the decimal length declared in the header.

Protocol examples in this document show header text without explicitly showing the final WebSocket message newline unless a payload boundary matters.

A receiver MUST reject a message if:

  • the newline is missing
  • the header is invalid UTF-8
  • the header contains non-ASCII bytes
  • the header exceeds 4096 bytes before the newline
  • the header contains carriage return
  • the header has leading spaces
  • the header has trailing spaces
  • the header contains tabs
  • the header contains multiple adjacent spaces
  • the header contains empty fields
  • the header contains extra fields not allowed by the command grammar
  • the command is unknown
  • the command is not allowed in the current connection role
  • a required payload length is missing
  • a payload length is malformed
  • a payload length does not match the actual payload byte count
  • a payload length exceeds the allowed maximum
  • a non-payload command carries payload bytes

Maximum header size:

4096 bytes

Maximum signaling payload size:

65536 bytes

The maximum signaling payload size applies only to:

SIGNAL
SIGNAL-FROM
@SIGNAL

Payload length MAY be:

0

6. Header grammar

Command names are case-sensitive.

Header fields are separated by exactly one ASCII space:

0x20

Senders MUST NOT emit:

  • leading spaces
  • trailing spaces
  • tabs
  • multiple spaces between fields
  • empty fields
  • unknown fields

Receivers MUST reject malformed commands.

Decimal integer fields use:

0

or:

[1-9][0-9]*

Leading zeros are not allowed except for the value 0.

Negative numbers are not allowed.

The command set is closed.

FROG/1 does not define generic extension headers, comment lines, ignored metadata fields, or unknown command pass-through.

7. Identifiers and encodings

7.1 Network names

Network names use this regex:

^[A-Z0-9_]{1,16}$

Valid examples:

A
FOO
BLUTELLA
P2PCHAT
FILEXFER

Invalid examples:

gnutella
WEB-GAME
THIS_NETWORK_NAME_IS_TOO_LONG

Network names are protocol tokens.

Applications with longer human-readable names SHOULD choose a short canonical network token.

7.2 Strict Crockford Base32

Peer fingerprints, peer keys, server IDs, route IDs, nonces, public keys, and signatures use strict canonical Crockford Base32.

Allowed alphabet:

0123456789ABCDEFGHJKMNPQRSTVWXYZ

The letters below are not valid:

I
L
O
U

Lowercase letters are not valid.

Hyphens are not valid.

Checksum symbols are not valid.

Human-entry aliases are not valid.

Padding characters are not valid.

Receivers MUST reject non-canonical encodings.

No Base64 variant is used by FROG/1.

7.3 Crockford Base32 byte-string encoding

When raw bytes are encoded as strict Crockford Base32, the bytes are interpreted as one big-endian bit stream.

The bit stream is split into five-bit groups.

If the bit length is not a multiple of five, zero bits are appended at the end of the bit stream to complete the final five-bit group.

No padding character is emitted.

A receiver decoding a fixed-length raw byte field MUST reject the value unless re-encoding the decoded bytes produces the exact original string.

This rejects non-zero unused pad bits and non-canonical encodings.

7.4 Peer fingerprints

Peer fingerprints are exactly:

26 characters

Peer fingerprint syntax:

^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$

A peer fingerprint represents the first 130 bits of the Crockford Base32 encoding of the SHA-256 digest of a raw Ed25519 public key.

7.5 Peer keys

A peer key has this form:

<network>:<peer_fingerprint>

Peer key syntax:

^[A-Z0-9_]{1,16}:[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$

Example:

BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW

The part before the colon is the network.

The part after the colon is the peer fingerprint.

A peer key is the normal wire representation for peer identity in FROG/1.

7.6 Server IDs

Server IDs are exactly:

26 characters

Server ID syntax:

^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$

Server IDs are not network-qualified.

A server ID MUST NOT contain a colon.

A server ID MUST NOT contain a leading marker character.

Example:

4KVETTPBZR80KG1GTZ55CZ1KS9

7.7 Route IDs

Route IDs are exactly:

26 characters

Route ID syntax:

^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$

Route IDs MUST be cryptographically random.

A compliant implementation SHOULD generate 130 random bits and encode them as 26 strict Crockford Base32 characters.

A route ID is an opaque temporary signaling handle.

A route ID identifies one selected hop-by-hop signaling path between two peer keys.

For federated lookup, the route ID is also the flood request identifier.

7.8 Nonces

Nonces are exactly:

26 characters

Nonce syntax:

^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$

Nonces MUST contain at least 128 bits of entropy.

A compliant implementation SHOULD generate 130 random bits and encode them as 26 strict Crockford Base32 characters.

7.9 Client request IDs

Client request IDs are called cid.

CID syntax:

^[A-Z0-9_-]{1,32}$

The value below is reserved and MUST NOT be used as a CID:

-

A client MUST NOT reuse a CID while another request with the same CID is outstanding on the same WebSocket connection.

7.10 Federation request IDs

Federation request IDs are called fcid.

FCID syntax:

^[A-Z0-9_-]{1,32}$

The value below is reserved and MUST NOT be used as an FCID:

-

For @LIST and @SERVERS, fcid is scoped to the sister connection.

For recursive federated random discovery, a request is identified by:

origin_server_id
fcid

The origin server MUST NOT reuse an active fcid for the same origin_server_id.

Origin servers SHOULD generate fcid values with at least 96 bits of entropy.

For federated lookup, route_id is the request identifier.

8. Server URI format

Each rendezvous server has exactly one canonical public URI in FROG/1.

A server URI MUST be an absolute URI using one of these schemes:

ws://
wss://

URI parsing and generic URI syntax are based on RFC 3986.

Internationalized domain handling is based on IDNA A-label form as defined by RFC 5890 and RFC 5891.

IPv6 literal canonical text form is based on RFC 5952.

A server URI MUST be canonical.

A canonical server URI obeys all of these rules:

  • scheme is lowercase
  • host is lowercase when the host is a DNS name
  • internationalized DNS names use A-label / punycode form
  • DNS names MUST NOT have a trailing dot
  • IPv4 literals are allowed
  • IPv6 literals are allowed
  • IPv6 literals MUST use RFC 5952 canonical form
  • userinfo is not allowed
  • query is not allowed
  • fragment is not allowed
  • default port is omitted
  • non-default port, when present, is decimal 1..65535 without leading zeroes
  • path is present
  • path begins with /
  • path does not contain literal dot-segments
  • path does not contain percent-encoded dot-segments
  • percent escapes, if present, use uppercase hex
  • percent escapes MUST NOT encode unreserved ASCII characters
  • the URI contains ASCII bytes only
  • maximum length is 200 ASCII bytes

The default path is:

/

Therefore this is canonical:

wss://rv.example.net/

This is not canonical:

wss://rv.example.net

Default ports MUST be omitted.

These are not canonical:

wss://rv.example.net:443/
ws://rv.example.net:80/

Non-default ports MAY be used:

wss://rv.example.net:8443/

IPv4 literal example:

ws://192.0.2.10:9000/

IPv6 literal example:

wss://[2001:db8::1]:9443/

Receivers MUST NOT normalize server URIs before authentication verification.

The exact URI string from @HELLO is used in the server authentication string.

A server receiving a non-canonical server_uri MUST reject it.

Clients receiving non-canonical URIs in TRY MUST ignore those URIs.

Sister servers receiving non-canonical URIs in @SERVERS MUST reject the message.

9. Cryptography

9.1 Algorithm

FROG/1 uses:

Pure Ed25519

Implementations MUST NOT use:

  • Ed25519ph
  • Ed25519ctx
  • DER wrappers
  • PEM wrappers
  • SPKI wrappers
  • JWK wrappers

9.2 Public key format

Public keys are raw 32-byte Ed25519 public keys.

Encoded public keys use strict Crockford Base32.

The encoded length MUST be:

52 characters

9.3 Signature format

Signatures are raw 64-byte Ed25519 signatures.

Encoded signatures use strict Crockford Base32.

The encoded length MUST be:

103 characters

10. Peer identity

A peer fingerprint is derived from the raw 32-byte Ed25519 public key.

Algorithm:

peer_fingerprint = CROCKFORD_BASE32(SHA256(raw_public_key))[0:26]

The SHA-256 digest is interpreted as a big-endian bit stream.

The first encoded character uses the first five high-order bits of the digest.

A peer fingerprint is exactly 26 strict Crockford Base32 characters.

Example peer fingerprint:

AS3NN9TMCD3MR0M5VXEVYAYAPW

A peer key is created by prefixing the peer fingerprint with a network and a colon:

peer_key = network ":" peer_fingerprint

Example peer key:

BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW

A peer MAY reuse a persistent keypair.

A peer MAY generate an ephemeral keypair.

Both behaviors are valid.

If a peer reuses the same public key in different networks, the peer fingerprint is the same, but the peer keys are different.

11. Server identity

A server ID is derived from the raw 32-byte Ed25519 public key.

Algorithm:

server_id = CROCKFORD_BASE32(SHA256(raw_server_public_key))[0:26]

The SHA-256 digest is interpreted as a big-endian bit stream.

The first encoded character uses the first five high-order bits of the digest.

A server ID is exactly 26 strict Crockford Base32 characters.

Example:

4KVETTPBZR80KG1GTZ55CZ1KS9

Public rendezvous servers MUST use stable server keypairs.

Changing a server keypair creates a new server identity.

A server ID is global infrastructure identity.

A server ID is not scoped to a network.

12. Authentication strings

All authentication strings are UTF-8.

No trailing newline is included.

12.1 Client authentication string

Clients sign exactly:

FROG-AUTH-V1\n<server_nonce>\n<server_uri>\n<peer_key>\n<server_id>

Rendered as lines:

FROG-AUTH-V1
<server_nonce>
<server_uri>
<peer_key>
<server_id>

No trailing newline is included.

server_uri is the exact canonical URI the client used to open the WebSocket connection.

The client MUST NOT use a server URI supplied by the server for this field.

The server MUST verify that server_uri equals its own configured canonical public URI.

The server MUST verify that the supplied public key hashes to the peer fingerprint part of peer_key.

Signing the full peer key binds authentication to the selected network.

12.2 Server authentication string

Servers sign exactly:

FROG-SERVER-AUTH-V1\n<server_nonce>\n<self_server_uri>\n<self_server_id>\n<peer_server_uri>\n<peer_server_id>

Rendered as lines:

FROG-SERVER-AUTH-V1
<server_nonce>
<self_server_uri>
<self_server_id>
<peer_server_uri>
<peer_server_id>

No trailing newline is included.

self_server_uri and self_server_id are those of the server sending @AUTH.

peer_server_uri and peer_server_id are those of the server that sent the challenge.

The signed self_server_uri and self_server_id MUST match exactly the values sent by the authenticating server in @HELLO.

The signed peer_server_uri and peer_server_id MUST match exactly the verifier’s own configured canonical URI and server ID.

Receivers MUST NOT normalize URIs before verification.

13. Command grammar

The command set is closed.

A receiver MUST reject any command that does not match one of the forms below.

Command direction matters.

A client MUST NOT send server response commands.

A sister server MUST NOT send client-only commands on a sister connection.

13.1 Client connection commands

Client to server:

HELLO FROG/1
JOIN <peer_key>
AUTH <public_key> <signature>
LEAVE
GETSERVERS <cid> <limit>
FIND <cid> <limit>
LOOKUP <cid> <target_peer_key>
SIGNAL <route_id> <kind> <payload_length>
<payload_bytes>

Server to client:

HELLO FROG/1 <server_id>
CHAL <nonce>
OK JOIN
OK LEAVE
TRY <cid_or_dash> <count> <server_uri>...
PEERS <cid> <count> <peer_key>...
FOUND <cid> <target_peer_key> <route_id>
SIGNAL-FROM <route_id> <source_peer_key> <kind> <payload_length>
<payload_bytes>
ERR <id_or_dash> <code>

kind MUST be one of:

OFFER
ANSWER
ICE

For TRY, cid_or_dash is either a valid CID or:

-

The value - is used only for unsolicited TRY messages.

For TRY and PEERS, the declared count MUST equal the number of following URI or peer key fields.

TRY count MUST be in this range:

0..7

PEERS count MUST be in this range:

0..7

13.2 Sister connection commands

Either direction during sister authentication:

@HELLO FROG/1 <server_id> <server_uri>
@CHAL <nonce>
@AUTH <public_key> <signature>
@OK AUTH
@ERR <id_or_dash> <code>

Either direction after sister authentication:

@LIST <fcid> <limit>
@SERVERS <fcid> <count> <server_id> <server_uri>...
@FIND <fcid> <origin_server_id> <requesting_peer_key> <limit> <ttl>
@PEERS <fcid> <origin_server_id> <count> <peer_key>...
@LOOKUP <route_id> <origin_server_id> <source_peer_key> <target_peer_key> <ttl>
@FOUND <route_id> <target_peer_key>
@SIGNAL <route_id> <source_peer_key> <kind> <payload_length>
<payload_bytes>
@ERR <id_or_dash> <code>

For @SERVERS, the declared count is the number of (server_id, server_uri) pairs.

The number of following fields MUST be exactly:

count * 2

@SERVERS count MUST be in this range:

0..7

For @PEERS, the declared count MUST equal the number of following peer key fields.

@PEERS count MUST be in this range:

0..7

13.3 Payload commands

Only these commands carry payload bytes:

SIGNAL
SIGNAL-FROM
@SIGNAL

All other commands MUST have no payload bytes.

14. Connection roles

A WebSocket connection starts in state:

NEW

The first FROG command selects the connection role.

If the first command is:

HELLO

the connection is a client connection.

If the first command is:

@HELLO

the connection is a sister-server connection.

A connection MUST NOT mix client commands and sister-server commands.

After a connection sends or receives @HELLO, it MUST NOT use client commands on that connection.

After a connection sends or receives HELLO, it MUST NOT use sister-server commands on that connection.

15. Client connection state machine

Client connection states:

NEW
CLIENT_HELLO_OK
CLIENT_AUTH_PENDING
REGISTERED
CLOSED

Allowed client-to-server commands by state:

NEW:
  HELLO

CLIENT_HELLO_OK:
  JOIN
  GETSERVERS
  LEAVE

CLIENT_AUTH_PENDING:
  AUTH
  LEAVE

REGISTERED:
  LEAVE
  GETSERVERS
  FIND
  LOOKUP
  SIGNAL

Commands received in the wrong state MUST fail with:

ERR <cid_or_dash> BAD_STATE

If the command has a syntactically valid CID, the server MUST echo that CID.

Otherwise, the server MUST use:

-

Malformed commands fail with:

ERR <id_or_dash> BAD_REQUEST

If the correlation ID field is present and syntactically valid, the receiver SHOULD echo it.

If the correlation ID is absent, reserved, malformed, or cannot be parsed safely, the receiver MUST use:

-

For severe framing errors, a server MAY close without sending an error.

Servers MAY send unsolicited TRY messages after receiving a valid client HELLO.

A server MAY send TRY - <count> <server_uri>... and then close the WebSocket connection when it is overloaded, draining, restarting, or not accepting new registrations.

Clients MUST treat TRY as advisory.

Clients SHOULD validate and cache canonical URIs from TRY before reconnecting.

Clients SHOULD apply backoff and jitter when following TRY redirects.

16. Client handshake

A client begins with:

HELLO FROG/1

The server replies:

HELLO FROG/1 <server_id>

Example:

HELLO FROG/1 4KVETTPBZR80KG1GTZ55CZ1KS9

After this response, the connection is in:

CLIENT_HELLO_OK

The server ID in the response is used in the client authentication string.

A server that is not accepting new registrations MAY send unsolicited TRY after sending HELLO FROG/1 <server_id> and then close the WebSocket connection.

17. Client registration

A client requests registration with:

JOIN <peer_key>

Example:

JOIN BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW

The server validates:

  • connection state is CLIENT_HELLO_OK
  • peer key syntax is valid
  • the network part of the peer key is valid
  • the peer fingerprint part of the peer key is valid

If validation succeeds, the server stores pending join state and replies:

CHAL <nonce>

Example:

CHAL 8QAK1JY7Z5T2N9VVK36ZP3JH2M

The connection is now in:

CLIENT_AUTH_PENDING

The client responds:

AUTH <public_key> <signature>

The server verifies:

  • pending join state exists
  • challenge has not expired
  • public key decodes to exactly 32 bytes
  • public key encoding is canonical strict Crockford Base32
  • public key encoded length is 52 characters
  • signature decodes to exactly 64 bytes
  • signature encoding is canonical strict Crockford Base32
  • signature encoded length is 103 characters
  • public key hashes to the peer fingerprint part of the claimed peer key
  • signature is valid
  • signed authentication string matches the pending join context
  • signed server_uri equals the server’s configured canonical public URI

The signed authentication string is:

FROG-AUTH-V1\n<server_nonce>\n<server_uri>\n<peer_key>\n<server_id>

If authentication succeeds, the server replies:

OK JOIN

The connection is now in:

REGISTERED

A connection may contain exactly one active registration.

A registered connection MUST NOT send another JOIN.

A server MUST reject an additional JOIN on a registered connection with:

ERR - BAD_STATE

An expired client challenge fails with:

ERR - AUTH_FAILED

17.1 Duplicate local peer keys

A server MUST allow a duplicate local peer key claim to proceed through challenge and authentication.

After successful authentication for a peer key that is already registered locally, the server MUST atomically replace the old local registration with the new connection.

The server SHOULD close the old connection after replacement.

Only one active local registration for a peer key may exist on a server.

This replacement rule supports reconnects after browser reloads, mobile network changes, stale WebSocket state, and crashed clients.

A duplicate unauthenticated claim MUST NOT evict an existing registration.

Routes bound to an old local connection MUST expire when that connection is replaced or closed.

A duplicate peer fingerprint in a different network is not a duplicate peer key.

18. Presence

Presence is temporary.

A peer is present while its registered WebSocket connection remains open and registered.

If a registered client connection closes, the server MUST immediately remove the peer registration.

Servers SHOULD use WebSocket Ping/Pong or transport keepalive to detect dead connections.

Servers MAY close idle, unresponsive, abusive, or overloaded connections.

If a server closes a registered client connection, the server MUST remove the peer registration.

Clients SHOULD reconnect and re-register after disconnect.

A peer is reachable through FROG only while actively registered.

19. Leaving

A registered client may leave cleanly:

LEAVE

The server removes the peer registration and replies:

OK LEAVE

The server SHOULD then close the WebSocket connection.

A client in CLIENT_HELLO_OK or CLIENT_AUTH_PENDING MAY send:

LEAVE

The server SHOULD reply:

OK LEAVE

and close the connection.

20. Request concurrency

The following client commands require a CID:

GETSERVERS
FIND
LOOKUP

Servers MUST echo the same CID in the corresponding response or error when the CID is syntactically valid.

Clients MAY have multiple outstanding CID-bearing requests.

Responses MAY arrive out of order.

Clients MUST match responses by CID.

A client MUST NOT reuse a CID while another request with the same CID is outstanding on the same connection.

Signaling commands do not use CIDs.

Signaling errors use the route ID as the error correlation ID.

21. Client server discovery

Clients may ask for known verified rendezvous servers.

This command is allowed before registration.

Request:

GETSERVERS <cid> <limit>

Example:

GETSERVERS A1 7

The limit MUST be in this range:

1..7

Response:

TRY <cid> <count> <server_uri>...

Example:

TRY A1 2 wss://rv2.example.net/ wss://rv3.example.org/

The count MUST equal the number of server URIs.

The count MUST be in this range:

0..7

The server MUST return no more than the requested limit.

The server MUST return no more than 7 URIs.

The server MUST return only verified sister records.

The server MUST NOT return itself.

A server MAY send unsolicited alternatives:

TRY - <count> <server_uri>...

Example:

TRY - 1 wss://rv2.example.net/

Unsolicited TRY count MUST be in this range:

0..7

Clients SHOULD:

  • store learned servers
  • deduplicate exact canonical URIs
  • prefer recently successful servers
  • remove consistently failing servers over time
  • retain original bootstrap servers
  • ignore non-canonical URIs

A TRY message is advisory.

A TRY message does not guarantee that the listed servers are available.

22. Random peer discovery

A registered client discovers random peers using:

FIND <cid> <limit>

Example:

FIND F1 3

The server uses the network from the registered peer key.

The limit MUST be in this range:

1..7

Successful response:

PEERS <cid> <count> <peer_key>...

Example:

PEERS F1 2 BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA BLUTELLA:B4V7K9MT2Q36X5ZR8C1JAYP0ND

The count MUST equal the number of peer keys.

The count MUST be in this range:

0..7

Every returned peer key MUST be in the same network as the requesting peer key.

The server:

  • MUST exclude the requesting peer when selecting from local presence
  • MUST NOT expose the full peer table
  • SHOULD randomize selection
  • SHOULD prefer currently registered peers
  • MAY include peers learned from recursive authenticated @FIND requests

If no peers are available:

PEERS <cid> 0

Example:

PEERS F1 0

A client MUST perform LOOKUP before signaling.

A peer key returned by FIND is only a hint.

The peer may disconnect before LOOKUP.

23. Exact peer lookup

A registered client locates a known peer using:

LOOKUP <cid> <target_peer_key>

Example:

LOOKUP L1 BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA

The target peer key MUST be valid.

The target peer key MUST be in the same network as the registered peer key.

A client MUST NOT look up its own registered peer key.

A self-lookup MUST fail with:

ERR <cid> BAD_REQUEST

A cross-network lookup MUST fail with:

ERR <cid> BAD_REQUEST

On success:

FOUND <cid> <target_peer_key> <route_id>

Example:

FOUND L1 BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA 2N9VVK36ZP3JH2M8QAK1JY7Z5T

On failure:

ERR <cid> PEER_NOT_FOUND

or:

ERR <cid> LOOKUP_TIMEOUT

Lookup failure does not prove global offline status.

24. Lookup behavior

On LOOKUP, a server MUST first check its local peer table using the target peer key.

If the target peer key is registered locally, the server creates local route state and replies with FOUND.

If the target peer key is not registered locally, the server SHOULD perform a bounded recursive federated lookup using @LOOKUP.

The origin server creates and reserves the candidate route_id before sending any @LOOKUP.

If a generated route_id collides with existing local route or pending lookup state, the server MUST generate a new route_id.

The origin server sends @LOOKUP to eligible live sisters.

The first valid @FOUND response wins.

Later @FOUND responses for the same route_id MUST be ignored.

If the lookup timeout expires before a valid @FOUND is received, the server replies:

ERR <cid> LOOKUP_TIMEOUT

If the server has no local target and no eligible sister servers to query, it MAY reply:

ERR <cid> PEER_NOT_FOUND

Lookup failure does not prove global offline status.

Lookup timeout:

3000 milliseconds

Recommended origin lookup TTL:

5

Maximum lookup TTL:

7

25. Route state

Successful lookup creates temporary route state on every server along the selected route path.

Route IDs MUST be cryptographically random strict Crockford Base32 values.

Route IDs are exactly 26 characters.

Route state contains:

route_id
peer_a_key
peer_b_key
side_a
side_b
created_at
expires_at

peer_a_key is the source peer key that initiated the lookup.

peer_b_key is the target peer key.

peer_a_key and peer_b_key MUST be in the same network.

side_a is either:

local peer_a connection
adjacent sister server_id toward peer_a

side_b is either:

local peer_b connection
adjacent sister server_id toward peer_b

A server MAY cache the parsed network internally for indexing, validation, or rate limiting.

The network is not a separate protocol field in route state.

Default route lifetime:

180 seconds

Servers SHOULD refresh route expiration whenever valid signaling uses the route.

A local route side that refers to a client connection is bound to that exact WebSocket connection.

If that WebSocket connection closes or is replaced, routes bound to it MUST expire.

A server MUST NOT deliver signaling to a replaced or closed local connection.

A server that still has expired route state MUST fail signaling with:

ERR <route_id> ROUTE_EXPIRED

or:

@ERR <route_id> ROUTE_EXPIRED

After route state has been deleted, the server MAY instead return:

ERR <route_id> ROUTE_NOT_FOUND

or:

@ERR <route_id> ROUTE_NOT_FOUND

Clients MAY perform a fresh LOOKUP.

There is no explicit route close command in FROG/1.

Routes expire automatically.

26. Client signaling

FROG signaling payloads are opaque bytes.

Servers MUST NOT:

  • parse signaling payloads
  • modify signaling payloads
  • interpret signaling payloads

Signaling commands are valid only in state:

REGISTERED

Client request:

SIGNAL <route_id> <kind> <payload_length>
<payload_bytes>

Delivered form:

SIGNAL-FROM <route_id> <source_peer_key> <kind> <payload_length>
<payload_bytes>

kind MUST be one of:

OFFER
ANSWER
ICE

The sending peer is inferred from the registered connection.

The sending peer MUST be one endpoint in the route.

If the route side for the sending peer is local, the sending WebSocket connection MUST be the exact connection stored in the route side.

The target peer is inferred from the route.

Servers MUST reject signaling for:

  • unknown routes
  • expired routes
  • routes whose endpoints do not match
  • routes whose local connection does not match
  • routes whose peer key network does not match the sender’s registered peer key network
  • payloads larger than 65536 bytes

If the sender is peer_a_key, the outgoing side is side_b.

If the sender is peer_b_key, the outgoing side is side_a.

If the outgoing side of the route is a local peer connection, the server delivers the corresponding SIGNAL-FROM message to the target peer’s WebSocket connection.

If the target peer is no longer registered locally, the server SHOULD expire the route and send:

ERR <route_id> PEER_NOT_FOUND

to the sending client.

If the outgoing side of the route is an adjacent sister server, the local server relays the signaling command to that server using:

@SIGNAL

If the required sister server is unavailable, the server SHOULD fail with:

ERR <route_id> SERVER_UNAVAILABLE

27. Sister federation model

FROG/1 federation is an authenticated and authorized overlay graph.

A server MAY maintain sister connections to configured seed servers, learned verified servers, or operator-configured servers.

A server is not required to connect directly to every other server.

Federated lookup and discovery use bounded recursive flooding.

Federated signaling follows route state created by lookup.

Servers MUST use duplicate suppression and TTL limits to prevent forwarding loops.

Lower connectivity reduces lookup coverage.

This is acceptable because FROG is best-effort.

A server MUST NOT use an arbitrary inbound authenticated connection for federation commands unless the remote server ID is authorized by local policy.

For automatically learned federation, a server SHOULD use connections established by opening the remote server’s exact verified canonical URI.

28. Sister connection state machine

Sister connection states:

NEW
SISTER_AUTH
SISTER
CLOSED

Allowed received commands by state:

NEW:
  @HELLO

SISTER_AUTH:
  @CHAL
  @AUTH
  @OK AUTH
  @ERR

SISTER:
  @LIST
  @SERVERS
  @FIND
  @PEERS
  @LOOKUP
  @FOUND
  @SIGNAL
  @ERR

Commands received in the wrong state MUST fail with:

@ERR <id_or_dash> BAD_STATE

A server MAY close the connection after sending the error.

A server MUST NOT accept @AUTH unless it previously sent an unexpired @CHAL on that connection.

A server MUST NOT accept @OK AUTH unless it previously sent @AUTH on that connection.

A server MUST NOT enter SISTER state until:

  • it has authenticated the remote server
  • the remote server has acknowledged this server’s authentication
  • the remote server is authorized by local policy

Repeated @HELLO after a valid remote @HELLO MUST fail with:

@ERR - BAD_STATE

29. Sister server authentication

Federation uses the same WebSocket transport and FROG message format as clients.

Server-to-server commands are prefixed with:

@

The initiating server sends:

@HELLO FROG/1 <server_id> <server_uri>

Example:

@HELLO FROG/1 4KVETTPBZR80KG1GTZ55CZ1KS9 wss://rv1.example.net/

The receiving server replies:

@HELLO FROG/1 <server_id> <server_uri>

Example:

@HELLO FROG/1 9M4RX2C7DA8V6N0PGQBT3W5ZK1 wss://rv2.example.org/

Each side MUST challenge and authenticate the other side before accepting federation commands.

Handshake order:

  1. Initiator sends @HELLO.
  2. Receiver sends @HELLO.
  3. Receiver sends @CHAL.
  4. Initiator sends @AUTH.
  5. Receiver sends @OK AUTH.
  6. Initiator sends @CHAL.
  7. Receiver sends @AUTH.
  8. Initiator sends @OK AUTH.

After both authentication exchanges succeed and local authorization succeeds, the connection is in state:

SISTER

Challenge:

@CHAL <nonce>

Authentication response:

@AUTH <public_key> <signature>

Successful authentication response:

@OK AUTH

The signature is over the server authentication string:

FROG-SERVER-AUTH-V1\n<server_nonce>\n<self_server_uri>\n<self_server_id>\n<peer_server_uri>\n<peer_server_id>

The verifier MUST check:

  • @HELLO was received from this server
  • claimed server_id syntax is valid
  • claimed server_id is not the verifier's own server_id
  • claimed server_uri is canonical
  • public key decodes to exactly 32 bytes
  • public key encoding is canonical strict Crockford Base32
  • public key encoded length is 52 characters
  • signature decodes to exactly 64 bytes
  • signature encoding is canonical strict Crockford Base32
  • signature encoded length is 103 characters
  • public key hashes to claimed server_id
  • signature is valid for the exact authentication string
  • signed self_server_uri exactly matches the URI from the signer’s @HELLO
  • signed self_server_id exactly matches the server_id from the signer’s @HELLO
  • signed peer_server_uri exactly matches the verifier’s own configured canonical URI
  • signed peer_server_id exactly matches the verifier’s own server ID

A server SHOULD close the connection if sister authentication is not completed within:

30 seconds

An expired sister challenge fails with:

@ERR - AUTH_FAILED

30. Verified sisters

A live sister connection is authenticated after the sister authentication handshake succeeds and authorized after local policy permits federation commands.

A server record is verified for gossip only after this node has successfully opened a WebSocket connection to the record’s exact canonical server_uri and authenticated a server whose derived server_id matches the advertised server_id.

A server MUST NOT add a URI learned from inbound @HELLO to the verified gossip set merely because inbound authentication succeeded.

A server MAY use an authenticated inbound sister connection for federation commands only if local policy authorizes the remote server ID.

A server MUST NOT return an unverified URI in TRY, GETSERVERS responses, or @SERVERS responses.

A server_id has exactly one canonical public URI in FROG/1.

If a server learns a different URI for an already verified server_id, it MUST treat the new URI as unverified until it is explicitly verified by outbound connection.

If two verified records conflict for the same server_id, the server SHOULD retain the most recently verified successful URI and discard the other.

31. Duplicate sister connections

Multiple authenticated connections between the same pair of servers MAY exist transiently.

A server MUST handle duplicate sister connections safely.

A server MUST NOT process the same federation response twice merely because duplicate sister connections exist.

For steady state, servers SHOULD keep at most one authenticated and authorized sister connection per remote server_id.

If both servers have duplicate authenticated and authorized connections, the preferred connection is the one initiated by the server whose server_id is lexicographically smaller as ASCII.

If the preferred connection exists, both servers SHOULD close other authenticated and authorized sister connections for that same remote server_id.

If the preferred connection does not exist, either authenticated and authorized connection MAY be used until the preferred connection is established.

Route sides that refer to sister servers use the adjacent sister server_id.

When sending to a sister server_id, a server uses its currently selected live connection to that server.

32. Sister liveness

FROG/1 does not define @PING or @PONG.

Sister servers SHOULD use WebSocket Ping/Pong or transport keepalive for liveness.

When a sister connection closes, the server SHOULD remove it from the live sister set.

Closing a sister connection does not necessarily remove the sister from the verified sister set.

33. Sister server discovery

An authenticated and authorized sister server may request known verified servers:

@LIST <fcid> <limit>

Example:

@LIST G1 7

The limit MUST be in this range:

1..7

Response:

@SERVERS <fcid> <count> <server_id> <server_uri>...

Example:

@SERVERS G1 2 4KVETTPBZR80KG1GTZ55CZ1KS9 wss://rv1.example.net/ 9M4RX2C7DA8V6N0PGQBT3W5ZK1 wss://rv2.example.org/

The count is the number of (server_id, server_uri) pairs.

The number of following fields MUST be exactly:

count * 2

The count MUST be in this range:

0..7

The server MUST return no more than the requested limit.

The server MUST return no more than 7 server records.

The server MUST return only verified sister records.

The server MUST NOT return itself.

The server SHOULD NOT return the requesting sister.

Server discovery provides candidate sister servers.

Server discovery does not require or imply that every server connects directly to every other server.

A server receiving @SERVERS SHOULD attempt outbound verification of selected unknown candidates before adding them to the verified gossip set.

34. Flood forwarding

Federated flood requests are forwarded hop-by-hop across live sister connections.

Flood requests are bounded by:

  • ttl
  • duplicate suppression
  • local forwarding limits
  • rate limits
  • request timeout

The ttl field is a decimal integer.

Valid TTL range:

0..7

A receiver MUST reject a flood request with a TTL greater than 7.

For @FIND, the error is:

@ERR <fcid> BAD_REQUEST

For @LOOKUP, the error is:

@ERR <route_id> BAD_REQUEST

A receiver that accepts a flood request with ttl = 0 MUST check its own local state but MUST NOT forward the request.

A receiver that forwards a flood request MUST decrement ttl by one.

A server MUST NOT forward a flood request back to the sister server from which it received that request.

A server MUST suppress duplicate flood requests.

For @LOOKUP, the duplicate key is:

route_id

For @FIND, the duplicate key is:

origin_server_id
fcid

If a duplicate flood request has identical fields, the server SHOULD ignore it.

If a duplicate flood request has the same duplicate key but conflicting fields, the server MUST reject it.

For @FIND, the error is:

@ERR <fcid> BAD_STATE

For @LOOKUP, the error is:

@ERR <route_id> BAD_STATE

A server MAY apply a local fanout limit when forwarding flood requests.

Recommended forwarding limits:

max_lookup_forward_fanout = 7
max_find_forward_fanout = 7

If a server has more eligible sister servers than the applicable forwarding limit, it SHOULD select a randomized subset.

Servers SHOULD rotate randomized subsets over time.

Servers MUST rate-limit flood requests.

@FIND duplicate suppression state MUST be retained at least until the federated find timeout expires.

@LOOKUP duplicate suppression state MUST be retained at least until the lookup timeout expires.

If a route is created, conflicting duplicate @LOOKUP requests for the same route_id MUST be rejected for the lifetime of the route.

35. Federated random discovery

Federated random discovery uses bounded recursive flooding.

A server may ask live sisters for peers using:

@FIND <fcid> <origin_server_id> <requesting_peer_key> <limit> <ttl>

Example:

@FIND R1 4KVETTPBZR80KG1GTZ55CZ1KS9 BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW 3 3

The limit MUST be in this range:

1..7

The ttl MUST be in this range:

0..7

The receiving server MUST validate:

  • connection is in SISTER state
  • fcid syntax is valid
  • origin_server_id syntax is valid
  • requesting_peer_key syntax is valid
  • limit is in the valid range
  • ttl is in the valid range

If origin_server_id is the receiving server’s own server ID, the receiving server SHOULD treat the request as a loop and ignore it unless it is needed for duplicate-conflict detection.

The responding server searches its own local peer table in the network parsed from requesting_peer_key.

Response:

@PEERS <fcid> <origin_server_id> <count> <peer_key>...

Example:

@PEERS R1 4KVETTPBZR80KG1GTZ55CZ1KS9 2 BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA BLUTELLA:B4V7K9MT2Q36X5ZR8C1JAYP0ND

The count MUST equal the number of peer keys.

The count MUST be in this range:

0..7

Every returned peer key MUST be in the same network as requesting_peer_key.

The responding server:

  • MUST NOT include requesting_peer_key
  • MUST NOT expose its full peer table
  • SHOULD randomize selection
  • SHOULD return only currently registered local peers
  • MUST NOT return more than the requested limit

For a client FIND that uses federation, the origin server creates pending find state:

client_cid
fcid
origin_server_id = local server_id
requesting_peer_key
client_limit
forwarded_to
unique_peer_keys
created_at
expires_at

For a non-origin server receiving non-duplicate @FIND, the server stores pending find state:

origin_server_id
fcid
requesting_peer_key
limit
side_origin = authenticated sister server from which @FIND was received
forwarded_to = set of sister server IDs
created_at
expires_at

A server MAY cache the parsed network from requesting_peer_key.

A server receiving @FIND SHOULD forward it to eligible live sisters if:

  • ttl is greater than 0
  • the request is not a duplicate
  • local forwarding limits allow forwarding
  • rate limits allow forwarding

When forwarding, the server MUST decrement ttl by one.

A server receiving @PEERS for a pending federated find request MUST validate:

  • fcid matches pending request
  • origin_server_id matches pending request
  • the authenticated sender is in forwarded_to
  • count matches the number of peer keys
  • all peer keys are valid
  • all peer keys are in the same network as the requesting peer key
  • count is not greater than the original requested limit

A server receiving valid @PEERS MUST forward it toward the origin server unless it is the origin server.

The origin server aggregates unique peer keys until one of these occurs:

  • the requested client limit is reached
  • the find timeout expires
  • the request is canceled

The origin server then replies to the client with exactly one PEERS response.

Federated find timeout:

1500 milliseconds

Recommended origin find TTL:

3

If no peers are found, the origin server replies:

PEERS <cid> 0

A client MUST still perform LOOKUP before signaling.

A peer key returned by FIND is only a hint.

The peer may disconnect before LOOKUP.

36. Federated exact lookup

Federated exact lookup uses bounded recursive flooding.

The origin server creates a candidate route ID and sends:

@LOOKUP <route_id> <origin_server_id> <source_peer_key> <target_peer_key> <ttl>

Example:

@LOOKUP 2N9VVK36ZP3JH2M8QAK1JY7Z5T 4KVETTPBZR80KG1GTZ55CZ1KS9 BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA 5

The receiving server MUST validate:

  • connection is in SISTER state
  • route_id syntax is valid
  • origin_server_id syntax is valid
  • source_peer_key syntax is valid
  • target_peer_key syntax is valid
  • source_peer_key and target_peer_key are in the same network
  • ttl is in the valid range

If origin_server_id is the receiving server’s own server ID, the receiving server SHOULD treat the request as a loop and ignore it unless it is needed for duplicate-conflict detection.

If the receiving server has already processed the same route_id with identical lookup fields, it SHOULD ignore the duplicate.

If the receiving server has already processed the same route_id with different lookup fields, it MUST reject the request:

@ERR <route_id> BAD_STATE

If the target peer key is registered locally, the receiving server stores route state:

route_id
peer_a_key = source_peer_key
peer_b_key = target_peer_key
side_a = authenticated sister server from which @LOOKUP was received
side_b = local target peer connection
created_at
expires_at

The receiving server then replies toward side_a:

@FOUND <route_id> <target_peer_key>

Example:

@FOUND 2N9VVK36ZP3JH2M8QAK1JY7Z5T BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA

If the target peer key is not registered locally and ttl is greater than zero, the receiving server SHOULD forward the lookup to eligible live sisters.

When forwarding, the server MUST decrement ttl by one.

The server MUST NOT forward the lookup to the sister server from which it received the lookup.

Before forwarding, the server stores pending lookup state:

route_id
origin_server_id
peer_a_key = source_peer_key
peer_b_key = target_peer_key
side_a = authenticated sister server from which @LOOKUP was received
forwarded_to = set of sister server IDs
created_at
expires_at

The origin server also stores pending lookup state:

route_id
client_cid
origin_server_id = local server_id
peer_a_key = source_peer_key
peer_b_key = target_peer_key
side_a = local source peer connection
forwarded_to = set of sister server IDs
created_at
expires_at

A server receiving @FOUND for a pending lookup validates:

  • route_id is pending
  • target_peer_key matches pending lookup
  • the authenticated sender is in forwarded_to

The first valid @FOUND wins.

The server stores route state:

route_id
peer_a_key
peer_b_key
side_a = pending side_a
side_b = authenticated sister server from which @FOUND was received
created_at
expires_at

If side_a is an adjacent sister server, the server forwards the same @FOUND toward side_a.

If side_a is the local source peer connection, the server replies to the client:

FOUND <cid> <target_peer_key> <route_id>

Later @FOUND responses for the same route_id MUST be ignored.

FROG/1 does not use @NOTFOUND.

Lookup failure is detected by timeout.

37. Federated signaling

When a route crosses servers, client-facing signaling commands remain unchanged.

A server relays signaling along the hop-by-hop route state created by lookup.

Federated signaling command:

@SIGNAL <route_id> <source_peer_key> <kind> <payload_length>
<payload_bytes>

kind MUST be one of:

OFFER
ANSWER
ICE

For every route:

peer_a_key is the lookup source peer key.

peer_b_key is the lookup target peer key.

side_a is the next hop toward peer_a_key.

side_b is the next hop toward peer_b_key.

If source_peer_key = peer_a_key, the message travels from side_a toward side_b.

If source_peer_key = peer_b_key, the message travels from side_b toward side_a.

The receiving server MUST validate:

  • connection is in SISTER state
  • route_id exists
  • route is not expired
  • source_peer_key is peer_a_key or peer_b_key
  • the authenticated remote server is the expected incoming side for source_peer_key
  • kind is valid
  • payload length is valid
  • payload length is not greater than 65536

If validation succeeds, the receiving server forwards or delivers the message.

If the outgoing side is a local peer connection, the server delivers:

SIGNAL-FROM <route_id> <source_peer_key> <kind> <payload_length>
<payload_bytes>

If the outgoing side is an adjacent sister server, the server forwards the original federated signaling command to that sister server.

Servers MUST NOT parse, modify, or interpret signaling payloads.

If the local target peer is no longer registered, the receiving server SHOULD expire the route and reply toward the sender:

@ERR <route_id> PEER_NOT_FOUND

If the required sister server is unavailable, the server SHOULD reply toward the sender:

@ERR <route_id> SERVER_UNAVAILABLE

A server receiving @ERR <route_id> <code> for a known route SHOULD forward the error toward the opposite side from the authenticated sender.

If the opposite side is local, the server sends:

ERR <route_id> <code>

If the opposite side is a sister server, the server sends:

@ERR <route_id> <code>

38. Error responses

Client-visible errors use:

ERR <id_or_dash> <code>

Federation errors use:

@ERR <id_or_dash> <code>

The ID is selected as follows:

CID-bearing client request: use cid
FCID-bearing federation request: use fcid
client signaling command: use route_id
federated lookup: use route_id
federated signaling command: use route_id
no useful correlation ID: use -

If the correlation ID field is present and syntactically valid, the receiver SHOULD echo it.

If the correlation ID is absent, reserved, malformed, or cannot be parsed safely, the receiver MUST use:

-

The value - means no request or route ID is available.

Required client-visible error codes:

BAD_REQUEST
BAD_STATE
AUTH_REQUIRED
AUTH_FAILED
PEER_NOT_FOUND
LOOKUP_TIMEOUT
ROUTE_NOT_FOUND
ROUTE_EXPIRED
TARGET_MISMATCH
PAYLOAD_TOO_LARGE
RATE_LIMITED
SERVER_UNAVAILABLE
INTERNAL

Required federation error codes:

BAD_REQUEST
BAD_STATE
AUTH_REQUIRED
AUTH_FAILED
PEER_NOT_FOUND
LOOKUP_TIMEOUT
ROUTE_NOT_FOUND
ROUTE_EXPIRED
TARGET_MISMATCH
PAYLOAD_TOO_LARGE
RATE_LIMITED
SERVER_UNAVAILABLE
INTERNAL

Meaning:

BAD_REQUEST

Malformed command, invalid field, invalid syntax, invalid limit, or protocol parse error.

BAD_STATE

Command is not valid in the current connection state.

AUTH_REQUIRED

Command requires authentication, authorization, or registration.

AUTH_FAILED

Authentication failed.

PEER_NOT_FOUND

A peer is not currently known locally, a route endpoint disappeared, or no eligible lookup path exists. This does not prove the peer is globally offline.

LOOKUP_TIMEOUT

A federated lookup did not produce a valid route before the lookup timeout.

ROUTE_NOT_FOUND

Route ID is unknown.

ROUTE_EXPIRED

Route state exists or recently existed but is expired.

TARGET_MISMATCH

Signaling source, route, connection, or server side does not match route state.

PAYLOAD_TOO_LARGE

Declared payload length exceeds the allowed maximum.

RATE_LIMITED

Request was rejected by rate limiting.

SERVER_UNAVAILABLE

Required sister server or route endpoint server is unavailable.

INTERNAL

Server encountered an internal failure.

Servers MAY close the WebSocket after sending an error for:

  • malformed framing
  • authentication failure
  • authorization failure
  • protocol confusion
  • repeated bad requests
  • rate-limit abuse
  • payload abuse

For severe framing errors, a server MAY close without sending an error if sending an error is not safe or practical.

39. Required limits and timers

FROG/1 required limits:

max_header_size = 4096 bytes
max_signaling_payload_size = 65536 bytes
max_find_limit = 7 peers
max_peer_response_count = 7 peers
max_getservers_limit = 7 servers
max_sister_list_limit = 7 servers
max_server_response_count = 7 servers
max_server_uri_length = 200 ASCII bytes
route_ttl = 180 seconds
auth_challenge_ttl = 30 seconds
lookup_timeout = 3000 milliseconds
find_timeout = 1500 milliseconds
max_flood_ttl = 7
recommended_lookup_ttl = 5
recommended_find_ttl = 3
recommended_lookup_forward_fanout = 7 sister servers
recommended_find_forward_fanout = 7 sister servers

Servers MAY use stricter operational limits for rate limiting, overload control, and connection admission.

Servers MUST NOT advertise success for data they discard.

Servers MUST expire:

  • pending client authentication challenges
  • pending sister authentication challenges
  • pending federated lookups
  • pending federated finds
  • peer presence on connection close
  • route state
  • duplicate suppression state

40. Abuse protection

Servers SHOULD rate-limit:

  • new WebSocket connections
  • connections per IP
  • JOIN attempts
  • failed authentication attempts
  • GETSERVERS requests
  • FIND requests
  • LOOKUP requests
  • SIGNAL traffic
  • @LIST requests
  • @FIND requests
  • @LOOKUP requests
  • @SIGNAL traffic
  • server verification attempts

Servers SHOULD bound:

  • maximum registered peers
  • maximum peers per network
  • maximum pending challenges
  • maximum pending finds
  • maximum pending lookups
  • maximum route count
  • maximum sister connections
  • maximum outbound verification attempts
  • maximum queued outbound messages per connection

Servers SHOULD aggressively expire stale state.

Servers MAY reject new registrations under overload with:

ERR - RATE_LIMITED

or:

ERR - SERVER_UNAVAILABLE

Servers MAY reject expensive requests under overload with the same codes.

Servers MAY send TRY - <count> <server_uri>... before closing a valid client connection under overload.

41. Client bootstrap behavior

A client application SHOULD ship with a hardcoded list of bootstrap rendezvous servers.

Example:

wss://rv1.example.net/
wss://rv2.example.org/

Client startup behavior:

  1. Load hardcoded bootstrap servers.
  2. Load cached learned servers.
  3. Deduplicate exact canonical URIs.
  4. Randomize or rotate the server list.
  5. Attempt WebSocket connections using subprotocol frog.v1.
  6. Complete HELLO.
  7. Register using JOIN and AUTH.
  8. Request additional servers using GETSERVERS.
  9. Cache successful alternatives.
  10. Remain connected while present.

Clients SHOULD retain the original hardcoded bootstrap list even after learning new servers.

Clients SHOULD reconnect and re-register after disconnect.

Clients SHOULD use backoff when reconnecting.

Clients SHOULD avoid reconnect storms by adding jitter.

If a client receives unsolicited TRY, it SHOULD validate and cache canonical URIs before reconnecting.

42. Client behavior

A compliant client SHOULD:

  1. Connect to a bootstrap or cached server.
  2. Offer WebSocket subprotocol frog.v1.
  3. Send HELLO.
  4. Register with JOIN and AUTH.
  5. Stay connected while present.
  6. Request additional servers using GETSERVERS.
  7. Cache TRY results.
  8. Use FIND to discover random peers.
  9. Use LOOKUP to locate known peers.
  10. Exchange signaling using SIGNAL.
  11. Reconnect and re-register after disconnect.
  12. Remove persistently failing servers from the preferred cache.

A peer is reachable through FROG only while actively registered.

43. Server behavior

A compliant rendezvous server SHOULD:

  1. Accept WebSocket connections using subprotocol frog.v1.
  2. Maintain a stable server keypair.
  3. Derive and advertise its server_id.
  4. Enforce canonical server_uri syntax.
  5. Authenticate clients.
  6. Authenticate and authorize sister servers.
  7. Store temporary peer presence.
  8. Remove peer presence when registered connections close.
  9. Replace duplicate local peer key registrations only after successful authentication.
  10. Maintain verified sister records.
  11. Maintain live sister connections.
  12. Exchange server gossip.
  13. Provide TRY alternatives.
  14. Support local FIND and LOOKUP.
  15. Support recursive federated @FIND and @LOOKUP.
  16. Relay signaling traffic along route state.
  17. Enforce payload limits.
  18. Enforce rate limits.
  19. Expire stale route state.
  20. Bound memory growth.
  21. Continue functioning after restart.

A server restart may lose temporary peer, pending request, duplicate suppression, and route state.

Clients and applications MUST tolerate that loss.

44. Reachability model

FROG is best-effort.

A peer may fail to connect because:

  • peer is offline
  • peer disconnected
  • rendezvous server restarted
  • federation lookup timed out
  • flood TTL was too small
  • federation graph did not contain a working path to the target server
  • route expired
  • target peer rejected the connection
  • NAT traversal failed
  • server was overloaded
  • rate limits triggered
  • signaling was dropped

Applications MUST handle connection failure.

FROG does not guarantee universal reachability.

45. Privacy and security boundary

FROG does not provide anonymity.

Rendezvous servers can observe:

  • client IP addresses
  • peer keys
  • peer fingerprints
  • server IDs
  • network names
  • lookup targets
  • discovery requests
  • timing metadata
  • signaling payload sizes
  • signaling payload bytes

FROG authenticates registration to a peer key.

FROG does not by itself provide end-to-end authentication of the resulting WebRTC data channel.

A malicious or compromised rendezvous server can drop, delay, replay within route lifetime, or modify signaling payloads unless the application protects against this.

Applications that treat a peer key as an identity MUST authenticate the data channel after connection setup.

A minimal application-level identity check is:

  1. Each side sends its raw Ed25519 public key over the data channel.
  2. Each side verifies that the public key hashes to the peer fingerprint part of the expected peer key.
  3. Each side verifies that the network part of the expected peer key is the expected application namespace.
  4. Each side sends a fresh random challenge.
  5. Each side signs a transcript-bound challenge string with its Ed25519 private key.
  6. Each side verifies the signature before trusting the peer identity.

FROG does not define that application-level proof.

FROG does not encrypt signaling payloads end-to-end.

wss:// protects signaling on each transport hop, but rendezvous servers still see payload bytes.

Applications requiring stronger privacy must build it above or beside FROG.

46. Compliance

A compliant client implementation MUST support:

HELLO
JOIN
CHAL
AUTH
LEAVE
GETSERVERS
TRY
FIND
PEERS
LOOKUP
FOUND
SIGNAL
SIGNAL-FROM
OK
ERR

A compliant rendezvous server implementation MUST support all client commands and additionally:

@HELLO
@CHAL
@AUTH
@OK AUTH
@LIST
@SERVERS
@FIND
@PEERS
@LOOKUP
@FOUND
@SIGNAL
@ERR

A client-only implementation does not need to support sister-server commands.

A public rendezvous server implementation MUST support sister-server commands.

47. Example local session

Peer A key:

BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW

Peer B key:

BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA

Server ID:

4KVETTPBZR80KG1GTZ55CZ1KS9

Server URI:

wss://rv.example.net/

Route ID:

2N9VVK36ZP3JH2M8QAK1JY7Z5T

Client connects.

Client:

HELLO FROG/1

Server:

HELLO FROG/1 4KVETTPBZR80KG1GTZ55CZ1KS9

Client:

JOIN BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW

Server:

CHAL 8QAK1JY7Z5T2N9VVK36ZP3JH2M

Client signs:

FROG-AUTH-V1
8QAK1JY7Z5T2N9VVK36ZP3JH2M
wss://rv.example.net/
BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW
4KVETTPBZR80KG1GTZ55CZ1KS9

Client:

AUTH 0EGGFFZKSR8BW7BGVMCEEJY0K5KY9NHGKEJGTQRXVJ3684JN66W0 HAMFPA9XA6MWMRRS07F69D8NJN1F7FGP0X2V0MAJ62J9HE8YTE64KYTKWDTSS9HZSTATECCTQGJ8XTC9J66BS0NA03TXZGJBZT7TA30

Server:

OK JOIN

Client:

FIND F1 3

Server:

PEERS F1 1 BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA

Client:

LOOKUP L1 BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA

Server:

FOUND L1 BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA 2N9VVK36ZP3JH2M8QAK1JY7Z5T

Client sends a binary WebSocket message containing:

SIGNAL 2N9VVK36ZP3JH2M8QAK1JY7Z5T OFFER 2841
<2841 offer bytes>

Peer B receives:

SIGNAL-FROM 2N9VVK36ZP3JH2M8QAK1JY7Z5T BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW OFFER 2841
<2841 offer bytes>

Peer B sends:

SIGNAL 2N9VVK36ZP3JH2M8QAK1JY7Z5T ANSWER 1906
<1906 answer bytes>

Peer A receives:

SIGNAL-FROM 2N9VVK36ZP3JH2M8QAK1JY7Z5T BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA ANSWER 1906
<1906 answer bytes>

ICE candidates are exchanged using SIGNAL with kind ICE.

WebRTC connects.

Application protocol traffic begins over the direct data channel.

FROG is no longer in the data path.

48. Example recursive federated lookup

Peer A is connected to Server A.

Peer B is connected to Server C.

Server A is connected to Server B.

Server B is connected to Server C.

Server A is not directly connected to Server C.

Peer A:

LOOKUP L1 BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA

Server A creates route ID:

2N9VVK36ZP3JH2M8QAK1JY7Z5T

Server A sends to Server B:

@LOOKUP 2N9VVK36ZP3JH2M8QAK1JY7Z5T 4KVETTPBZR80KG1GTZ55CZ1KS9 BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA 5

Server B does not have Peer B locally.

Server B stores pending lookup state and forwards to Server C with decremented TTL:

@LOOKUP 2N9VVK36ZP3JH2M8QAK1JY7Z5T 4KVETTPBZR80KG1GTZ55CZ1KS9 BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA 4

Server C has Peer B registered locally.

Server C stores route state:

peer_a_key = BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW
peer_b_key = BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA
side_a = Server B
side_b = local Peer B connection

Server C replies to Server B:

@FOUND 2N9VVK36ZP3JH2M8QAK1JY7Z5T BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA

Server B stores route state:

peer_a_key = BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW
peer_b_key = BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA
side_a = Server A
side_b = Server C

Server B forwards to Server A:

@FOUND 2N9VVK36ZP3JH2M8QAK1JY7Z5T BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA

Server A stores route state:

peer_a_key = BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW
peer_b_key = BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA
side_a = local Peer A connection
side_b = Server B

Server A replies to Peer A:

FOUND L1 BLUTELLA:7XQ0J5M8V4K2R9N3T6W1CZEHYA 2N9VVK36ZP3JH2M8QAK1JY7Z5T

Peer A sends:

SIGNAL 2N9VVK36ZP3JH2M8QAK1JY7Z5T OFFER 2841
<2841 offer bytes>

Server A relays to Server B:

@SIGNAL 2N9VVK36ZP3JH2M8QAK1JY7Z5T BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW OFFER 2841
<2841 offer bytes>

Server B relays to Server C:

@SIGNAL 2N9VVK36ZP3JH2M8QAK1JY7Z5T BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW OFFER 2841
<2841 offer bytes>

Server C delivers to Peer B:

SIGNAL-FROM 2N9VVK36ZP3JH2M8QAK1JY7Z5T BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW OFFER 2841
<2841 offer bytes>

The answer and ICE messages follow the same route in the opposite direction.

49. Example busy server redirect

Client connects.

Client:

HELLO FROG/1

Server:

HELLO FROG/1 4KVETTPBZR80KG1GTZ55CZ1KS9

Server:

TRY - 2 wss://rv2.example.net/ wss://rv3.example.org/

The server closes the WebSocket connection.

The client validates the canonical URIs, caches them, applies backoff and jitter, and attempts another server using normal bootstrap behavior.

50. Test vectors

50.1 Peer key

Peer private seed:

000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f

Peer public key hex:

03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8

Peer public key:

0EGGFFZKSR8BW7BGVMCEEJY0K5KY9NHGKEJGTQRXVJ3684JN66W0

Derived peer fingerprint:

AS3NN9TMCD3MR0M5VXEVYAYAPW

Network:

BLUTELLA

Derived peer key:

BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW

50.2 Server key

Server private seed:

202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f

Server public key hex:

29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7

Server public key:

56PBNRA1QK5F1CHE3AAD6K8BRWV1WMKD1FZ15J4QJJY968MPDQBG

Derived server ID:

4KVETTPBZR80KG1GTZ55CZ1KS9

50.3 Client authentication signature

Authentication nonce:

8QAK1JY7Z5T2N9VVK36ZP3JH2M

Server URI:

wss://rv.example.net/

Peer key:

BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW

Server ID:

4KVETTPBZR80KG1GTZ55CZ1KS9

Exact client authentication string:

FROG-AUTH-V1
8QAK1JY7Z5T2N9VVK36ZP3JH2M
wss://rv.example.net/
BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW
4KVETTPBZR80KG1GTZ55CZ1KS9

No trailing newline is included.

Expected signature using the peer private seed:

HAMFPA9XA6MWMRRS07F69D8NJN1F7FGP0X2V0MAJ62J9HE8YTE64KYTKWDTSS9HZSTATECCTQGJ8XTC9J66BS0NA03TXZGJBZT7TA30

50.4 Server authentication signature

Authentication nonce:

8QAK1JY7Z5T2N9VVK36ZP3JH2M

Self server URI:

wss://rv.example.net/

Self server ID:

4KVETTPBZR80KG1GTZ55CZ1KS9

Peer server URI:

wss://rv2.example.org/

Peer server ID:

9M4RX2C7DA8V6N0PGQBT3W5ZK1

Exact server authentication string:

FROG-SERVER-AUTH-V1
8QAK1JY7Z5T2N9VVK36ZP3JH2M
wss://rv.example.net/
4KVETTPBZR80KG1GTZ55CZ1KS9
wss://rv2.example.org/
9M4RX2C7DA8V6N0PGQBT3W5ZK1

No trailing newline is included.

Expected signature using the server private seed:

11Y0VX78BAMYRM1T40MPB68RNSEKN86NJSWPX6XSJ61P72MHPWH8YV1SZNQQZH0QJBY4X6PBJYYD74VA96SB4CMC72SS8JSW97D7E1G

50.5 Valid identifiers

Valid network:

BLUTELLA

Valid peer fingerprint:

AS3NN9TMCD3MR0M5VXEVYAYAPW

Valid peer key:

BLUTELLA:AS3NN9TMCD3MR0M5VXEVYAYAPW

Valid server ID:

4KVETTPBZR80KG1GTZ55CZ1KS9

Valid route ID:

2N9VVK36ZP3JH2M8QAK1JY7Z5T

Valid nonce:

8QAK1JY7Z5T2N9VVK36ZP3JH2M

Valid CID:

A1

Valid FCID:

R1

Valid server URI:

wss://rv.example.net/

Valid IPv4 literal server URI:

ws://192.0.2.10:9000/

Valid IPv6 literal server URI:

wss://[2001:db8::1]:9443/

50.6 Invalid identifiers

Invalid network because lowercase is not allowed:

blutella

Invalid network because hyphen is not allowed:

WEB-GAME

Invalid network because it is too long:

THIS_NETWORK_NAME_IS_TOO_LONG

Invalid peer fingerprint because O is not allowed:

AS3NN9TMCD3MROM5VXEVYAYAPW

Invalid peer fingerprint because lowercase is not allowed:

as3nn9tmcd3mr0m5vxevyayapw

Invalid peer key because the colon is missing:

BLUTELLAAS3NN9TMCD3MR0M5VXEVYAYAPW

Invalid peer key because the network is lowercase:

blutella:AS3NN9TMCD3MR0M5VXEVYAYAPW

Invalid peer key because the peer fingerprint is lowercase:

BLUTELLA:as3nn9tmcd3mr0m5vxevyayapw

Invalid server ID because server IDs are not network-qualified:

BLUTELLA:4KVETTPBZR80KG1GTZ55CZ1KS9

Invalid CID because - is reserved:

-

Invalid server URI because the path is missing:

wss://rv.example.net

Invalid server URI because the default port is present:

wss://rv.example.net:443/

Invalid server URI because query is not allowed:

wss://rv.example.net/?x=1

Invalid server URI because IPv6 is not canonical RFC 5952 form:

wss://[2001:0db8:0000:0000:0000:0000:0000:0001]/

Invalid server URI because port has a leading zero:

wss://rv.example.net:0443/

Invalid server URI because the DNS name has a trailing dot:

wss://rv.example.net./

51. Naming

Protocol name:

FROG

Meaning:

Federated Rendezvous Overlay Gateway

Current protocol version:

FROG/1

WebSocket subprotocol:

frog.v1