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

162 lines
9.3 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 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](https://github.com/dj1jay/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/`](../svxlink-docker/). It builds jayReflector, mounts a minimal `svxreflector.conf`, publishes TCP **and** UDP on the listen port, and runs only `svxreflector`.
```sh
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:
```sh
./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](./adr/0002-home-directory-config.md).
Manual lab JSON (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.
## Optional FM-Funknetz link (Satellite / Trunk)
Default: `ENABLE_FM_LINK=0` — local island only. No network uplink.
When DJ1JAY provides parameters:
1. Edit [`config/svxreflector.link.conf`](../svxlink-docker/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.
### 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).
```text
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](../../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 &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.
## 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. |