Build DJ1JAY’s fork with an optional Satellite/Trunk stub, allow -c/--config, open capture and playback as separate devices, and add a lab start helper. Co-authored-by: Cursor <cursoragent@cursor.com>
9.3 KiB
SvxLink lab for Funknetz
Funknetz must be proven against a local reflector 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.
The lab runs jayReflector (DJ1JAY / FM-Funknetz), a Trunk/Satellite-capable fork of SvxReflector. The process is still named svxreflector. You do not need the svxlink node binary, LocalRx/LocalTx, or EchoLink to exercise the headset client.
Funknetz is a station on the conference: it connects to the local reflector on TCP+UDP. Optional FM-Funknetz uplink is Satellite (outbound) or Trunk — configured separately when DJ1JAY provides parameters.
Running a local reflector
Lab compose lives in svxlink-docker/. It builds jayReflector, mounts a minimal svxreflector.conf, publishes TCP and UDP on the listen port, and runs only svxreflector.
cd svxlink-docker
cp -n .env.example .env # if needed
docker compose up -d --build
| Item | Lab value |
|---|---|
| Binary | jayReflector → /usr/bin/svxreflector |
| Listen port | 5300 TCP and UDP (local clients; FM-Funknetz production client port is often 5308) |
| Config | 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 (PKI under data/pki → /etc/svxlink/pki) |
| 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) |
Lab app start
With the reflector already up, from the funknetz repo root:
./teststart.sh
That script requires svxlink-docker/.env (it will not copy .env.example), checks FUNKNETZ_* against config/svxreflector.conf, writes svxlink-docker/.funknetz.generated.json (gitignored), enables logging to svxlink-docker/logs/funknetz-lab.log, builds funknetz.app if missing or stale, and launches the binary with -c pointing at the generated file. The default operator config remains ~/.funknetz.json; -c / --config only select another path (same create/rewrite rules). See ADR 0002.
Manual lab JSON (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.
Optional FM-Funknetz link (Satellite / Trunk)
Default: ENABLE_FM_LINK=0 — local island only. No network uplink.
When DJ1JAY provides parameters:
- Edit
config/svxreflector.link.conf(prefer Satellite for home labs). - Set
ENABLE_FM_LINK=1in.env. docker compose up -d(rebuild only if the image changed).
The entrypoint appends the link file after the base conf. Local Funknetz settings (FUNKNETZ_*, port 5300) stay the same.
Satellite (recommended): outbound only
With Satellite, your container dials out to the parent on TCP 5303. The network does not open a connection toward you. You do not need public DNS, a static IP, or inbound port-forwarding for the uplink. Only your local clients need to reach host :5300 (typically localhost).
Funknetz ──:5300──► jayReflector (lab) ──outbound :5303──► FM-Funknetz parent
Fill from DJ1JAY (jayReflector README shape):
SATELLITE_OF=parent.example.org:5303SATELLITE_ID=…SATELLITE_SECRET=…- optional
SATELLITE_FILTER=…for “only some TGs”
Trunk (only if agreed)
Equal mesh peer: mutual TCP 5302, matching [TRUNK_x] section name and SECRET, plus filters (ALLOW_TGS / TG_MAP). That does require the peer to reach your trunk port — not the default lab layout. Prefer Satellite unless DJ1JAY asks for Trunk.
Research baseline: docs/research/2026-10-07-jayreflector-fm-funknetz.md.
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.
Wire protocol
There is no separate RFC. The contract is the message types in SvxLink / jayReflector (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.
Reflector linking (lab context)
| Concept | What it is in this lab |
|---|---|
Stock sm0svx peering |
Unsupported — not what we build. |
| jayReflector Satellite | Lab dials parent :5303; optional SATELLITE_FILTER for some TGs. Outbound only. |
| jayReflector Trunk | Mesh peer on :5302 with filters / TG_MAP. Needs mutual reachability. |
svxlink dual ReflectorLogic bridge |
Not required here; Satellite/Trunk replace that for FM-Funknetz attach. |
| Same reflector, same TG, two stations | Normal conference — local test without any uplink. |