Protocol Specification
For developers who want a compatible client/server or a deep understanding. All protocols are simple text/binary formats with no encryption (--key encryption lives in the FRS1 layer, see below).
1. Signaling REST API
Base URL: signaling_addr (e.g. http://host:8080).
POST /room/create
Request:
{ "prefix": "game", "ttl": 43200, "addr": "223.117.153.115:44276" }| Field | Description |
|---|---|
prefix | room prefix |
ttl | lifetime in seconds |
addr | the caller's public address (learned via UDP probe) |
Response 200:
{ "room_id": "game-a3f9c2", "host_addr": "223.117.153.115:44276" }GET /room/{id}
Response 200:
{
"room_id": "game-a3f9c2",
"host_addr": "223.117.153.115:44276",
"guest_addr": "223.117.153.115:44282",
"created_at": 1724900000,
"expires_at": 1724943200
}guest_addr is null before a guest joins. Expired rooms return 404.
POST /room/{id}/join
Request:
{ "addr": "223.117.153.115:44282" }Response 200:
{ "room_id": "game-a3f9c2", "host_addr": "223.117.153.115:44276" }DELETE /room/{id}
204 on success, 404 if missing.
GET /health
Returns ok.
2. UDP public probe
The client sends to the probe port (same as HTTP, or signaling_udp):
ECHO <token>The server echoes (the source address is the client's NAT-mapped public address):
ADDR <token> <ip>:<port>token is generated by the client (8 hex chars) to correlate responses.
3. Hole-punching protocol
Datagrams are ASCII text:
PUNCH <token> # punch request
ACK <token> # punch confirmation (token echoed)| Scenario | Behavior |
|---|---|
receive PUNCH <t> | reply ACK <t> (with retries), record peer address → direct |
receive ACK <t> matching local token | direct |
receive an FRS1 frame (magic FRS1) | peer already in data phase → direct |
Punch targets = peer's advertised address ± spread ports (own port excluded). Window ~3s.
4. FRS1 reliable stream protocol
UDP datagrams, 15-byte header + payload:
offset size field
0 4 magic = "FRS1"
4 1 flags: 0x01=DATA, 0x02=FIN, 0=pure ACK
5 4 seq (u32 BE)
9 4 ack (u32 BE) — highest contiguous seq received + 1
13 2 len (u16 BE) — payload length
15 len payload (DATA frames; with --key, ciphertext incl. 16B Poly1305 tag)Sender state machine
next_seqstarts at 1; window is 32 frames- Data frames enter the window and are sent; windowed frames go into the retransmit queue
- On
ack=N: remove all frames withseq < N; when the FIN frame is acked (ack > fin_seq), close completes - 150ms timer: retransmit all unacknowledged windowed frames
- Idle 1s: send a pure ACK (keepalive, keeps NAT mapping alive)
Receiver state machine
next_expectedstarts at 1- Data frame with
seq == next_expected: deliver payload,next_expected += 1, reply ACK - Out-of-order/duplicate frames: drop (go-back-N), still reply ACK to hint the sender
- FIN received: set
rx_closed, reply ACK - Read buffer full (1MB): drop frames without advancing seq (triggers retransmit = flow control)
Encryption
With --key, DATA payload = ChaCha20-Poly1305.encrypt(nonce=seq, plaintext); the ciphertext includes a 16-byte auth tag; plaintext frames cap at 1184 bytes. ACK/FIN frames stay plaintext.
Close
shutdown(): send a FIN frame (seq = next_seq, into the retransmit queue), wait forack > fin_seq- 5s without confirmation: treat the peer as gone, best-effort close (no error)
Error handling
- Windows
WSAECONNRESET(10054)/WSAECONNREFUSED(10061): ignored (ICMP poisoning) - Peer socket closed: subsequent recv errors end the session
5. Tunnel framing protocol
A byte stream over the FRS1 stream (or relay TCP), for local TCP bridging and multi-connection reuse:
Guest → Host: "CNEW" # 4 bytes, new connection
Guest → Host: [u32 len BE][payload] # data frame, len ≤ 1 MiB
Guest → Host: [u32 0] # end frame
Host → Guest: [u32 len BE][payload] # data frame
Host → Guest: [u32 0] # end frame- While waiting for
CNEW, the host treats any other 4 bytes as a residual data-frame header and skips it (close-race protection) - End frames are acknowledged symmetrically: on local close, send an end frame and wait for the peer's; on receiving one, reply (if not already sent) and end the connection
6. Relay protocol
TCP text lines (\r\n terminated):
Client → Server: HELLO <room_id> <HOST|GUEST>
Server → Client: WAIT\r\n | OK\r\n | ERROR <reason>\r\n| Response | Meaning |
|---|---|
WAIT | slotted, waiting for the peer |
OK | peer already waiting, paired |
ERROR ROOM_EXPIRED / ERROR BAD_HELLO / ERROR BAD_ROLE / ERROR ALREADY_CONNECTED / ERROR NO_PEER | rejected |
After pairing, the server copies both directions; the ends are transparent. Pairing waits up to 10 minutes.
Compatibility notes
- Punching and relay phases use plain ASCII/binary for easy packet debugging
- frp-sh's protocols are not compatible with frp — they are self-designed