Funknetz needs a protocol 2.0 reflector without production credentials; the compose stack and smoke test prove the station can log in and select a talk group. Co-authored-by: Cursor <cursoragent@cursor.com>
8.8 KiB
SvxLink lab for Funknetz
Funknetz must be proven against a local SvxReflector before it touches the live network. This document is that lab: how to run the reflector, what it will accept from a station, and what the wire protocol actually does—so a headset client can be tested without FM-Funknetz credentials.
Upstream SvxLink is two processes with different jobs. svxlink is a node: logics, optional radio hardware, modules. svxreflector is the conference: stations connect over IP, and it forwards audio among those on the same talk group—one talker at a time. Funknetz is a station on that conference; the lab runs only svxreflector. You do not need a full RF node, LocalRx/LocalTx, or EchoLink to exercise the headset client.
Running a local reflector
Lab compose lives in svxlink-docker/. It builds upstream SvxLink (community Docker pattern from f4hlv/svxlink-docker), mounts a minimal svxreflector.conf, publishes TCP and UDP on the listen port, and runs only svxreflector. There is no first-party sm0svx image on Docker Hub.
cd svxlink-docker
docker compose up -d --build
| Item | Lab value |
|---|---|
| Listen port | 5300 TCP and UDP (upstream default; FM-Funknetz production uses 5308) |
| Config | svxlink-docker/config/svxreflector.conf → /etc/svxlink/svxreflector.conf |
| Auth (protocol 2.0) | [USERS] → [PASSWORDS]; station authKey matches that secret |
| Server cert | [SERVER_CERT] COMMON_NAME=… required on first start (current upstream bootstraps a local PKI even for AUTH_KEY clients) |
| Protocol 3 PKI | Material under /var/lib/svxlink/pki (compose volume data/) |
| Codec | Default OPUS when compiled in (CODECS optional) |
| Go smoke test | GOWORK=off go test ./station/ -run TestConnectsToLabReflector -v (needs libopus; skips if port 5300 is down) |
Point ~/.funknetz.json at the lab (values match .env / the mounted conf; secrets are local-test placeholders):
{
"host": "127.0.0.1",
"port": 5300,
"callsign": "DL0FN-1",
"authKey": "local-lab-secret-change-me",
"talkGroup": 9,
"enableLogging": false,
"logFile": "/tmp/funknetz.log"
}
A second station can use callsign DL0FN-2 with the same auth key. Do not reuse production FM-Funknetz credentials in the lab.
What the reflector admits
The binary does not know “Funknetz” or “HamLink.” It admits a station by protocol version, callsign shape, proof of the auth key (or a certificate on protocol 3), and uniqueness of that callsign on the session. App names are never checked. FM-Funknetz’s tickets, issued keys, and the published rule to use ReflectorV2 are network policy layered on top of the same knobs—not a separate allowlist inside svxreflector.
| Check | Behavior |
|---|---|
| Protocol version | Client announces major/minor. Server accepts about 0.6 through 3.0. Major ≥ 3 takes the certificate/TLS path; older clients get an HMAC challenge. |
| Callsign | Must match ACCEPT_CALLSIGN (default: typical ham callsign, optional -suffix); optional REJECT_CALLSIGN blocks. |
| Auth key (protocol < 3) | Callsign must map in [USERS] to a [PASSWORDS] secret. Client proves it with HMAC-SHA1 over a 20-byte challenge. Wrong or missing → “Access denied.” |
| Certificates (protocol ≥ 3) | Mutual TLS; peer cert CN is the callsign and must pass the same callsign checks. |
| Unique session | A second concurrent login with the same callsign is denied. |
| Talk group | Optional per-TG ALLOW / ALLOW_MONITOR regexes after login. |
| Software / version string | None. Optional node-info JSON is for status only. |
For the lab and for production as FM-Funknetz documents it, the station speaks protocol 2.0 (ReflectorV2): auth key, not certificates. Upstream’s man page treats AUTH_KEY as deprecated for “modern” reflectors; that is a different target than the live network’s written instruction.
Wire protocol
There is no separate RFC. The contract is the message types in upstream SvxLink (ReflectorMsg.h and the client logics). Control rides TCP as length-prefixed frames (4-byte big-endian length, then payload). Audio and UDP keepalives ride UDP to the same host and port. Message types are numeric. Protocol 2.0 authenticates with the auth-key HMAC and sends plaintext UDP with a header of type, client id, and sequence. Protocol 3.0 uses X.509/TLS and encrypts UDP (AES-128-GCM). Funknetz targets 2.0.
Typical protocol 2.0 login:
- TCP connect to the listen port.
- Station →
MsgProtoVermajor 2, minor 0. - Reflector →
MsgAuthChallenge(20 random bytes). - Station →
MsgAuthResponse(callsign + HMAC-SHA1 digest over the challenge; the auth key is never sent as its own field). - Reflector →
MsgAuthOk, thenMsgServerInfo(client id, codec list, connected nodes). - Station opens UDP to the same host/port; sends heartbeats; selects or monitors talk groups; sends audio as
MsgUdpAudio.
User messages (type ≥ 100) matter only after the session is connected. Unknown types are ignored for forward compatibility. One talker per talk group; other stations’ audio on that group is ignored until the talker stops.
TCP messages
| Type | Name | Role |
|---|---|---|
| 1 | MsgHeartbeat |
Keep the TCP session alive |
| 5 | MsgProtoVer |
Station announces major/minor (first message) |
| 6 | MsgProtoVerDowngrade |
Reflector asks for a lower version |
| 10 | MsgAuthChallenge |
Starts auth-key auth |
| 11 | MsgAuthResponse |
Callsign + HMAC digest |
| 12 | MsgAuthOk |
Auth succeeded |
| 13 | MsgError |
Fatal; peer should disconnect |
| 14–21 | Encryption / CSR / CA | Protocol 3 certificate setup |
| 100 | MsgServerInfo |
Client id, nodes, codecs |
| 102 / 103 | MsgNodeJoined / MsgNodeLeft |
Presence |
| 104 / 105 | MsgTalkerStart / MsgTalkerStop |
Talker on a talk group (protocol ≥ 2 includes TG) |
| 106 | MsgSelectTG |
Station selects talk group |
| 107 | MsgTgMonitor |
Station sets monitored talk groups |
| 109 | MsgRequestQsy |
Request QSY (0 = reflector chooses) |
| 110–113 | State / signal / TX status | Optional station telemetry |
| 111 | MsgNodeInfoV2 |
JSON node info (protocol 2 body) |
| 114 | MsgStartUdpEncryption |
Protocol 3: OK to encrypt UDP |
UDP messages
| Type | Name | Role |
|---|---|---|
| 1 | MsgUdpHeartbeat |
Keepalive / NAT binding |
| 101 | MsgUdpAudio |
Encoded audio bytes (no talk-group field in the active class) |
| 102 | MsgUdpFlushSamples |
End of audio stream |
| 103 | MsgUdpAllSamplesFlushed |
Peer has flushed |
| 104 | MsgUdpSignalStrengthValues |
Signal reports over UDP |
The codec name is negotiated in MsgServerInfo, not per audio packet. When Opus is available and CODECS is unset, the reflector prefers OPUS.
Linking and talk-group sharing
svxreflector interconnects stations on talk groups. It has no peer-reflector configuration: two reflector processes do not mesh. Upstream has discussed multi-reflector interconnect (GitHub issue #597); it is not shipped in the tree this lab tracks. FM-Funknetz states the same limit and attaches other networks by talk-group sharing / bridge—audio continuity on a designated talk-group number, implemented by a client present on both sides (typically an extra svxlink instance on the server), not by reflector peering.
A Funknetz station only consumes a shared talk group (select or monitor it). Creating a bridge on the live network needs coordination with the FM-Funknetz team (loop risk). Named cross-network talk groups in their prose include 2621 (Berlin), 2629 (Sachsen), and possibly 2624; app ingress paths such as Zello, FRN, and Teamspeak often default to 262.
| Concept | What it actually is |
|---|---|
| Reflector peering | Unsupported. Not how you join two Verbünde. |
LINKS= / ReflectorLink |
Audio link between logics inside one svxlink process (e.g. radio ↔ ReflectorLogic). Not two svxreflector servers. |
MONITOR_TGS / select TG |
Watching or joining a talk group on one reflector. Uses a bridge TG if operators already bridge it; does not create the path. |
| EchoLink / Zello / FRN / Teamspeak | Alternate ingress into FM-Funknetz. Different protocols; EchoLink cannot switch talk groups. |
| Same reflector, same TG, two stations | Normal conference behavior—not a bridge test. |
To simulate sharing locally: run two svxreflector instances on distinct ports and users, plus one bridging svxlink (two ReflectorLogic sections or two processes) linked so audio crosses on chosen talk groups. Prove with a third station on each side. Exact dual-ReflectorLogic INIs for FM-Funknetz’s production bridge are not published; compose the lab from upstream logic-linking docs.