Files
funknetz/docs/svxlink.md
T
konradandCursor f8cad55e9c Switch the lab reflector to jayReflector and harden local station use.
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>
2026-10-07 12:14:41 +02:00

9.3 KiB
Raw Blame History

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.

Default: ENABLE_FM_LINK=0 — local island only. No network uplink.

When DJ1JAY provides parameters:

  1. Edit config/svxreflector.link.conf (prefer Satellite for home labs).
  2. Set ENABLE_FM_LINK=1 in .env.
  3. 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.

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:5303
  • SATELLITE_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:

  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.

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.