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

122 lines
8.8 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.
# 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/`](../svxlink-docker/). It builds upstream SvxLink (community Docker pattern from [f4hlv/svxlink-docker](https://github.com/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.
```sh
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):
```json
{
"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 &lt; 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.