docs/JAMULUS_PROTOCOL.md: message reference, directory flows, small fixes - #3794
docs/JAMULUS_PROTOCOL.md: message reference, directory flows, small fixes#3794mcfnord wants to merge 2 commits into
Conversation
softins
left a comment
There was a problem hiding this comment.
This looks good. Just a few comments and suggested changes.
|
Please squash the commits here. |
…ixes - Explain how receivers classify datagrams (protocol frame vs audio) and specify the CRC and the acknowledge/retransmit mechanism - Add a complete message-ID reference for connection-based and connectionless messages - Add a section on directory registration, server lists and NAT hole punching (registration refresh/timeout intervals, CLM message flows) - Fix message names to match protocol.h (REQ_CHANNEL_INFOS, CHANNEL_INFOS, REQ_CONN_CLIENTS_LIST) - Apply style guide capitalisation (Client, Server, Directory, Channel, Jitter Buffer) per https://jamulus.io/contribute/Style-and-Tone - Move SPECIAL_SPLIT_MESSAGE (2001) out of the connection-based table into its own paragraph explaining it's a transport container, not a message type - Document the raw (uncompressed PCM) audio option alongside OPUS/OPUS64: how it's advertised (FS_RAW_AUDIO in CLM_SERVER_FEATURES), disabled (--noraw), and how the Server tells it apart from OPUS (packet-size comparison, no distinct message type) Co-Authored-By: Peter L Jones <pljones@users.noreply.github.com> Co-Authored-By: Tony Mountifield <tony@mountifield.org> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5d057a5 to
2386d21
Compare
…, set the scene for connection-based vs connectionless, state the retransmit bound - "connectionless UDP packets" was doubly redundant: UDP is inherently both connectionless and packet-based. - The text used "connection-based" and "established audio connection" right after calling the transport connectionless, with no explanation. Added a sentence defining what counts as connected in Jamulus terms, so the two message-class names have something to refer to. - The retransmit sentence stopped at "until the acknowledgement arrives". There is in fact no retry counter: OnTimerSendMess() re-sends and SendMessage() restarts the timer while the queue is non-empty, so the loop is bounded only by an ACKN popping the queue or CProtocol::Reset() clearing it (disconnect, time-out, protocol disabled). Stated that, plus the concrete 400 ms value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
||
| UDP offers no delivery guarantee and no notion of a connection, so Jamulus layers its own session and reliability semantics on top of it. A Client and Server count as *connected* once the Client is sending valid audio packets and the Server has assigned it a channel. The two message classes below are named relative to that session, not to anything at the transport level. | ||
|
|
||
| Messages with an ID below 1000 are connection-based: they apply to an established session, and each is acknowledged by an `ACKN (1)` message carrying the same sequence counter. Until that acknowledgement arrives, the sender retransmits the message every `SEND_MESS_TIMEOUT_MS` (400) ms. The protocol layer sets no retry limit: retransmission ends when the message is acknowledged, or when the channel clears the send queue via `CProtocol::Reset()` — on disconnect, on channel time-out, or when the protocol is disabled. |
There was a problem hiding this comment.
Mm, leaves me asking how big an exploit surface that is -- I don't think it's that large as there aren't very many protocol messages that a malicious client could send to a server after initiating a connection that require a response that could have the ACKN unsent. But a spoofable sender address would open this.
There was a problem hiding this comment.
🤖 AI: Traced it. CreateAndSendMessage() (protocol.cpp:577) is the only entry point onto the retransmit-until-ACK queue (SendMessQueue), used by 21 distinct message types — jitter-buffer size, client ID, channel gain/pan, mute state, connected-client list, channel info, chat text, network-transport props, split-message support, raw-audio support, licence-required, opus-supported, channel-level-list request, version/OS, recorder state. Audio doesn't go through this path — it's UDP fire-and-forget, separate from the ACK'd control-message queue.
SendMessQueue (protocol.h:321, a std::list) has no size cap — nothing like MAX_SIZE_MESS_QUEUE exists. There's also no dedup: EnqueueMessage() (protocol.cpp:519) unconditionally push_back()s on every call, with no check for an existing entry of the same iID. ACK handling only pops SendMessQueue.front() when its iCnt and iID match the incoming ACKN (protocol.cpp:709-719) — strict FIFO, one message retransmitted at a time regardless of queue depth.
So repeated calls to the same setter do pile up, and there's a legitimate path that does exactly that: CAudioMixerBoard::UpdateGainValue() (audiomixerboard.cpp:1858) emits ChangeChanGain on every unsuppressed fader update, unthrottled, which reaches CreateChanGainMes() via CChannel::SetRemoteChanGain() (channel.h:134). Measured directly with a minimal headless harness linking the unmodified protocol.cpp/.h and leaving MessReadyForSending unconnected, so no ACKN is ever processed: 200 calls to CreateChanGainMes() on one channel grow SendMessQueue by exactly one entry per call, zero pops, ending at depth 200; repeated at 50000 calls, still perfectly linear with no cap encountered. Combined with the unbounded retransmit window from the earlier reply, a connection whose ACKs are being dropped or spoofed accumulates one queue entry per UI action indefinitely, not just per handshake step.
There was a problem hiding this comment.
Ah, so every connection-oriented message, regardless of whether the recipient expects it (e.g. Client should not send something to Server...) get an ACKN back anyway?
There was a problem hiding this comment.
🤖 AI: Yes, and it is wider than the message type. For a connected channel the acknowledgement is unconditional for anything that passes the frame and CRC check: CreateAndImmSendAcknMess sits outside the switch ( iRecIDModified ) and outside the if ( bEvaluateMessage ) guard, so no branch inside either can suppress it.
Now it is measured, against a server built from an unmodified main, over loopback. Each of these was sent to a connected channel 10 times and acknowledged all 10:
PROTMESSID_REQ_JITT_BUF_SIZE, handled — the controlPROTMESSID_DISCONNECTION, defined but with nocase- ID 99, not defined at all
PROTMESSID_CLIENT_ID, which only ever travels server to clientPROTMESSID_JITT_BUF_SIZEwith a wrong-sized body- one part of a 3-part split message, so never reassembled or evaluated
The last two are the ones worth noting: a body the evaluator rejects on size still gets acknowledged, and so does a split fragment that is never evaluated at all — the sender is told the message arrived and was accepted when nothing has read it.
Negative controls, same rig, 0/10 each: an PROTMESSID_ACKN itself, a frame with a corrupted CRC, and a connectionless ID.
One bound on it — this needs an established channel. An unconnected peer sending the same undefined ID gets 0/10, because the protocol path calls FindChannel without allowing a new channel, so there is nothing to parse into.
There was a problem hiding this comment.
the sender is told the message arrived and was accepted when nothing has read it.
That part is explicitly intentional in the design, I believe.
The only issue here is the "forever" part, really.
One file. Fills the main gaps in the protocol doc, verified against
src/protocol.cpp/src/socket.cpp/src/serverlist.cppon current main:CSocket::ProcessPacket()), the CRC parameters, and how acknowledge/retransmit actually works — previously the doc said only "must be acknowledged".protocol.h.CLM_SEND_EMPTY_MESSAGE/CLM_EMPTY_MESSAGENAT hole-punch flow — previously undocumented here despite being a third of the connectionless messages.protocol.h(REQ_CHANNEL_INFOS,CHANNEL_INFOS,REQ_CONN_CLIENTS_LIST).CHANGELOG: SKIP
🤖 Generated with Claude Code