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..65535without 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_uriequals 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
@FINDrequests
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:
- Initiator sends
@HELLO. - Receiver sends
@HELLO. - Receiver sends
@CHAL. - Initiator sends
@AUTH. - Receiver sends
@OK AUTH. - Initiator sends
@CHAL. - Receiver sends
@AUTH. - 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:
@HELLOwas 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
SISTERstate - 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
SISTERstate - 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
SISTERstate - 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:
- Load hardcoded bootstrap servers.
- Load cached learned servers.
- Deduplicate exact canonical URIs.
- Randomize or rotate the server list.
- Attempt WebSocket connections using subprotocol
frog.v1. - Complete
HELLO. - Register using
JOINandAUTH. - Request additional servers using
GETSERVERS. - Cache successful alternatives.
- 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:
- Connect to a bootstrap or cached server.
- Offer WebSocket subprotocol
frog.v1. - Send
HELLO. - Register with
JOINandAUTH. - Stay connected while present.
- Request additional servers using
GETSERVERS. - Cache
TRYresults. - Use
FINDto discover random peers. - Use
LOOKUPto locate known peers. - Exchange signaling using
SIGNAL. - Reconnect and re-register after disconnect.
- 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:
- Accept WebSocket connections using subprotocol
frog.v1. - Maintain a stable server keypair.
- Derive and advertise its server_id.
- Enforce canonical server_uri syntax.
- Authenticate clients.
- Authenticate and authorize sister servers.
- Store temporary peer presence.
- Remove peer presence when registered connections close.
- Replace duplicate local peer key registrations only after successful authentication.
- Maintain verified sister records.
- Maintain live sister connections.
- Exchange server gossip.
- Provide
TRYalternatives. - Support local
FINDandLOOKUP. - Support recursive federated
@FINDand@LOOKUP. - Relay signaling traffic along route state.
- Enforce payload limits.
- Enforce rate limits.
- Expire stale route state.
- Bound memory growth.
- 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:
- Each side sends its raw Ed25519 public key over the data channel.
- Each side verifies that the public key hashes to the peer fingerprint part of the expected peer key.
- Each side verifies that the network part of the expected peer key is the expected application namespace.
- Each side sends a fresh random challenge.
- Each side signs a transcript-bound challenge string with its Ed25519 private key.
- 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