Skip to content

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:

json
{ "prefix": "game", "ttl": 43200, "addr": "223.117.153.115:44276" }
FieldDescription
prefixroom prefix
ttllifetime in seconds
addrthe caller's public address (learned via UDP probe)

Response 200:

json
{ "room_id": "game-a3f9c2", "host_addr": "223.117.153.115:44276" }

GET /room/{id}

Response 200:

json
{
  "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:

json
{ "addr": "223.117.153.115:44282" }

Response 200:

json
{ "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):

text
ECHO <token>

The server echoes (the source address is the client's NAT-mapped public address):

text
ADDR <token> <ip>:<port>

token is generated by the client (8 hex chars) to correlate responses.

3. Hole-punching protocol

Datagrams are ASCII text:

text
PUNCH <token>   # punch request
ACK <token>     # punch confirmation (token echoed)
ScenarioBehavior
receive PUNCH <t>reply ACK <t> (with retries), record peer address → direct
receive ACK <t> matching local tokendirect
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:

text
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_seq starts 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 with seq < 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_expected starts 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 for ack > 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:

text
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):

text
Client → Server: HELLO <room_id> <HOST|GUEST>
Server → Client: WAIT\r\n | OK\r\n | ERROR <reason>\r\n
ResponseMeaning
WAITslotted, waiting for the peer
OKpeer already waiting, paired
ERROR ROOM_EXPIRED / ERROR BAD_HELLO / ERROR BAD_ROLE / ERROR ALREADY_CONNECTED / ERROR NO_PEERrejected

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

frp-sh · 社交化 P2P 打洞工具