Files
funknetz/docs/svxlink.md
T
konradandCursor eda79e6662 Add a local SvxReflector Docker lab for headset testing.
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>
2026-10-05 20:13:25 +02:00

8.8 KiB
Raw Blame History

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:

  1. TCP connect to the listen port.
  2. Station → MsgProtoVer major 2, minor 0.
  3. Reflector → MsgAuthChallenge (20 random bytes).
  4. Station → MsgAuthResponse (callsign + HMAC-SHA1 digest over the challenge; the auth key is never sent as its own field).
  5. Reflector → MsgAuthOk, then MsgServerInfo (client id, codec list, connected nodes).
  6. 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.