Skip to content

docs/JAMULUS_PROTOCOL.md: message reference, directory flows, small fixes - #3794

Open
mcfnord wants to merge 2 commits into
jamulussoftware:mainfrom
mcfnord:enhance-protocol-doc
Open

docs/JAMULUS_PROTOCOL.md: message reference, directory flows, small fixes#3794
mcfnord wants to merge 2 commits into
jamulussoftware:mainfrom
mcfnord:enhance-protocol-doc

Conversation

@mcfnord

@mcfnord mcfnord commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

One file. Fills the main gaps in the protocol doc, verified against src/protocol.cpp / src/socket.cpp / src/serverlist.cpp on current main:

  • How a receiver tells protocol packets from audio packets (they share one port; parse-or-audio in CSocket::ProcessPacket()), the CRC parameters, and how acknowledge/retransmit actually works — previously the doc said only "must be acknowledged".
  • A complete message-ID reference table for both message classes, with the legacy IDs called out. This is the piece a Wireshark user or protocol reader currently has to reconstruct from protocol.h.
  • A new section on directories: registration and refresh/timeout intervals, server-list responses (why both reduced and full lists are sent), and the CLM_SEND_EMPTY_MESSAGE/CLM_EMPTY_MESSAGE NAT hole-punch flow — previously undocumented here despite being a third of the connectionless messages.
  • Fixes message names that drifted from protocol.h (REQ_CHANNEL_INFOS, CHANNEL_INFOS, REQ_CONN_CLIENTS_LIST).

CHANGELOG: SKIP

🤖 Generated with Claude Code

@ann0see
ann0see self-requested a review July 17, 2026 22:08
Comment thread docs/JAMULUS_PROTOCOL.md Outdated
Comment thread docs/JAMULUS_PROTOCOL.md Outdated
Comment thread docs/JAMULUS_PROTOCOL.md Outdated
@ann0see ann0see added the AI AI generated or potentially AI generated label Jul 21, 2026
Comment thread docs/JAMULUS_PROTOCOL.md
Comment thread docs/JAMULUS_PROTOCOL.md
@ann0see
ann0see requested a review from softins July 23, 2026 09:17

@softins softins left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. Just a few comments and suggested changes.

Comment thread docs/JAMULUS_PROTOCOL.md
Comment thread docs/JAMULUS_PROTOCOL.md
Comment thread docs/JAMULUS_PROTOCOL.md Outdated
Comment thread docs/JAMULUS_PROTOCOL.md Outdated
Comment thread docs/JAMULUS_PROTOCOL.md Outdated
Comment thread docs/JAMULUS_PROTOCOL.md Outdated
@ann0see

ann0see commented Jul 26, 2026

Copy link
Copy Markdown
Member

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>
@mcfnord
mcfnord force-pushed the enhance-protocol-doc branch from 5d057a5 to 2386d21 Compare July 27, 2026 06:41
Comment thread docs/JAMULUS_PROTOCOL.md Outdated
Comment thread docs/JAMULUS_PROTOCOL.md Outdated
Comment thread docs/JAMULUS_PROTOCOL.md Outdated
…, 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>
Comment thread docs/JAMULUS_PROTOCOL.md

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mcfnord mcfnord Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, it needs fixing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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 control
  • PROTMESSID_DISCONNECTION, defined but with no case
  • ID 99, not defined at all
  • PROTMESSID_CLIENT_ID, which only ever travels server to client
  • PROTMESSID_JITT_BUF_SIZE with 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.

@pljones pljones Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ann0see
ann0see self-requested a review August 9, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI AI generated or potentially AI generated

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants