Files
funknetz/docs/rfc.md
T

896 lines
73 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<T>` | `uint16` element count, then each element packed as `T`. A `vector<uint8_t>` is therefore a `uint16` length and then raw bytes. |
| `std::set<T>` | `uint16` element count, then each element in set order. For `set<uint32_t>` that order is ascending numeric order. |
| `std::array<T,N>` and `T[N]` | N elements, no count prefix |
[AsyncMsg.h `MsgPacker<std::string>` lines 407–445; `MsgPacker<std::vector>` lines 447–496; `MsgPacker<std::set>` 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<N>` 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<uint8_t>` of 20 bytes | Server, major &lt; 3, and one protocol 3 fallback |
| 11 | `MsgAuthResponse` | `string` callsign, then `vector<uint8_t>` 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<uint8_t>` SHA-256 | Server, major ≥ 3 |
| 20 | `MsgCABundleRequest` | empty | Client, major ≥ 3 |
| 21 | `MsgCABundle` | `string` CA PEM, `vector<uint8_t>` signature, `string` signing-certificate PEM | Server, major ≥ 3 |
| 100 | `MsgServerInfo` | `uint16` reserved, `uint16` client id, `vector<string>` nodes, `vector<string>` codecs | Server, after login |
| 101 | `MsgNodeList` | `vector<string>` callsigns | Server, only if client version &lt; 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<uint32>` 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<uint8_t>` IV random, `vector<uint8_t>` UDP key, `string` JSON | Client with major ≥ 3 |
| 111 | `MsgNodeInfoV2` | `string` JSON | Client with major &lt; 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<N>`.
| Type | Class | Body after the UDP header of the version in use | Direction |
| --- | --- | --- | --- |
| 1 | `MsgUdpHeartbeat` | empty | Both |
| 101 | `MsgUdpAudio` | `vector<uint8_t>` 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 &lt; 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#<n>` 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 &lt; 0.7, forces the talk group when the version is &lt; 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_<OPTION>`. A peer MUST decode that Opus packet at 16000 Hz, mono. `opus_encode` of int16 PCM with the same controls produces the same packet format.
`SPEEX`, `GSM`, `S16`, and `RAW` are alternative names. Each datagram is one output of that encoder, with the same `uint16` length prefix. This memo does not restate those encoders’ internal frames. The stock server prefers `OPUS` when it is compiled in.
There is no packet-loss concealment in the reflector session. A gap advances the sequence and the next packet is decoded when it arrives. The stock client logs the gap and writes the received packet to the decoder [ReflectorV2Logic.cpp lines 1409–1440].
## B.12 Security considerations
The shared key is authenticated at login and is not transmitted. The proof is HMAC-SHA1 over the 20-byte nonce, compared with `CRYPTO_memcmp` [ReflectorMsg.h lines 456–484]. SHA-1 is the algorithm this code names (`"sha1"`). The challenge is 20 bytes from `RAND_bytes`.
After login, TCP frames and UDP datagrams are plaintext. They have no MAC. An on-path attacker can read audio and callsigns, and can change, drop, or insert TCP frames and UDP datagrams. The server accepts a version 2 UDP datagram when the client id is known and the source address equals the TCP peer. The client accepts a datagram when the source address and port equal the TCP peer. Those checks bind the datagram to the path’s addresses; they do not authenticate the payload.
The sequence number rejects a datagram that looks more than half the `uint16` space behind the expected value. It is not a security association. A later sequence number is accepted and moves the window.
The login HMAC is not repeated on later messages. A captured `MsgAuthResponse` does not reveal the key, and it does not by itself open a new session, because the server generates a new challenge. Replaying it against a new challenge fails the MAC.
Callsigns and node JSON travel in the clear. `MsgError` strings in B.8.5 distinguish an illegal callsign from a bad key.
## B.13 Differences from version 3.0
Version 2.0 announces major 2. It authenticates with HMAC-SHA1 and `AUTH_KEY`. Its UDP header is the 6-byte plaintext header in B.7. Node information is type 111 with only a JSON string. It never sends types 14–21 or 114, and this server does not send those to it. Version 3.0 is Part C.
## B.14 References
Blob prefix: `https://github.com/sm0svx/svxlink/blob/fcd13014810af75d1852b3f2cfc8d9806178d0ab/`
| Section | Symbol | Path and lines read |
| --- | --- | --- |
| B.4, B.8.1 | `proto_ver(2, 0)`, `onConnected` | `src/svxlink/svxlink/ReflectorV2Logic.cpp` 127, 785–801 |
| B.7 | `ReflectorUdpMsgV2`, `sendUdpMsg`, `udpDatagramReceived` | `ReflectorMsg.h` 210–257; `ReflectorV2Logic.cpp` 1358–1489; `Reflector.cpp` 406–440, 1056–1147 |
| B.8.2 | `MsgAuthResponse::calcHMAC`, `handleMsgAuthResponse` | `ReflectorMsg.h` 412–485; `ReflectorClient.cpp` 718–781; `ReflectorV2Logic.cpp` 944–972 |
| B.8.3 | `handleMsgServerInfo` | `ReflectorV2Logic.cpp` 990–1130 |
| B.8.4 | `sendEncodedAudio`, `flushEncodedAudio`, `handleMsgTalkerStart` | `ReflectorV2Logic.cpp` 1192–1254, 1331–1355; `Reflector.cpp` 1159–1250 |
| B.8.5 | `sendError`, `handleHeartbeat` | `ReflectorClient.cpp` 1210–1298 |
| B.10 | timer constants | `ReflectorV2Logic.h` 207–212 |
| B.11 | `AudioEncoderOpus` | `src/async/audio/AsyncAudioEncoderOpus.cpp` 117–138, 632–647; `src/CMakeLists.txt` 50–53 |
Part A references apply to every encoding claim.
## B.15 Unresolved
- **Inference:** Whether the production FM-Funknetz reflector still speaks version 2.0. The network’s pages tell operators to use `TYPE=ReflectorV2`. This memo did not open a connection to that reflector.
- Packet-loss concealment is outside this protocol. The implementation logs a sequence gap and delivers the packets it has.
- The man page’s one-second default for `SQL_TIMEOUT_BLOCKTIME` does not match the binary default of 60 seconds (Part A.11).
- `TG_FOR_V1_CLIENTS` is 1 in the binary initializer and 999 in the shipped configuration template.
- The stock version 2.0 client does not disconnect when it understands no advertised codec. What a different server does in that case is not defined here.
- `ProtoVer::operator!=` ignores the minor number. The version 2.0 handshake does not call it.
---
# Part C — SvxReflector Protocol Version 3.0
## C.1 Status of this memo
Implementation-derived description of the version 3.0 session at commit `fcd13014810af75d1852b3f2cfc8d9806178d0ab`. Not an IETF standard. Encoding, the catalogue, callsigns, talk groups, and server timers are Part A. HMAC-SHA1 as the primary login of version 2.0 is Part B. This memo specifies the certificate session and the UDP cipher. A reader does not need Part B’s UDP header to implement version 3.0.
## C.2 Abstract
A client opens one TCP connection, announces protocol 3.0, optionally fetches a CA bundle, and then both sides speak TLS 1.2 or later on that TCP connection. The client is identified by the common name of its certificate, or, if it has none yet, by a certificate signing request and a possible HMAC fallback. It sends the reflector a random AES-128 key inside TLS. UDP datagrams are AES-128-GCM with an 8-byte tag. Audio forwarding rules are the same as Part A.11.
## C.3 Table of contents
- C.4 Introduction
- C.5 Terminology
- C.6 Transport
- C.7 UDP datagram
- C.8 Session
- C.9 State diagram
- C.10 Timing
- C.11 Audio
- C.12 Security considerations
- C.13 Differences from version 2.0
- C.14 References
- C.15 Unresolved
## C.4 Introduction
Version 3.0 is the session spoken by logic type `Reflector`. The `MsgProtoVer` class defaults are major 3 and minor 0, and this client sends that default [ReflectorLogic.cpp line 159 and lines 984–1003]. A downgrade to version 2 is present only inside `#if 0`. This client disconnects on `MsgProtoVerDowngrade` [ReflectorLogic.cpp lines 1226–1255].
This server treats major ≥ 3 as this session [ReflectorClient.cpp lines 649–656].
## C.5 Terminology
A client MUST send major 3 and minor 0, or another version that this server maps onto this session (major ≥ 3 and ≤ 3.0), as its first TCP payload.
A client MUST complete TLS after `MsgStartEncryption` before sending `MsgNodeInfo`. A client MUST put a 16-byte AES-128 key and a 6-byte IV random in that message. This server stores those fields and uses them as the UDP cipher key and IV prefix.
The HMAC challenge is not the normal login of this session. It remains a server fallback after a signing request, specified in C.8.4.
## C.6 Transport
TCP and UDP addresses are Part A.7. The version 3.0 client sets the TCP payload maximum to 32768 at construction and leaves it there [ReflectorLogic.cpp lines 219–220]. This server still starts at a receive maximum of 64. It raises that to 4096 when it sends `MsgStartEncryption`, and to 32768 when authentication completes [ReflectorClient.cpp lines 199–200, 711, and 1343].
Messages before `MsgStartEncryption` are plaintext frames from Part A.4. After both sides call `enableSsl`, further writes are TLS application data. The 4-byte length and the reflector payload sit inside the TLS records. The TLS object is `TLS_method()` with minimum version `TLS1_2_VERSION` and `SSL_OP_ALL` [AsyncSslContext.h lines 138–147]. `enableSsl` uses `SSL_set_accept_state` on the server and `SSL_set_connect_state` on the client, and sets `SSL_VERIFY_PEER` [AsyncTcpConnection.cpp lines 251–280]. `SSL_VERIFY_FAIL_IF_NO_PEER_CERT` is not set, so a client certificate is optional at the TLS layer.
The client’s UDP socket is `EncryptedUdpSocket` with local port 0 [ReflectorLogic.cpp lines 1712–1714].
## C.7 UDP datagram
### C.7.1 Cipher
The cipher name is the string `AES-128-GCM` [ReflectorMsg.h `UdpCipher::NAME`, line 1504]. The key length is the OpenSSL key length of that cipher, which is 16 bytes. The client generates it with `randomBytes` and `setCipherKey()` [ReflectorLogic.cpp lines 1715–1722; AsyncEncryptedUdpSocket.cpp lines 218–227].
Constants [ReflectorMsg.h lines 1504–1509]:
| Name | Value |
| --- | --- |
| Associated data for an ordinary datagram | 4 bytes |
| Tag | 8 bytes |
| IV | 12 bytes |
| IV random prefix | 12 − 4 − 2 = 6 bytes |
The tag is the GCM tag truncated by `EVP_CTRL_AEAD_GET_TAG` / `EVP_CTRL_AEAD_SET_TAG` to 8 bytes [AsyncEncryptedUdpSocket.cpp lines 311–315 and 397–412]. Both sides call `setTagLength(8)` [Reflector.cpp lines 310–311; ReflectorLogic.cpp line 1732].
### C.7.2 IV
The 12-byte IV is packed as [ReflectorMsg.h `UdpCipher::IV`, lines 1528–1581]:
```
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| random prefix (6 bytes) |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | client id (16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| counter (32, big-endian) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
The random prefix is the 6 bytes from the client’s `MsgNodeInfo`. If the vector is shorter, the remaining prefix bytes are 0. Extra bytes are ignored.
Direction changes the client-id field:
- Client to server: the client id from `MsgServerInfo` [ReflectorLogic.cpp `sendUdpMsg` lines 2252–2253].
- Server to client: the client-id field is 0 [ReflectorClient.cpp `udpCipherIV` lines 365–367]. The client’s receive path sets the same field to 0 [ReflectorLogic.cpp lines 2090–2091].
The counter is a `uint32`.
### C.7.3 Associated data and on-wire order
Ordinary associated data is the counter alone, 4 bytes, big-endian [ReflectorMsg.h `UdpCipher::AAD`, lines 1511–1517].
The client’s first datagram uses `InitialAAD`, which packs the parent counter (value 0) and then the real client id as a `uint16`. That associated data is 6 bytes [ReflectorMsg.h lines 1519–1526; ReflectorLogic.cpp `sendUdpRegisterMsg` lines 2278–2281].
`EncryptedUdpSocket::write` lays the datagram out as associated data, then the 8-byte tag, then the ciphertext [AsyncEncryptedUdpSocket.cpp lines 278–329]:
```
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| associated data (4 bytes, or 6 on registration) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| GCM tag (8 bytes) |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
| ciphertext of (uint16 type || message body) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
The plaintext inside the cipher is a version 3 UDP message: `uint16` type, then the A.6 body. There is no client id and no sequence in that plaintext. The counter in the IV and in the associated data is the sequence.
GCM additional data is the associated-data bytes. They are copied in the clear and also passed to `EVP_EncryptUpdate` as additional data. The plaintext is encrypted with `EVP_EncryptUpdate` / `EVP_EncryptFinal_ex`. The receiver checks the tag with `EVP_DecryptFinal_ex`. A failed tag is not delivered upward [AsyncEncryptedUdpSocket.cpp lines 370–433].
### C.7.4 Sequence
The client’s registration uses counter 0 and does not consume the send counter. That send counter is set to 1 before the socket is created, and later datagrams post-increment it [ReflectorLogic.cpp lines 1713 and 2268–2274].
This server’s send counter starts at 0 and is post-incremented, so the first server datagram uses counter 0 [ReflectorClient.cpp line 197; ReflectorClient.h line 481].
On receive, a counter less than the expected counter is dropped. A counter greater than expected is accepted and the expected value becomes `received + 1`. The comparison is unsigned and has no 32767-wide window [ReflectorLogic.cpp lines 2144–2160; Reflector.cpp lines 1109–1127]. The client’s expected counter starts at 0, which matches the server’s first datagram.
This server peeks the first 4 bytes. If that counter is 0, it treats the datagram as registration: the associated-data length is 6, the IV’s client-id field is the real client id, and the client id is taken from those extra two bytes [Reflector.cpp `udpCipherDataReceived` lines 899–942]. Any other counter uses associated-data length 4 and looks up the client by source address and port. A counter of 0 always executes `setUdpRxSeq(0)` before the rest of the receive path, including when the client was already registered [Reflector.cpp lines 1014–1044].
The client’s receive path always uses a 4-byte associated data and an IV client-id of 0, because this server does not send `InitialAAD`.
While the client is still waiting for the first server UDP heartbeat, expiry of the UDP transmit interval sends the registration datagram again (`sendUdpRegisterMsg`), which repeats counter 0 under the same key and the same IV [ReflectorLogic.cpp lines 2350–2355].
## C.8 Session
### C.8.1 Plaintext preface
1. The client connects TCP and sends `MsgProtoVer` 3.0. It waits for `MsgCAInfo` [ReflectorLogic.cpp lines 984–1003].
2. This server sends `MsgCAInfo`: a `uint16` equal to the CA bundle PEM size, and a `vector<uint8_t>` that is SHA-256 of those PEM bytes (`MsgCABundle::MD_ALG` is `"sha256"`) [Reflector.cpp lines 1710–1720; ReflectorMsg.h line 680].
3. The client requests `MsgCABundle` only when `CERT_DOWNLOAD_CA_BUNDLE` is true (the default) and the local CA file is missing or unreadable. If the file exists and its SHA-256 differs, the client logs a warning, does not request a replacement, and continues with the file it has [ReflectorLogic.cpp lines 1328–1392]. The source comment says the new bundle is not verified against the old one, so an existing file is kept.
4. On a bundle request, this server sends `MsgCABundle`: CA PEM, a signature, and the issuing certificate PEM. The signature is SHA-256 over the CA PEM bytes, produced with the issuing CA private key [Reflector.cpp lines 1719–1720]. The client verifies that signature with the public key in the supplied certificate PEM [ReflectorLogic.cpp lines 1433–1449]. The following source comment says the signing certificate is not checked against the previous CA. On success the client writes the PEM and continues. On failure it disconnects.
5. The client sends `MsgStartEncryptionRequest`.
6. This server, in the expect-start-encryption state, raises the receive maximum to 4096, sends `MsgStartEncryption`, and calls `enableSsl(true)` [ReflectorClient.cpp lines 684–714].
7. The client, on `MsgStartEncryption`, calls `enableSsl(true)` and waits for the TLS handshake [ReflectorLogic.cpp lines 1485–1501].
`MsgCABundleRequest` or `MsgStartEncryptionRequest` in any other server state closes the TCP connection without `MsgError`.
### C.8.2 TLS identity
The client generates a 2048-bit RSA key if `CERT_KEYFILE` does not exist [ReflectorLogic.cpp lines 323–329]. Its CSR is version 1, subject CN equal to `CALLSIGN`, basic constraints `critical, CA:FALSE`, key usage `critical, digitalSignature, keyEncipherment, keyAgreement`, and extended key usage `clientAuth`. Optional `CERT_EMAIL` values are subject-alternative names of type email. The CSR is signed by the client’s own key [ReflectorLogic.cpp lines 346–397].
The client checks the server certificate after the handshake:
- OpenSSL verification must return `X509_V_OK`.
- The peer certificate must match the remote hostname (`matchHost`, with one trailing dot removed) or the remote IP (`matchIp`).
Otherwise the client disconnects [ReflectorLogic.cpp lines 1058–1103].
This server, when the handshake completes:
- If the peer certificate is null, it sends `MsgClientCsrRequest` and waits for `MsgClientCsr` [ReflectorClient.cpp lines 425–432].
- If a certificate is present, the common name must pass `callsignOk`. Failure closes the connection without `MsgError`. Success authenticates that common name as the callsign and continues at C.8.5 [ReflectorClient.cpp lines 447–477].
### C.8.3 Certificate provisioning
`MsgClientCsr` carries the PEM. This server accepts it in the expect-CSR state or after the client is already connected. It rejects a PEM that is not a CSR with `"Invalid CSR received"`. `checkCsr` returns one of these strings, which are sent as `MsgError` [Reflector.cpp lines 741–754]:
- `Certificate signing request with invalid callsign '<CN>'`
- `Certificate signing request with no or invalid CERT_EMAIL`
If the server has a certificate whose public key matches the pending CSR, it sends `MsgClientCert` (PEM) and waits to disconnect. The stock client writes that certificate and reconnects [ReflectorLogic.cpp `handleMsgClientCert` around lines 1532–1617; ReflectorClient.cpp lines 833–844].
If this server is still in expect-CSR and has no matching certificate, it logs that fact and falls through to `sendAuthChallenge` [ReflectorClient.cpp lines 845–850].
### C.8.4 HMAC fallback
The challenge and HMAC-SHA1 construction are Part B.8.2. This server sends that challenge only on the path in C.8.3, after TLS is up. The version 3.0 client handles `MsgAuthChallenge` only in the expect-auth-answer state, which is where it sits after the handshake and after a CSR request (the CSR handler does not change state). It reads `AUTH_KEY` if present and sends `MsgAuthResponse` [ReflectorLogic.cpp lines 1259–1291]. `AUTH_KEY` is not required for the client to start. An empty key is what gets signed if the key is unset.
`MsgAuthOk` in the expect-auth-answer state moves the client to expect-server-info. If the local CSR file is newer than the certificate file, the client sends `MsgClientCsr` again [ReflectorLogic.cpp lines 1295–1324].
### C.8.5 Server info and UDP key
`MsgAuthOk` and `MsgServerInfo` follow a successful certificate common name or a successful HMAC, as in Part B.8.2, including `MsgNodeJoined` and the version &lt; 0.7 node list. A version 3.0 client is not forced onto `TG_FOR_V1_CLIENTS`.
On `MsgServerInfo` the client selects a codec. If no name works, it disconnects. That is different from version 2.0 [ReflectorLogic.cpp lines 1662–1684].
It then creates the encrypted UDP socket, generates the 6-byte IV prefix and the 16-byte key, and sends `MsgNodeInfo` (type 111):
1. `vector<uint8_t>` IV random (6 bytes)
2. `vector<uint8_t>` AES key (16 bytes)
3. `string` JSON
The JSON is single-line and contains `sw`, `swVer`, `projVer`, and, when `uname` succeeds, `machineArch`. `NODE_INFO_FILE` is merged first, then those keys are overwritten [ReflectorLogic.cpp lines 640–648 and 1740–1749].
This server, for major ≥ 3, stores the IV random and the key and then sends `MsgStartUdpEncryption` before it parses the JSON. A JSON parse error is a warning. An unpack failure of type 111 is `"Illegal MsgNodeInfo protocol message received"` [ReflectorClient.cpp lines 917–937].
The client, on `MsgStartUdpEncryption`, sends the registration datagram: `InitialAAD` with its client id, plaintext UDP type 1 (heartbeat), encrypted as in C.7 [ReflectorLogic.cpp lines 1983–2004].
This server’s first accepted registration datagram records the UDP source and sends a UDP heartbeat back (counter 0, IV client-id field 0). The client accepts that datagram only from the TCP peer’s address and port. When the plaintext type is heartbeat and the client is waiting for one, it marks the connection established, sends `MsgSelectTG` if its talk group is non-zero, and sends `MsgTgMonitor` if it has monitor talk groups [ReflectorLogic.cpp lines 2164–2183]. Audio sent before that heartbeat is dropped, because `isLoggedIn` is still false [ReflectorLogic.cpp lines 2186–2189].
### C.8.6 Audio and the rest of the catalogue
After the state is connected, `MsgUdpAudio`, `MsgUdpFlushSamples`, and `MsgUdpAllSamplesFlushed` have the same talk-group, talker, and flush behavior as Part A.11 and Part B.8.4. The bytes are the plaintext of C.7, not the version 2 header.
Talker TCP messages for this client use the major ≥ 2 layout (talk group plus callsign). `MsgRequestQsy` is sent to this client when it is on the requester’s talk group.
TCP types 102, 103, 104, 105, and 109 are informational in the same way as in Part B. Signal strength and transmitter status are TCP 112 and 113. The stock client sends them from local receiver and transmitter events; a headset client MAY omit them.
### C.8.7 Errors and disconnect
`MsgError` handling and the server strings in Part B.8.5 apply, plus the CSR strings in C.8.3. Additional version 3.0 client disconnects, without a reflector `MsgError`, are: unexpected CA, bundle, or start-encryption messages; a bad CA signature; TLS verification failure; hostname mismatch; an unexpected `MsgStartUdpEncryption`; failure to construct the AES-GCM socket.
Unexpected `MsgCABundleRequest` and `MsgStartEncryptionRequest` make this server call `disconnect()` with no `MsgError` [ReflectorClient.cpp lines 668–674 and 689–695].
Application types before authentication are discarded by this server with a warning, and they make this client disconnect [ReflectorLogic.cpp lines 1132–1138].
## C.9 State diagram
```mermaid
stateDiagram-v2
[*] --> ExpectCA: TCP connected, send version 3.0
ExpectCA --> ExpectBundle: local CA missing, send bundle request
ExpectBundle --> ExpectStartEnc: bundle stored, send start-encryption request
ExpectCA --> ExpectStartEnc: local CA usable, send start-encryption request
ExpectStartEnc --> ExpectTls: MsgStartEncryption, start TLS
ExpectTls --> ExpectAuthAnswer: TLS ready, peer verified
ExpectAuthAnswer --> ExpectServerInfo: certificate CN accepted, or HMAC MsgAuthOk
ExpectAuthAnswer --> ExpectAuthAnswer: CSR requested or certificate PEM received
ExpectServerInfo --> ExpectUdpEnc: MsgServerInfo, send type 111 with key
ExpectUdpEnc --> ExpectUdpHeartbeat: MsgStartUdpEncryption, send registration
ExpectUdpHeartbeat --> Connected: UDP heartbeat from the TCP peer
Connected --> [*]: MsgError, timeout, or TCP close
```
This server’s states for this session are: expect-version, expect-start-encryption, expect-TLS-ready, expect-CSR or expect-auth-response, then connected. UDP encryption is not a separate TCP state on the server; it starts when type 111 has been stored and type 114 has been sent.
## C.10 Timing
The 1000 ms tick and the client counters in Part A.12 apply. The version 3.0 UDP transmit counter, while the client is still in expect-UDP-heartbeat, retransmits the counter-0 registration instead of sending a new counter [ReflectorLogic.cpp lines 2350–2355].
The flush timer is 3000 ms. The local audio gap rule is `diff.tv_sec > 3` [ReflectorLogic.cpp lines 2341–2346]. Server limits are Part A.11 and A.12.
The client’s 60-second UDP receive counter starts at TCP connect, so the TLS handshake and the UDP registration have to finish inside that window or the client disconnects. The server’s 120-second UDP receive counter starts at TCP accept.
Certificate renewal is a server timer. It is at least 10 minutes after connect, and otherwise one minute after the certificate’s renewal time. The server then sends `MsgClientCert` and expects the client to disconnect [ReflectorClient.cpp lines 462–474 and 1416–1427]. The fraction `RENEW_AFTER` is used to compute that time from the certificate’s validity; the wire effect is a later `MsgClientCert`, not a new message type.
## C.11 Audio
Codec negotiation matches Part A.10, except a version 3.0 client that finds no usable name disconnects before creating the UDP socket [ReflectorLogic.cpp lines 1676–1684].
The Opus parameters are the same encoder as Part B.11: mono, 16000 Hz by default, 20 ms, 20000 bit/s, VBR, voice, maximum bandwidth medium-band, `opus_encode_float`, one raw Opus packet per UDP plaintext. The packet is then encrypted as C.7. Loss behavior is the counter rule in C.7.4. No concealment step is specified.
## C.12 Security considerations
Before `MsgStartEncryption`, the version number, CA size, CA SHA-256, and optional CA bundle are plaintext. The bundle signature is SHA-256 over the PEM, verified with the public key in the certificate the server sends in the same message. This client does not chain that certificate to a previously trusted CA when the local file is absent. When a local file already exists and the digest differs, this client keeps the old file.
After `enableSsl`, TCP frames are TLS with a minimum of TLS 1.2. The server authenticates with a certificate that this client checks against the hostname or IP. The client authenticates with a certificate common name, or with the HMAC fallback inside the TLS session, or by sending a CSR and waiting for an operator to sign it. The CSR itself is not proof of a callsign beyond the later checks.
The UDP key and IV prefix travel in `MsgNodeInfo` after TLS is up. UDP uses AES-128-GCM. The associated data and the 8-byte tag authenticate the counter and the ciphertext. An on-path attacker who does not have the key cannot read the audio of a datagram that verifies, and a modified datagram fails `EVP_DecryptFinal_ex` and is dropped.
The GCM tag is 8 bytes, not 16. That is the length passed to `EVP_CTRL_AEAD_SET_TAG`.
Client-to-server and server-to-client IVs differ in the client-id field (the real id versus 0), so the same counter in the two directions is not the same IV. The registration datagram and every retransmission of it use counter 0, the same random prefix, and the same key, so those transmissions repeat an IV. Ordinary audio counters increase by one per datagram until the `uint32` wraps. A later counter of 0 is also the registration peek on this server.
The server accepts UDP only after it can decrypt with the key from type 111 and, after registration, only from the recorded source address and port. The client accepts UDP only from the TCP peer’s address and port, decrypted with the same key.
Login HMAC, when the fallback runs, has the properties in Part B.12 and is carried inside TLS.
## C.13 Differences from version 2.0
Version 3.0 announces major 3. The plaintext phase ends at `MsgStartEncryption`. Identity is a certificate common name, with HMAC-SHA1 only as the fallback in C.8.4. UDP is AES-128-GCM rather than the 6-byte header in Part B.7. Type 111 carries the key and IV prefix plus JSON, and the JSON adds `machineArch` when `uname` works. The client waits for a decrypted UDP heartbeat before it selects a talk group or emits audio. A codec it cannot use ends the session.
## C.14 References
Blob prefix: `https://github.com/sm0svx/svxlink/blob/fcd13014810af75d1852b3f2cfc8d9806178d0ab/`
| Section | Symbol | Path and lines read |
| --- | --- | --- |
| C.4 | `MsgProtoVer` default, `onConnected` | `ReflectorMsg.h` 311–316; `ReflectorLogic.cpp` 159, 984–1003, 1226–1255 |
| C.6 | `SslContext`, `enableSsl` | `src/async/core/AsyncSslContext.h` 133–147; `AsyncTcpConnection.cpp` 251–280 |
| C.7 | `UdpCipher`, `EncryptedUdpSocket::write` | `ReflectorMsg.h` 1499–1582; `AsyncEncryptedUdpSocket.cpp` 244–329, 370–433 |
| C.7.4 | `udpCipherDataReceived`, `sendUdpRegisterMsg` | `Reflector.cpp` 899–970, 996–1127; `ReflectorLogic.cpp` 2071–2092, 2278–2281, 2350–2355 |
| C.8.1 | `handleMsgCAInfo`, `handleMsgCABundle`, `handleMsgStartEncryption` | `ReflectorLogic.cpp` 1328–1501; `ReflectorClient.cpp` 649–715 |
| C.8.1 | CA digest and signature | `Reflector.cpp` 1710–1720 |
| C.8.2 | CSR fields, `onSslConnectionReady` | `ReflectorLogic.cpp` 323–397, 1058–1103; `ReflectorClient.cpp` 410–477 |
| C.8.5 | `handleMsgServerInfo`, `handleNodeInfo` | `ReflectorLogic.cpp` 1621–1749, 1983–2004; `ReflectorClient.cpp` 917–937 |
| C.11 | `AudioEncoderOpus` | `AsyncAudioEncoderOpus.cpp` 117–138, 632–647 |
## C.15 Unresolved
- **Inference:** Whether any given production network, including FM-Funknetz, has switched its reflector to version 3.0. This memo did not connect to one.
- The source records a FIXME: a received CA bundle’s signing certificate is not verified against the CA the client already holds, and a digest mismatch on an existing file does not download a replacement [ReflectorLogic.cpp lines 1369–1373 and 1452–1453].
- Registration retransmission repeats the AES-GCM IV (C.7.4). How long a deployment can run before a `uint32` audio counter wraps is a function of the packet rate; the protocol text does not define wrap behavior beyond the comparisons in the source.
- Packet-loss concealment is not specified. A gap updates the expected counter and the next decrypted packet is delivered to the decoder.
- The man-page and binary defaults for `SQL_TIMEOUT_BLOCKTIME` differ (Part A.11).
---
# jayReflector
`dj1jay/jayReflector` at `50997ed21a37c7c05c132eca5ebb07bab4b80fc7` uses the same client-facing TCP type numbers 1, 5, 6, 10–21, and 100–114, and the same UDP type numbers 1 and 101–104, with the same version 2 header versus version 3 cipher split. `ReflectorClient.cpp` in that tree switches on the same `Msg*` cases as upstream. Types 115–122 (`MsgTrunkHello`, `MsgTrunkTalkerStart`, `MsgTrunkTalkerStop`, `MsgTrunkAudio`, `MsgTrunkFlush`, `MsgTrunkHeartbeat`, `MsgTrunkNodeList`, `MsgTrunkFilter`) are sent on the trunk and satellite TCP sessions (`TrunkLink.cpp`, `SatelliteLink.cpp`), not on the node client connection. They are not part of Protocol Version 2.0 or 3.0 as a station speaks them. MQTT, talk-group mapping, and satellite policy are outside this file.