# SvxReflector protocol **Date:** 2026-10-07 **Scope:** The TCP control and UDP audio protocol a station uses to join one SvxReflector, as implemented by upstream SvxLink. Two self-contained memos: Protocol Version 2.0 and Protocol Version 3.0. Shared byte encoding is specified once and cited by both. Protocol 1.x is a short historical section because this server still accepts it and still emits a distinct talker body. **Not this:** A Go client; production FM-Funknetz credentials or a live probe of that reflector; reflector-to-reflector trunking; MQTT; talk-group mapping policy. **Upstream:** GitHub `sm0svx/svxlink` `master` HEAD on 2026-10-07 was still commit [`fcd13014810af75d1852b3f2cfc8d9806178d0ab`](https://github.com/sm0svx/svxlink/commit/fcd13014810af75d1852b3f2cfc8d9806178d0ab) (2026-07-26, “Prepare for release 26.05.1”). In that commit [`src/versions`](https://github.com/sm0svx/svxlink/blob/fcd13014810af75d1852b3f2cfc8d9806178d0ab/src/versions): `PROJECT=26.05.1`, `SVXLINK=1.10.1`, `SVXREFLECTOR=1.4.1`. There is no IETF RFC. If this text and the cited source disagree, the source wins. **Fork check:** `dj1jay/jayReflector` `main` on 2026-10-07 was still [`50997ed21a37c7c05c132eca5ebb07bab4b80fc7`](https://github.com/dj1jay/jayReflector/commit/50997ed21a37c7c05c132eca5ebb07bab4b80fc7). Client-facing TCP and UDP message types match upstream; see the note after the memos. The lab binary is now [GeuReflector](https://github.com/audric/GeuReflector) at [`dbad88e8c3e0f1adb8342db190bb0b3ad00106f4`](https://github.com/audric/GeuReflector/commit/dbad88e8c3e0f1adb8342db190bb0b3ad00106f4); the station message table in this file is unchanged. This note is **factual** except sections labeled **Inference**. --- ## How to read this file Part A is the common encoding. Both memos use it normatively. Part B is Protocol Version 2.0 (`TYPE=ReflectorV2`: HMAC-SHA1, plaintext UDP). An implementer of only version 2.0 reads Part A and Part B. Part C is Protocol Version 3.0 (`TYPE=Reflector`: X.509, TLS on TCP, AES-128-GCM on UDP). An implementer of only version 3.0 reads Part A and Part C. The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are used as in RFC 2119. “A client MUST” means the server at this commit rejects or drops the session otherwise, or the stock client will not interoperate. “This server does” describes `svxreflector` at the pinned commit and is not a requirement on other servers. --- # Part A — Common encoding ## A.1 Status of this part Implementation-derived description of serialization at commit `fcd13014810af75d1852b3f2cfc8d9806178d0ab`. Not an IETF standard. ## A.2 Protocol model Roles: | Role | What it is | | --- | --- | | Client | A station that opens TCP to one reflector and sends UDP audio. In this tree the clients are logic type `ReflectorV2` (announces 2.0) and logic type `Reflector` (announces 3.0). | | Reflector | The `svxreflector` process. It accepts many clients and forwards audio inside a talk group. | | Talk group | A `uint32` number. Audio from the current talker is copied to other clients that have selected that talk group, and talker indications are also sent to clients that monitor it. | | Talker | At most one client per talk group whose audio is forwarded. The server assigns the talker from the first audio datagram on that talk group. | One TCP connection is one client. The server assigns a `uint16` client id in the range 1 through 65535, chosen with `std::mt19937` and skipped forward if that id is in use [ReflectorClient.cpp `newClientId`, lines 396–407]. ## A.3 Byte order and primitive types All multi-byte integers are big-endian. `Async::Msg` packs them with `htobe16`, `htobe32`, and `htobe64`, and unpacks with `be16toh`, `be32toh`, and `be64toh` [AsyncMsg.h `Packer16` / `Packer32` / `Packer64`, lines 286–385]. | Type in the C++ message | On the wire | | --- | --- | | `uint8_t`, `int8_t`, `char` | 1 byte, no endian swap | | `uint16_t`, `int16_t` | 2 bytes, big-endian | | `uint32_t`, `int32_t` | 4 bytes, big-endian | | `uint64_t`, `int64_t` | 8 bytes, big-endian | | `float` | 4 bytes, big-endian bit pattern (same overlay as `uint32_t`) | | `double` | 8 bytes, big-endian bit pattern (same overlay as `uint64_t`) | | `std::string` | `uint16` byte count, then that many bytes. The count is `val.size()`. No trailing NUL is included. A string longer than 65535 bytes fails to pack. Embedded NUL bytes are data. | | `std::vector` | `uint16` element count, then each element packed as `T`. A `vector` is therefore a `uint16` length and then raw bytes. | | `std::set` | `uint16` element count, then each element in set order. For `set` that order is ascending numeric order. | | `std::array` and `T[N]` | N elements, no count prefix | [AsyncMsg.h `MsgPacker` lines 407–445; `MsgPacker` lines 447–496; `MsgPacker` lines 498–555.] A derived message class does **not** pack its base class unless it uses `ASYNC_MSG_DERIVED_FROM`. The reflector messages do not. The sender writes the type with a separate `ReflectorMsg` or `ReflectorUdpMsg` / `ReflectorUdpMsgV2` header, then packs the derived object, which writes only its own members [ReflectorClient.cpp `sendMsg` lines 272–288; ReflectorV2Logic.cpp `sendMsg` lines 1298–1328]. `ASYNC_MSG_NO_MEMBERS` therefore means a body of zero bytes after the type header. ## A.4 TCP framing TCP carries a sequence of frames. Each frame is: ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | payload length (32 bits) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + payload + | (length bytes) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` The length is the number of payload bytes and does not include itself. It is written as four big-endian bytes [AsyncFramedTcpConnection.h `QueueItem`, lines 292–307]. The receiver closes the connection with disconnect reason protocol error if the length is greater than its current maximum receive size [AsyncFramedTcpConnection.cpp lines 262–266]. The payload is one reflector message: ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | message type (16) | message body ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` The type is a `uint16`. The body is the packed members of that message class and nothing else. Maximum payload sizes enforced by the reflector messages [ReflectorMsg.h lines 110–112]: | Constant | Bytes | When this server applies it | | --- | --- | --- | | `MAX_PREAUTH_FRAME_SIZE` | 64 | Receive limit from TCP accept until protocol 3 starts TLS, and the version 2.0 client’s limit until `MsgAuthOk` | | `MAX_SSL_SETUP_FRAME_SIZE` | 4096 | Server receive limit after it has sent `MsgStartEncryption`, until authentication completes | | `MAX_POSTAUTH_FRAME_SIZE` | 32768 | Server transmit limit for the whole session. Server receive limit after authentication. Version 3.0 client limit for the whole session. Version 2.0 client limit after `MsgAuthOk`. | The framing class’s own default, before those calls, is 1 MiB [AsyncFramedTcpConnection.h line 290]. Both sides unpack the type first. A payload shorter than 2 bytes fails that unpack. The version 2.0 client then disconnects. This server sends `MsgError` with `"Protocol error"` [ReflectorClient.cpp lines 506–513; ReflectorV2Logic.cpp lines 843–849]. Unknown type numbers are ignored after the type has been unpacked. Ignoring them still counts as traffic for the TCP receive timer. Application types (type ≥ 100) received before the authenticated state are specified in each memo. ## A.5 TCP message catalogue Type numbers are the template argument of `ReflectorMsgBase` in ReflectorMsg.h. Direction is who sends the message in this tree. “Either” means the class is handled on both sides. | Type | Class | Body after the type | Direction | | --- | --- | --- | --- | | 1 | `MsgHeartbeat` | empty | Both | | 5 | `MsgProtoVer` | `uint16` major, `uint16` minor | Client, first message | | 6 | `MsgProtoVerDowngrade` | `uint16` major, `uint16` minor | Server | | 10 | `MsgAuthChallenge` | `vector` of 20 bytes | Server, major < 3, and one protocol 3 fallback | | 11 | `MsgAuthResponse` | `string` callsign, then `vector` digest | Client | | 12 | `MsgAuthOk` | empty | Server | | 13 | `MsgError` | `string` message | Either. Receiver drops the connection. | | 14 | `MsgStartEncryptionRequest` | empty | Client, major ≥ 3 | | 15 | `MsgStartEncryption` | empty | Server, major ≥ 3 | | 16 | `MsgClientCsrRequest` | empty | Server, major ≥ 3 | | 17 | `MsgClientCsr` | `string` PEM | Client, major ≥ 3 | | 18 | `MsgClientCert` | `string` PEM | Server, major ≥ 3 | | 19 | `MsgCAInfo` | `uint16` PEM size, `vector` SHA-256 | Server, major ≥ 3 | | 20 | `MsgCABundleRequest` | empty | Client, major ≥ 3 | | 21 | `MsgCABundle` | `string` CA PEM, `vector` signature, `string` signing-certificate PEM | Server, major ≥ 3 | | 100 | `MsgServerInfo` | `uint16` reserved, `uint16` client id, `vector` nodes, `vector` codecs | Server, after login | | 101 | `MsgNodeList` | `vector` callsigns | Server, only if client version < 0.7 | | 102 | `MsgNodeJoined` | `string` callsign | Server to other clients | | 103 | `MsgNodeLeft` | `string` callsign | Server to other clients | | 104 | `MsgTalkerStart` | `uint32` talk group, `string` callsign | Server to clients with major ≥ 2 | | 104 | `MsgTalkerStartV1` | `string` callsign | Server to clients with major = 1. Same type number, shorter body. | | 105 | `MsgTalkerStop` | `uint32` talk group, `string` callsign | Server to clients with major ≥ 2 | | 105 | `MsgTalkerStopV1` | `string` callsign | Server to clients with major = 1 | | 106 | `MsgSelectTG` | `uint32` talk group | Client | | 107 | `MsgTgMonitor` | `set` talk groups | Client | | 108 | — | Not compiled. The old site-list `MsgNodeInfo` is inside `#if 0` [ReflectorMsg.h lines 944–1136]. | — | | 109 | `MsgRequestQsy` | `uint32` talk group. Zero means “server chooses”. | Client request; server fan-out to major ≥ 2 on that talk group | | 110 | `MsgStateEvent` | `string` src, `string` name, `string` msg | Client. This server logs it and does not fan it out. The stock clients’ send call is inside a comment. | | 111 | `MsgNodeInfo` | `vector` IV random, `vector` UDP key, `string` JSON | Client with major ≥ 3 | | 111 | `MsgNodeInfoV2` | `string` JSON | Client with major < 3. Same type number. | | 112 | `MsgSignalStrengthValues` | `vector` of receiver records | Client | | 113 | `MsgTxStatus` | `vector` of transmitter records | Client | | 114 | `MsgStartUdpEncryption` | empty | Server, major ≥ 3 | `MsgAuthChallenge::LENGTH` is 20. The body is a vector, so the wire form is `uint16` 20 followed by 20 bytes from `RAND_bytes`. If `RAND_bytes` fails, the vector is cleared and this server disconnects without sending the message [ReflectorMsg.h lines 366–381; ReflectorClient.cpp `sendAuthChallenge` lines 1401–1412]. `MsgAuthResponse` member order in the pack macro is callsign, then digest, even though the fields are declared in the other order [ReflectorMsg.h line 464]. `DIGEST_LEN` is 20. The digest vector is `uint16` 20 followed by 20 bytes. How those 20 bytes are computed is in Part B. `MsgServerInfo` always writes `m_reserved`, which this server leaves at 0 [ReflectorMsg.h lines 720–724]. The node list is filled after construction and includes every connected client that already has a callsign, including the client being welcomed, because the callsign is stored before `nodeList()` [ReflectorClient.cpp lines 1344–1358; Reflector.cpp `nodeList` lines 377–388]. Codec names are a vector; this server puts one name in it. Receiver record inside type 112, and the same record inside UDP type 104 [ReflectorMsg.h lines 1270–1302]: ``` 0 1 2 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | id (char) | siglev (uint8)| flags | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` Flag bits, from bit 0: enabled, squelch open, active. The C++ constructor parameter is `int16_t`, and the stock client clamps the value to 0..100, but the packed field is `uint8_t`. A vector of these records is prefixed with a `uint16` count. Transmitter record inside type 113 [ReflectorMsg.h lines 1341–1368]: `char` id, then `uint8` flags. Bit 0 means transmitting. A `uint16` count precedes the records. Type 110’s three strings are independent length-prefixed strings. ## A.6 UDP message types These type numbers live in a different namespace from the TCP types. They are the template argument of `ReflectorUdpMsgBase`. | Type | Class | Body after the UDP header of the version in use | Direction | | --- | --- | --- | --- | | 1 | `MsgUdpHeartbeat` | empty | Both | | 101 | `MsgUdpAudio` | `vector` encoded audio. No talk-group field. A variant that included a `uint32` talk group is inside `#if 0` and is not compiled [ReflectorMsg.h lines 1423–1444 and 1586–1618]. | Both | | 102 | `MsgUdpFlushSamples` | empty | Both. “End of this audio stream.” | | 103 | `MsgUdpAllSamplesFlushed` | empty | Both. “The samples have been written to the local audio sink.” | | 104 | `MsgUdpSignalStrengthValues` | same receiver-record vector as TCP type 112 | Client. This server accepts it. The stock client sends TCP type 112 instead; its UDP send is commented out. | The header in front of that body depends on the major version. Part B specifies the version 2 header. Part C specifies the version 3 header and the cipher. One audio datagram carries one encoder callback: the bytes from a single `writeEncodedSamples` call, stored in the vector. An empty vector is legal and is not passed to the decoder. ## A.7 Addresses This server binds TCP and UDP to the same `LISTEN_PORT`. The code default, if the configuration key is absent, is the string `"5300"` for TCP and the integer 5300 for UDP [Reflector.cpp lines 284–300]. The shipped `svxreflector.conf.in` sets `LISTEN_PORT=5300`. The man page says clients do not open inbound ports [svxreflector.conf.5 `LISTEN_PORT`]. The client sends UDP to `remoteHost()` and `remotePort()` of the TCP connection, which are the reflector’s address and that same port [ReflectorV2Logic.cpp `sendUdpMsg` lines 1487–1488]. The client’s UDP socket is constructed with local port 0, so the kernel chooses an ephemeral port [AsyncUdpSocket.h line 134; ReflectorV2Logic.cpp line 1055]. This server learns that ephemeral port from the first acceptable UDP datagram and then requires later datagrams to use it [Reflector.cpp lines 1095–1107]. Until that datagram arrives, the server has no UDP destination and its UDP sends return immediately [ReflectorClient.cpp `sendUdpMsg` lines 323–328]. DNS, when the client is configured with `DNS_DOMAIN`, is a lookup of SRV service `svxreflector` / `tcp` [ReflectorV2Logic.cpp line 223; ReflectorLogic.cpp line 260]. The UDP port is the TCP SRV port. There is no separate UDP SRV name in this client. ## A.8 Version admission `MsgProtoVer` is the first TCP payload the client sends. The class default is major 3, minor 0. The version 2.0 client constructs `(2, 0)` [ReflectorV2Logic.cpp line 127]. This server accepts a version that is greater than or equal to 0.6 and less than or equal to 3.0. Comparison is major, then minor [ProtoVer.h `operator<` lines 145–149; ReflectorClient.h lines 509–510; ReflectorClient.cpp `handleMsgProtoVer` lines 607–660]. | Announced version | This server | | --- | --- | | Greater than 3.0, and this is the first version message | Sends `MsgProtoVerDowngrade` with major 3, minor 0, and stays in the expect-version state | | Greater than 3.0 again | `MsgError` `"Unsupported protocol version M.N. May be at most 3.0."` | | Less than 0.6 | `MsgError` `"Unsupported protocol version M.N. Must be at least 0.6."` | | Major ≥ 3, within range | Part C | | Major < 3, within range | `MsgAuthChallenge`, then Part B’s authentication, with the version-dependent body variants in A.5 | `ProtoVer::operator!=` compares the major number only [ProtoVer.h lines 141–144]. The admission tests do not use it. A second `MsgProtoVer` outside the expect-version state produces `MsgError` `"Protocol version expected"`. ## A.9 Callsign grammar This server applies `callsignOk` to the authentication callsign and to a protocol 3 certificate common name [Reflector.cpp lines 650–701]. The callsign MUST be non-empty. It MUST match `ACCEPT_CALLSIGN` as a full `std::regex_match`. If that key is missing or empty, the pattern compiled into the server is: ``` [A-Z0-9][A-Z]{0,2}\d[A-Z0-9]{0,3}[A-Z](?:-[A-Z0-9]{1,3})? ``` `std::regex` with no flags uses the ECMAScript grammar. The match is case-sensitive. This server does not rewrite case. A callsign that fails the pattern produces `MsgError` `"Invalid callsign"`. If `REJECT_CALLSIGN` is set and the callsign matches it, the callsign is rejected the same way. The default configuration sets no reject pattern [svxreflector.conf.5]. A second simultaneous session with the same callsign produces `MsgError` `"Access denied"` [ReflectorClient.cpp lines 1336–1377]. ## A.10 Codec names `MsgServerInfo` carries codec names as strings. This server’s `CODECS` value is split on commas, and only the first name is kept. If `CODECS` is unset, the name is `OPUS` when Opus encode and decode are both compiled in, otherwise `SPEEX` when Speex is compiled in, otherwise `GSM` [ReflectorClient.cpp lines 214–239]. Names the audio library recognizes, when the corresponding library is compiled in [AsyncAudioEncoder.cpp `isAvailable` lines 123–134]: `NULL`, `RAW`, `S16`, `GSM`, `SPEEX`, `OPUS`, `DUMMY`. The man page lists `OPUS`, `SPEEX`, `GSM`, `S16` (signed 16-bit), and `RAW` (32-bit float) [svxreflector.conf.5 `CODECS`]. The client walks the server’s list and uses the first name for which both an encoder and a decoder exist. The selected name is not echoed. The audio datagram does not repeat it. Opus parameters are in each memo’s audio section; both clients use the same encoder class. ## A.11 Talk groups, talker, and QSY `MsgSelectTG` replaces the client’s current talk group. If the number differs from the current one, and this client is the talker, this server sends `MsgUdpFlushSamples` to the other clients on that talk group. If someone else is the talker, this server sends `MsgUdpFlushSamples` to the client that is leaving. There is no TCP acknowledgement. If the section `TG#` has `ALLOW` and the callsign does not match that regular expression, or if the expression is invalid, this server forces the client to talk group 0 and does not tell the client [ReflectorClient.cpp `handleSelectTG` lines 855–881 and `setTg` lines 1452–1466; TGHandler.cpp `allowTgSelection` lines 266–289]. The source comment marks the missing notification as a FIXME. `MsgTgMonitor` replaces the idle-monitor set. This server drops talk group 0 and any talk group that fails `ALLOW` or `ALLOW_MONITOR`. It does not send an error for the dropped numbers [ReflectorClient.cpp lines 885–913]. Audio is forwarded only when the client’s selected talk group is non-zero, the client is not in a talker block, and the client is the talker for that talk group. The first acceptable audio datagram makes that client the talker [Reflector.cpp lines 1159–1192]. While a client remains the talker, each forwarded audio datagram refreshes the talker timestamp. A periodic 1-second timer clears the talker when `diff.tv_sec > 3` after the last refresh (`TALKER_AUDIO_TIMEOUT` is 3) [TGHandler.h line 180; TGHandler.cpp lines 363–371]. Clearing the talker sends TCP talker-stop and UDP flush as below. On talker start, this server sends `MsgTalkerStart` (type 104, with talk group) to clients whose version is ≥ 2.0 and that have selected or monitor that talk group. On talker stop it sends `MsgTalkerStop` to the same filter, and `MsgUdpFlushSamples` to the other clients who have selected that talk group. Clients whose major version is 1, and only when the talk group equals `TG_FOR_V1_CLIENTS`, receive `MsgTalkerStartV1` / `MsgTalkerStopV1` (type 104 / 105, callsign only) [Reflector.cpp `onTalkerUpdated` lines 1297–1333]. The class comments say UDP audio may arrive before talker-start and after talker-stop, and that the TCP messages are informational [ReflectorMsg.h lines 797–805 and 848–856]. `TG_FOR_V1_CLIENTS` in the shipped configuration template is 999. If the key is absent, the binary member is initialized to 1 [Reflector.cpp line 232; svxreflector.conf.in line 14]. `MsgRequestQsy` from a client whose current talk group is 0 is ignored. Otherwise this server broadcasts `MsgRequestQsy` to version ≥ 2.0 clients that have selected the requester’s current talk group, including the requester. If the requested talk group is 0, the server walks `RANDOM_QSY_RANGE` for an empty talk group. If that range was not configured, it sends nothing [Reflector.cpp `requestQsy` lines 459–482 and `nextRandomQsyTg` lines 1409–1432]. The range format in the man page is `lower:count`. `SQL_TIMEOUT` defaults to 0, which disables the long-transmission block. When it is non-zero, the 1-second timer counts it down while the client is talker and then calls `setBlock` for `SQL_TIMEOUT_BLOCKTIME` seconds. The code default for that block is 60, and the value is raised to at least 1 [Reflector.cpp lines 317–323; TGHandler.cpp lines 131–133 and 374–379]. The man page says the block default is one second. The code path above is what this binary does when the key is absent. A blocked client’s UDP audio and UDP signal-strength datagrams are discarded. Other UDP types are still accepted. Receiving audio restarts the block timer if a block is already set [ReflectorClient.cpp lines 316–319]. ## A.12 Heartbeat timers shared by both clients Both stock clients use a periodic timer of 1000 ms, started when TCP connects [ReflectorV2Logic.cpp line 142; ReflectorLogic.cpp line 174]. | Counter | Reset value | Effect when it reaches zero | | --- | --- | --- | | TCP transmit | 10 | Send `MsgHeartbeat` | | TCP receive | 15 | Client disconnects | | UDP transmit | 15, or `UDP_HEARTBEAT_INTERVAL` if set | Send a UDP heartbeat (version 3 retransmits registration while waiting; see Part C) | | UDP receive | 60 | Client disconnects | Any successful TCP send resets the TCP transmit counter. Any unpacked TCP frame resets the TCP receive counter. A successfully sequenced UDP datagram resets the UDP receive counter. These counters start at TCP connect, so the UDP receive limit is running before the UDP socket exists. This server’s timer also fires every 1000 ms, and it is enabled when the client object is constructed, which is at TCP accept [ReflectorClient.cpp lines 188–191; AsyncTimer.h line 142 default `enabled=true`]. | Server counter | Reset | Effect at zero | | --- | --- | --- | | TCP transmit | 10 | `MsgHeartbeat` | | TCP receive | 15 | `MsgError` `"TCP heartbeat timeout"`, then close | | UDP transmit | 15 | `MsgUdpHeartbeat`, if a UDP destination is known | | UDP receive | 120 | `MsgError` `"UDP heartbeat timeout"`, then close | [ReflectorClient.h lines 512–515; ReflectorClient.cpp `handleHeartbeat` lines 1259–1298.] `MsgError` disables the server heartbeat timer, sends the error, and arms a one-shot disconnect timer of 10000 ms [ReflectorClient.cpp lines 1210–1226]. The client closes as soon as it unpacks `MsgError`. ## A.13 Historical protocol 1 This commit has no `ReflectorV1` client. The server still accepts major version 1, provided the minor version is at least 6 when the major is 0, and any minor when the major is 1, up through 3.0. What is distinct for major version 1: - UDP uses the version 2 header in Part B, because the encrypted header starts at major 3. - Authentication is the HMAC challenge in Part B. - After login the server sets the talk group to `TG_FOR_V1_CLIENTS`. The client cannot rely on its own `MsgSelectTG` staying in force; the next server-side `setTg` for that client uses the forced value at login [ReflectorClient.cpp lines 1366–1369]. - Talker TCP messages are types 104 and 105 with a callsign and no talk-group field. - `MsgRequestQsy` is not sent to these clients. - If the version is below 0.7, `MsgServerInfo` is followed by `MsgNodeList` (type 101) carrying the same callsign vector. Major 0, minor ≥ 6, is also on the challenge path and the version 2 UDP header. It is not in the “version 1” talker filter (`ProtoVer(1,0)` through `ProtoVer(1,999)`) and it is not in the “≥ 2.0” filter, so this server does not send it talker-start or talker-stop. It can still send `MsgSelectTG`. `MsgNodeList` is sent when the version is below 0.7 [Reflector.cpp lines 195–200 and 1360–1364]. ## A.14 References for Part A Blob prefix: `https://github.com/sm0svx/svxlink/blob/fcd13014810af75d1852b3f2cfc8d9806178d0ab/` | Topic | Symbol | Path | | --- | --- | --- | | Integer, string, vector, set packing | `MsgPacker`, `ASYNC_MSG_MEMBERS` | `src/async/core/AsyncMsg.h` | | TCP length prefix and max frame | `FramedTcpConnection::QueueItem`, `onDataReceived` | `src/async/core/AsyncFramedTcpConnection.h`, `.cpp` | | Type numbers and bodies | `ReflectorMsgBase`, `ReflectorUdpMsgBase` | `src/svxlink/reflector/ReflectorMsg.h` | | Header then body | `ReflectorClient::sendMsg` | `src/svxlink/reflector/ReflectorClient.cpp` | | Version limits | `MIN_MAJOR_VER`, `handleMsgProtoVer` | `src/svxlink/reflector/ReflectorClient.h`, `.cpp` | | Version comparison | `ProtoVer::operator<` | `src/svxlink/reflector/ProtoVer.h` | | Ports | `Reflector::initialize` | `src/svxlink/reflector/Reflector.cpp` | | Callsign | `Reflector::callsignOk` | `src/svxlink/reflector/Reflector.cpp` | | Codec list | `ReflectorClient` constructor | `src/svxlink/reflector/ReflectorClient.cpp` | | Talker and QSY | `onTalkerUpdated`, `requestQsy` | `src/svxlink/reflector/Reflector.cpp` | | Talker timeout | `TALKER_AUDIO_TIMEOUT`, `checkTimers` | `src/svxlink/reflector/TGHandler.h`, `TGHandler.cpp` | | Man-page defaults | `LISTEN_PORT`, `CODECS`, `ACCEPT_CALLSIGN` | `src/doc/man/svxreflector.conf.5` | --- # Part B — SvxReflector Protocol Version 2.0 ## B.1 Status of this memo Implementation-derived description of the version 2.0 session at commit `fcd13014810af75d1852b3f2cfc8d9806178d0ab`. Not an IETF standard. Encoding rules, the message catalogue, callsign grammar, talk-group rules, and the server timers are Part A. This memo does not use the certificate or UDP-cipher procedures in Part C. ## B.2 Abstract A client opens one TCP connection, sends protocol version 2.0, proves knowledge of a shared key with HMAC-SHA1 over a server nonce, receives a client id and a codec name, then exchanges plaintext UDP datagrams with that reflector on the same host and port. Audio for a talk group has a single talker. The UDP datagrams are not encrypted and are not authenticated beyond the source address and the client id. ## B.3 Table of contents - B.4 Introduction - B.5 Terminology - B.6 Transport - B.7 UDP header - B.8 Session - B.9 State diagram - B.10 Timing - B.11 Audio - B.12 Security considerations - B.13 Differences from version 3.0 - B.14 References - B.15 Unresolved ## B.4 Introduction Version 2.0 is the session spoken by logic type `ReflectorV2`. On TCP connect that client sends `MsgProtoVer` with major 2 and minor 0 [ReflectorV2Logic.cpp lines 127 and 785–801]. FM-Funknetz’s public instruction to set `TYPE=ReflectorV2` selects this session. Whether a given production reflector still accepts it was not measured for this memo. A client that only implements this memo MUST send major 2 (or another major less than 3 that this server accepts) and MUST implement the HMAC response. Sending major 3 selects Part C on this server. ## B.5 Terminology The Part A keyword rule applies. A client MUST send version 2.0 as its first TCP payload if it wants this session on this server. This server answers major values less than 3, down to 0.6, with the same HMAC exchange. Major 1 additionally follows Part A.13. This server checks the HMAC. A client that sends a wrong digest receives `MsgError` `"Access denied"` and is not logged in. ## B.6 Transport The client MUST connect TCP to the reflector. The stock client requires `HOSTS` or `DNS_DOMAIN` [ReflectorV2Logic.cpp lines 213–223]. UDP is sent to the TCP peer’s address and port, from an ephemeral local port, as in Part A.7. Until `MsgAuthOk`, the stock client sets the maximum TCP payload to 64 bytes [ReflectorV2Logic.cpp line 801]. A version 2.0 `MsgAuthResponse` for a callsign that matches the default grammar fits: type (2) + callsign (2 + at most 12) + digest (2 + 20) is at most 38 bytes. After `MsgAuthOk` the client raises the maximum to 32768 [ReflectorV2Logic.cpp line 986]. This server’s receive maximum stays 64 until authentication, then becomes 32768. Its transmit maximum is 32768 from accept [ReflectorClient.cpp lines 199–200 and 1343]. ## B.7 UDP header For every major version less than 3, including 2.0, each UDP datagram is: ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | type (16) | client id (16) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | sequence (16) | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | message body | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` `type`, `client id`, and `sequence` are big-endian `uint16` [ReflectorUdpMsgV2, ReflectorMsg.h lines 210–257]. The body is A.6. The client id is the value from `MsgServerInfo`. The sender increments a `uint16` sequence once per datagram it transmits, starting at 0 when the TCP connection is established [ReflectorV2Logic.cpp lines 797–798 and 1479]. The receiver computes `uint16 diff = received - expected`, wrapping modulo 65536. - If `diff > 32767`, the datagram is old. The stock client and this server drop it and do not advance the expected sequence. - If `diff` is 1 or more and at most 32767, the datagram is accepted and the expected sequence becomes `received + 1`. Lost numbers are not retransmitted. - If `diff` is 0, the datagram is the expected one and the expected sequence becomes `received + 1`. [ReflectorV2Logic.cpp lines 1400–1417; Reflector.cpp lines 1129–1147.] The stock client also drops a datagram whose source address is not the TCP peer, whose source port is not the TCP peer’s port, or whose client id is not the id from `MsgServerInfo` [ReflectorV2Logic.cpp lines 1366–1398]. This server, for a version less than 3, looks up the client by the header’s client id, drops the datagram if the source address is not that client’s TCP address, remembers the source port from the first accepted datagram, and then requires that port [Reflector.cpp lines 1056–1107]. The lookup does not require the datagram to be encrypted. `sendUdpDatagram` for version less than 3 writes the version 2 header and calls the plaintext UDP write. The sequence it writes is `udpCipherIVCntrNext() & 0xffff`, a `uint32` counter that starts at 0 and is post-incremented, truncated to 16 bits [Reflector.cpp lines 431–439; ReflectorClient.h line 481]. ## B.8 Session The stock client states that are actually assigned are: disconnected, expect authentication challenge, expect authentication ok, expect server info, connected [ReflectorV2Logic.h lines 198–202]. `STATE_AUTHENTICATED` sits in the enum and is not assigned; server-info handling requires the expect-server-info state, which is already past it, so node information is allowed. ### B.8.1 Connect and version 1. The client opens TCP. On connect it sends `MsgProtoVer(2, 0)`, zeros both UDP sequences, starts the 1-second timer, and waits for a challenge [ReflectorV2Logic.cpp lines 785–801]. 2. This server, for major 2, sends `MsgAuthChallenge`. If the server sends `MsgProtoVerDowngrade`, this client prints that it cannot downgrade and disconnects [ReflectorV2Logic.cpp lines 926–940]. A client that wants to stay on 2.0 MUST treat a downgrade request as fatal or MUST send a new `MsgProtoVer` no higher than the server’s number and then speak only that version. This particular client does the former. This server will accept a following `MsgProtoVer(2, 0)`. ### B.8.2 Authentication The challenge body is 20 random bytes (Part A.5). The client rejects a challenge whose vector length is not 20 [ReflectorV2Logic.cpp lines 962–968]. The client computes the digest as follows [ReflectorMsg.h `calcHMAC` lines 470–484]: 1. The key is the `AUTH_KEY` string’s raw bytes. The stock client refuses to start if `AUTH_KEY` is missing, empty, or the placeholder `Change this key now!` [ReflectorV2Logic.cpp lines 263–274]. The key is not hex-decoded and is not sent on the wire. 2. The MAC is HMAC-SHA1: OpenSSL `EVP_PKEY_HMAC` via `newRawPrivateKey`, then `signInit("sha1")`, over exactly the 20 challenge bytes (`MsgAuthChallenge::LENGTH`). The callsign is not an input to the MAC. 3. The MAC output is 20 bytes. The client sends `MsgAuthResponse`: the callsign string, then the digest vector. The callsign is the configured string, without case conversion [ReflectorV2Logic.cpp line 970]. This server then: 1. Requires state expect-auth-response. Otherwise `MsgError` `"Authentication response unexpected"`. 2. Requires the body to unpack. Otherwise `"Illegal MsgAuthResponse protocol message received"`. 3. Requires `callsignOk` (Part A.9). Otherwise `"Invalid callsign"`. 4. Looks up `[USERS]` for that exact callsign to get a password-group name, then `[PASSWORDS]` for that group to get the key string. It recomputes HMAC-SHA1 and compares with `CRYPTO_memcmp`. Empty key or a mismatch yields `"Access denied"` [ReflectorClient.cpp lines 718–781 and `lookupUserKey` lines 1315–1333]. 5. If the callsign is already in the connected-callsign map, `"Access denied"`. On success this server sets the receive frame maximum to 32768, sends `MsgAuthOk`, sends `MsgServerInfo`, sends `MsgNodeList` only when the version is < 0.7, forces the talk group when the version is < 2.0, and broadcasts `MsgNodeJoined` to the other clients [ReflectorClient.cpp lines 1336–1371]. ### B.8.3 Server info, codec, node info, talk group The client accepts `MsgServerInfo` only in the expect-server-info state. It stores the client id. It selects the first codec name it can both encode and decode (Part A.10). If none match, it logs that and continues; it does not disconnect. The encoder remains the `DUMMY` encoder installed at startup, so it will not emit audio [ReflectorV2Logic.cpp lines 990–1052 and 341]. It then builds a single-line JSON object and sends it as `MsgNodeInfoV2` (type 111, one string). The stock object contains at least: | Key | Value | | --- | --- | | `sw` | `"SvxLink"` | | `swVer` | the SvxLink application version string | | `projVer` | the project version string | If `NODE_INFO_FILE` is set, that JSON is parsed first and those three keys are then overwritten. Version 2.0 does not add `machineArch` [ReflectorV2Logic.cpp lines 425–460 and 1061–1069]. A client SHOULD send type 111 in the version 2 layout (a single string). This server selects the layout from the announced major version. A major below 3 that sends the version 3 layout fails the unpack and receives `"Illegal MsgNodeInfo protocol message received"` [ReflectorClient.cpp lines 917–950]. A JSON value this server cannot parse produces a warning and does not drop the connection. No JSON keys are required for audio. If the client’s configured talk group is non-zero, it sends `MsgSelectTG`. If it has monitor talk groups, it sends `MsgTgMonitor`. It then sends a UDP heartbeat [ReflectorV2Logic.cpp lines 1117–1128]. `MsgSelectTG` and `MsgTgMonitor` may be sent again later. The stock client also sends `MsgRequestQsy` when its local command logic asks for a QSY [ReflectorV2Logic.cpp lines 516–525]. A client that never sends type 109 is accepted. ### B.8.4 Audio, flush, and talker indications The client sends `MsgUdpAudio` when the encoder emits a frame, and `MsgUdpFlushSamples` when the encoder finishes a transmission. It arms a 3000 ms one-shot timer on flush. If that timer fires, it tells the encoder that all samples are flushed, which causes `MsgUdpAllSamplesFlushed` [ReflectorV2Logic.cpp lines 1331–1355 and 1529–1538]. This server, on `MsgUdpFlushSamples` from the current talker, clears the talker and immediately sends `MsgUdpAllSamplesFlushed` back to that client. It does not wait for the other clients. The source comment says waiting would not scale [Reflector.cpp lines 1232–1250]. Incoming `MsgUdpAllSamplesFlushed` is ignored by the server. The client uses it to complete the local encoder flush [ReflectorV2Logic.cpp lines 1449–1451]. `MsgTalkerStart` for version 2.0 includes the talk group. The stock client, if it is idle, selects that talk group. If it is using monitor priorities and the new talk group has a higher priority, it switches. It does not drop the TCP session when it ignores a talker message [ReflectorV2Logic.cpp lines 1192–1234]. A client that has already selected one talk group and does not want to follow other talk groups MUST still accept the message, and MAY ignore it. Audio does not wait for the TCP message: the client writes any non-empty audio payload from an accepted UDP datagram into the decoder [ReflectorV2Logic.cpp lines 1426–1441]. `MsgTalkerStop` is logged and emitted as a local event. The stock client does not treat it as the end of audio by itself. The decoder is flushed by `MsgUdpFlushSamples`, or by the local 3-second gap rule in B.10. `MsgNodeJoined` and `MsgNodeLeft` are informational. A failed unpack disconnects the stock client. ### B.8.5 Errors and disconnect This client disconnects on `MsgError`, on a message that fails to unpack, on an unexpected challenge, authentication-ok, or server-info, and on a TCP or UDP heartbeat timeout [ReflectorV2Logic.cpp `handleTimerTick` lines 1542–1576]. It does not send its own `MsgError` in those paths. This server’s `MsgError` strings for this session are: | String | Condition | | --- | --- | | `Protocol error` | TCP type field failed to unpack | | `Protocol version expected` | `MsgProtoVer` outside the expect-version state | | `Illegal MsgProtoVer protocol message received` | Version body failed to unpack | | `Unsupported protocol version M.N. May be at most 3.0.` | Version above 3.0 after a downgrade was already offered | | `Unsupported protocol version M.N. Must be at least 0.6.` | Version below 0.6 | | `Authentication response unexpected` | Response in the wrong state | | `Illegal MsgAuthResponse protocol message received` | Response body failed to unpack | | `Invalid callsign` | `callsignOk` failed | | `Access denied` | Unknown user, bad HMAC, or callsign already connected | | `Illegal MsgSelectTG protocol message received` | Body failed to unpack | | `Illegal MsgTgMonitor protocol message received` | Body failed to unpack | | `Illegal MsgNodeInfo protocol message received` | Body failed to unpack | | `Illegal MsgRequestQsy protocol message received` | Body failed to unpack | | `Illegal MsgStateEvent protocol message received` | Body failed to unpack | | `TCP heartbeat timeout` | No TCP frame for 15 seconds | | `UDP heartbeat timeout` | No accepted UDP datagram for 120 seconds | Application messages (type ≥ 100) before authentication are discarded with a warning. This server does not send `MsgError` for them [ReflectorClient.cpp lines 516–526]. The stock client disconnects if it receives type ≥ 100 before `MsgAuthOk` [ReflectorV2Logic.cpp lines 852–857]. The stock client also refuses to send type ≥ 100 before that point [ReflectorV2Logic.cpp lines 1304–1310]. There is no application-level close message. Either side closes the TCP connection. This server then broadcasts `MsgNodeLeft` if the client had a callsign [Reflector.cpp lines 865–869]. ### B.8.6 Messages a minimal client may ignore A client that only talks on one preselected talk group still needs types 1, 5, 6, 10, 11, 12, 13, 100, and 111 in the version 2 layout, plus UDP types 1, 101, 102, and 103. It SHOULD send 106. It MAY ignore, after a successful unpack, types 101, 102, 103, 104, 105, 109, and 110. It MAY omit 107, 109, 110, 112, and 113. If it sends 112 or 113, the bodies are Part A.5. This server will not send types 14–21 or 114 to a version 2.0 client. ## B.9 State diagram ```mermaid stateDiagram-v2 [*] --> ExpectChallenge: TCP connected, send version 2.0 ExpectChallenge --> ExpectAuthOk: MsgAuthChallenge, send HMAC response ExpectAuthOk --> ExpectServerInfo: MsgAuthOk ExpectServerInfo --> Connected: MsgServerInfo, send node info, UDP heartbeat ExpectChallenge --> [*]: MsgError, downgrade, or timeout ExpectAuthOk --> [*]: unexpected message or timeout ExpectServerInfo --> [*]: unexpected message or timeout Connected --> [*]: MsgError, timeout, or TCP close ``` This server’s states for this session are expect-version, then expect-auth-response, then connected. The certificate states in Part C are not used. ## B.10 Timing Client constants are Part A.12. Additional client constants [ReflectorV2Logic.h lines 207–212; ReflectorV2Logic.cpp lines 142–143]: | Timer | Value | | --- | --- | | Heartbeat tick | 1000 ms | | Encoder flush timeout | 3000 ms, one-shot | | Local gap after the last received audio | `diff.tv_sec > 3`, then the decoder is flushed | | Default talk-group select timeout | 30 s | | Default temporary monitor timeout | 3600 s | The select and monitor timeouts are local UI behavior of the SvxLink logic. This server does not time out a selected talk group. Server talker and heartbeat limits are Part A.11 and A.12. The first UDP heartbeat from the client both creates the return path and resets the server’s UDP receive counter. A client that waits longer than 120 seconds after TCP accept before sending any UDP datagram receives `"UDP heartbeat timeout"`. The client’s own UDP receive limit is 60 seconds from TCP connect. ## B.11 Audio The codec name is chosen in B.8.3. The payload of UDP type 101 is a `uint16` length and then that many codec bytes. When the name is `OPUS`, the encoder in this tree is libopus with these settings [AsyncAudioEncoderOpus.cpp constructor lines 117–138; `writeSamples` lines 632–647; src/CMakeLists.txt lines 50–53]: | Parameter | Value | | --- | --- | | Sample rate | `INTERNAL_SAMPLE_RATE`, CMake default 16000 | | Channels | 1 | | Application | `OPUS_APPLICATION_AUDIO` | | Frame size | 20 ms, which is 320 samples at 16000 Hz | | Bitrate | 20000 bit/s | | VBR | enabled | | Maximum bandwidth | `OPUS_BANDWIDTH_MEDIUMBAND` | | Bandwidth | `OPUS_AUTO` | | Signal type | `OPUS_SIGNAL_VOICE` | | DTX | disabled | | LSB depth | 16 when `OPUS_MAJOR > 0` | | API | `opus_encode_float` on float samples | The bytes passed to `writeEncodedSamples` are one raw Opus packet, not an Ogg page. A positive `nbytes` becomes one UDP datagram. Complexity, in-band FEC, and expected packet-loss percent are not set in the constructor; they stay at the libopus defaults unless the client configuration sets `OPUS_ENC_