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>
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
# Hand-edited config in the home directory
|
||||
|
||||
The operator edits connection settings by hand. There is no settings window in version 1. The file is `~/.funknetz.json`, created mode 0600 when missing. Application Support would match other Mac apps and would be a worse place to open in an editor. That path is part of the station's contract; moving it later strands existing files.
|
||||
The operator edits connection settings by hand. There is no settings window in version 1. The default file is `~/.funknetz.json`, created mode 0600 when missing. Application Support would match other Mac apps and would be a worse place to open in an editor. That default path is part of the station's contract; moving it later strands existing files. `-c` / `--config` may select another file; create, rewrite, and validate rules are the same—only the path changes.
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# Code review: funknetz
|
||||
|
||||
**Date:** 2026-10-07
|
||||
**Scope:** Full review of the funknetz checkout (headset station + local SvxReflector lab).
|
||||
**Fixed point:** `8e75515` (`Record the station glossary and the window and config decisions.`)
|
||||
**Compared tip:** `HEAD` = `f90b615`
|
||||
**Diff:** `git diff 8e75515...HEAD` (4222 insertions across 38 files)
|
||||
**Spec:** [docs/issues/2026-10-03-funknetz-station/spec.md](../../docs/issues/2026-10-03-funknetz-station/spec.md) (workspace issue tracker)
|
||||
**Standards:** [knowledge/engineering/go/](../../knowledge/engineering/go/), [CONTEXT.md](../CONTEXT.md), [docs/adr/](./adr/)
|
||||
|
||||
Two axes below are independent. Findings are not merged or ranked across axes.
|
||||
|
||||
### Commits in range (`8e75515..HEAD`)
|
||||
|
||||
| Commit | Summary |
|
||||
| --- | --- |
|
||||
| `9c4f662` | Join one FM-Funknetz talk group from a headset window |
|
||||
| `6233292` | Pause playback while transmitting and declare microphone use |
|
||||
| `ec5f660` | Append an optional station log and announce the software after login |
|
||||
| `eda79e6` | Add a local SvxReflector Docker lab for headset testing |
|
||||
| `f90b615` | Merge branch `lab/svxlink-docker` |
|
||||
|
||||
### Working tree (not in `HEAD` diff)
|
||||
|
||||
At review time, uncommitted edits touched `docs/svxlink.md`, `svxlink-docker/` (Dockerfile, compose, entrypoint, conf, `.env.example`, `.gitignore`), root `.gitignore`, and an untracked `svxlink-docker/config/svxreflector.link.conf`. Protocol and lab notes in this report include that working tree where it clarifies jayReflector Satellite/Trunk; Standards and Spec axes stay on `8e75515...HEAD` only.
|
||||
|
||||
### Tests run
|
||||
|
||||
```sh
|
||||
GOWORK=off go test ./station/ -count=1 # ok (~10s)
|
||||
```
|
||||
|
||||
`main`, `audio`, and `internal/opus` have no package tests. `TestConnectsToLabReflector` skips when `127.0.0.1:5300` is down (or `FUNKNETZ_LAB=0`).
|
||||
|
||||
---
|
||||
|
||||
## Standards
|
||||
|
||||
### Documented standards
|
||||
|
||||
**Hard — `modern.md` (new JSON → `encoding/json/v2`)**
|
||||
`station/software.go` marshals with v1 `encoding/json` while `station/config.go` already uses `encoding/json/v2`. Same module, new code: prefer v2.
|
||||
|
||||
**Hard — `errors.md` (wrap with `%w`; don’t drop causes)**
|
||||
Underlying failures are replaced with a bare string:
|
||||
- `app.go` / config path resolution paths that surface `could not be read`
|
||||
- `station/config.go` `loadConfig` / `parseConfig` / rewrite path: same `"could not be read"` without `%w`
|
||||
|
||||
**Hard — `testing.md` (prefer `t.Fatal` over `panic`)**
|
||||
`station/config_test.go` `configJSON` / `configWithLog`: `panic(err)` on marshal failure.
|
||||
|
||||
**Judgement — `errors.md` / `modern.md` (`errors.Is`)**
|
||||
`station/config.go`: `os.IsNotExist(err)` — prefer `errors.Is(err, fs.ErrNotExist)`.
|
||||
|
||||
**Judgement — `concurrency-context.md` + `functions-methods.md` (synchronous APIs; obvious goroutine lifetime)**
|
||||
`audio.Start` returns after starting a cleanup goroutine on `ctx.Done()`. `app.go` fire-and-forgets `Run` with `_ =` (comment present; discard rule satisfied).
|
||||
|
||||
**Judgement — `modern.md` (`slices.Contains`)**
|
||||
`station/protocol.go` `hasOPUS` hand-loops instead of `slices.Contains`.
|
||||
|
||||
**OK / tooling skip**
|
||||
`_ = sendUDP(...)` sites carry justifying comments (`errors.md`). `gofmt`/layout not reviewed. `App` pass-through methods are required by ADR `0001-wails-station-window.md` (overrides Middle Man).
|
||||
|
||||
### Baseline smells (all judgement)
|
||||
|
||||
**Divergent Change** — `station.Station` / `session.go`: connection, TX, Opus, UDP, UI state, logging in one type.
|
||||
|
||||
**Data Clumps** — `onTCP` / `onServerInfo` pass `udpCh`, `udpErr`, `udpReady`, `udpRx`, `lastTCP` as pointers together.
|
||||
|
||||
**Duplicated Code** — `audio/device.go` repeats channel-count logic for capture vs playback; `reflector_test.go` reimplements append/parse helpers already in `protocol.go`.
|
||||
|
||||
**Mysterious Name** — `loaded`, `fileShape`, `up` / `outage` don’t say what they hold.
|
||||
|
||||
**Primitive Obsession** — `State.Connection` as free strings (`"Connecting"` / `"Connected"` / …); session `reason` as `"tcp"`/`"udp"`/`"timer"`.
|
||||
|
||||
**Middle Man** — `Fail` → `stop` → `endFatal` (thin hop).
|
||||
|
||||
---
|
||||
|
||||
## Spec
|
||||
|
||||
**Tests:** `GOWORK=off go test ./...` — `station` OK (~10s); `main` / `audio` / `internal/opus` have no tests. `TestConnectsToLabReflector` skips when `127.0.0.1:5300` is down (or `FUNKNETZ_LAB=0`).
|
||||
|
||||
### (a) Missing / partial
|
||||
|
||||
- **Separate default devices (partial).** Spec: “A device adapter behind the station talks to the operating system’s default input and default output” / US 52. `audio.Start` opens one `malgo.Duplex` device, not independent default input and default output—fails if duplex is unavailable even when separate devices would work.
|
||||
- **Broken-JSON wording (partial).** Spec: “quit and say the file could not be read” (US 10). Implementation exits with `could not be read` (no “the file”).
|
||||
|
||||
Stories 1–9, 11–50, 53–62, 64–74 and the protocol / log Implementation Decisions look covered at the station seam (config, login, TG select, OPUS, PTT, heartbeats, retries, fatalities, log).
|
||||
|
||||
### (b) Scope creep
|
||||
|
||||
- **Local reflector lab.** Spec Solution / US 1–74 never ask for Docker or SvxReflector; Testing Decisions: “An in-process reflector double is the stand-in for TCP and UDP.” Diff adds `svxlink-docker/`, `docs/svxlink.md`, and `station/lab_reflector_test.go` (real TCP to `:5300`).
|
||||
- **Audio-open fatal.** Spec never defines device-open failure copy. `app.go` calls `Fail("could not open the audio device")`, inventing an English sentence and stopping retries outside US 57–59.
|
||||
|
||||
### (c) Looks implemented, looks wrong
|
||||
|
||||
- **Duplex vs defaults (same as (a)).** Spec wants default **input and** default **output**; duplex is a different contract and can reject a valid headset/speakers pair.
|
||||
- No other clear behavioral mismatches found against US 1–74 / protocol / log rules at HEAD (`8e75515...HEAD`). Wails labels, callsign/TG, PTT OR of button+Space, flush-on-release, busy-room inert PTT, and log line shapes match the quoted decisions.
|
||||
|
||||
---
|
||||
|
||||
## Protocol deep dive: SvxLink vs funknetz
|
||||
|
||||
There is no separate RFC. The contract is SvxLink / jayReflector message types (`ReflectorMsg.h` and client logics). Funknetz targets **protocol 2.0** (ReflectorV2 / AUTH_KEY), matching FM-Funknetz’s published guidance. Background: [docs/research/2026-10-03-fm-funknetz-go-client.md](../../docs/research/2026-10-03-fm-funknetz-go-client.md), lab notes in [docs/svxlink.md](./svxlink.md).
|
||||
|
||||
### 1. What funknetz implements
|
||||
|
||||
**Framing.** TCP: 4-byte BE length + `uint16` type + body (`station/protocol.go`). UDP V2 header: type, client id, sequence (`station/session.go`). Strings/blobs: `uint16` length + bytes. Max TCP frame 32768.
|
||||
|
||||
**TCP types handled or sent** (`protocol.go`, `session.go`, `software.go`):
|
||||
|
||||
| Type | Role in funknetz |
|
||||
| --- | --- |
|
||||
| 5 | Send ProtoVer **2.0** first |
|
||||
| 10 / 11 / 12 | HMAC-SHA1 challenge (20 B) → callsign + digest; AuthOK is a no-op |
|
||||
| 13 | Fatal string → stop retries |
|
||||
| 100 | Parse reserved, client id, nodes, codecs; require `"OPUS"` |
|
||||
| 111 | Send NodeInfoV2 JSON `{"sw","swVer"}` only |
|
||||
| 106 | Send configured TG once after 111 |
|
||||
| 104 / 105 | Talker start/stop: honor only matching TG; own callsign start ignored |
|
||||
| 1 | TCP heartbeat |
|
||||
|
||||
Unknown TCP types are ignored.
|
||||
|
||||
**UDP:** send/recv type **1** heartbeat; **101** audio (`uint16` len + Opus); **102** flush on PTT release (after padding a partial frame). Drop datagrams whose client id ≠ ours; **no** sequence check on RX. While transmitting, discard RX audio.
|
||||
|
||||
**Auth / codecs / Opus.** Auth key never on the wire. Opus-only: 16 kHz mono, 320 samples / 20 ms, 20 kbit/s VBR, voice, medium-band, DTX off (`internal/opus/opus.go`).
|
||||
|
||||
**Heartbeats / talker rules.** Match ReflectorV2 counters: TCP TX 10 s / RX 15 s; UDP TX 15 s / RX 60 s. Room busy from talker-start (matching TG) or any accepted RX audio; PTT inert while busy; mic never looped to playback.
|
||||
|
||||
**Tests assert:** login ProtoVer 2.0, HMAC digest, NodeInfo keys, SelectTG, initial UDP HB seq 0; heartbeats, retries, MsgError / no-OPUS stop retry, TCP/UDP timeouts, TG-filtered talker, busy blocks TX, PTT + flush 102, seq wrap, wrong-client-id audio dropped.
|
||||
|
||||
### 2. Parity with SvxLink ReflectorV2
|
||||
|
||||
| Capability | Upstream (ReflectorV2 / research) | funknetz |
|
||||
| --- | --- | --- |
|
||||
| Proto 2.0 + AUTH_KEY HMAC-SHA1 | ReflectorV2Logic | Yes |
|
||||
| Framed TCP + UDP same host/port | AsyncFramedTcp / V2 UDP | Yes |
|
||||
| MsgServerInfo → client id + codecs | type 100 | Yes (OPUS required) |
|
||||
| MsgSelectTG | 106 | Yes |
|
||||
| MsgNodeInfoV2 | 111 JSON | Yes (minimal) |
|
||||
| TalkerStart/Stop with TG | 104 / 105 (≥ proto 2) | Yes |
|
||||
| UDP HB / Audio / FlushSamples | 1 / 101 / 102 | Yes |
|
||||
| One talker/TG; client mute when busy | Reflector + client | Client-side busy |
|
||||
| Heartbeat timers 10 / 15 / 15 / 60 | ReflectorV2Logic.h | Yes |
|
||||
| Opus defaults 16 kHz / 20 ms / 20 kbit | AsyncAudioEncoderOpus | Yes |
|
||||
|
||||
### 3. Differences
|
||||
|
||||
**Intentional (spec Out of Scope):** protocol 3 / certs / AES-GCM UDP; MsgTgMonitor / QSY / DTMF; DNS SRV / baked-in host; GSM / Speex / RAW; rich node-info; multi-TG; station list / siglev UI.
|
||||
|
||||
**Thinner than ReflectorV2 client:**
|
||||
|
||||
| Topic | Upstream V2 | funknetz |
|
||||
| --- | --- | --- |
|
||||
| MsgProtoVerDowngrade (6) | Client should retry lower ver | Ignored |
|
||||
| RX UDP sequence / peer filter | Drop behind-seq; peer = TCP peer | Client-id only; connected UDP socket |
|
||||
| MsgUdpAllSamplesFlushed (103) | Flush handshake | Not sent/handled |
|
||||
| TalkerStop vs late audio | Audio may still arrive after 105 | Stop clears busy; **any later 101 re-sets `roomBusy`** without a named talker (`session.go` `onDatagram`) — can strand PTT |
|
||||
| Pre-auth size cap | 64 B | Always up to 32768 |
|
||||
| MONITOR_TGS / StateEvent / Signal / TxStatus | Optional client telemetry | Absent |
|
||||
| Codecs | First mutually supported | Hard-fail if no OPUS |
|
||||
| jayReflector type 121 trunk node list | Extra | Ignored (OK) |
|
||||
|
||||
### 4. Currently missing
|
||||
|
||||
**(A) Intentionally out of scope for v1 headset** — Proto 3 / encryption; MsgTgMonitor; MsgRequestQsy; NodeJoined / Left UI; StateEvent / SignalStrength / TxStatus; DNS SRV; non-OPUS codecs; rich NODE_INFO; multi-TG.
|
||||
|
||||
**(B) Gaps that can still affect ReflectorV2 / FM-Funknetz / jayReflector headset use**
|
||||
|
||||
1. **Late audio after TalkerStop** re-busying the room without a named talker (upstream documents post-stop audio).
|
||||
2. **No ProtoVerDowngrade** handling (unlikely on FM’s V2 servers; breaks if a server asks to step down).
|
||||
3. **No UDP flush / all-flushed** — usually fine for app clients; edge cases if a peer expects 103.
|
||||
4. **No SRV** — operators must set host/port (FM docs often use 5308 + `DNS_DOMAIN` on nodes).
|
||||
5. **No MsgTgMonitor** — idle monitoring of other TGs unavailable; single SelectTG is enough for one configured TG.
|
||||
6. **Stricter RX sequence** not implemented — more permissive than V2; rarely blocks interop.
|
||||
|
||||
Core login + SelectTG + Opus UDP path is aligned with FM-Funknetz’s ReflectorV2 guidance.
|
||||
|
||||
### 5. Lab stack
|
||||
|
||||
| Stack | Role |
|
||||
| --- | --- |
|
||||
| **funknetz `svxlink-docker`** | Builds **jayReflector** → `svxreflector`; TCP+UDP **5300**; `[USERS]` / `[PASSWORDS]` for proto 2; optional **Satellite** (outbound **5303**) / **Trunk** (**5302**); clients stay on local `:5300` |
|
||||
| **Stock sm0svx SvxReflector** | Same client wire protocol; **no** reflector peering |
|
||||
| **jayReflector Satellite / Trunk** | Server-side mesh / uplink; Funknetz does **not** speak Trunk / Satellite |
|
||||
|
||||
Headset interop testing needs a ReflectorV2-capable `svxreflector` with OPUS and AUTH_KEY users. Satellite / Trunk only matter when exercising FM uplink, not the client protocol itself.
|
||||
|
||||
### Message coverage (quick map)
|
||||
|
||||
| Direction | Implemented | Not implemented (by design unless noted) |
|
||||
| --- | --- | --- |
|
||||
| TCP send | 5, 11, 1, 111, 106 | 14–21 (proto 3), 107 monitor, 109 QSY, 110–113 telemetry |
|
||||
| TCP recv | 1, 10, 12, 13, 100, 104, 105 | 6 downgrade (**gap**), 101 NodeList (legacy), 102/103 presence, 114 UDP encrypt |
|
||||
| UDP send | 1, 101, 102 | 103 all-flushed, 104 signal |
|
||||
| UDP recv | 1 (timer), 101 | sequence validation, 102/103 handshake |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Standards:** 3 hard findings (json/v2 in `software.go`; bare `could not be read` dropping causes; `panic` in test helpers); several judgement smells — worst hard issue is inconsistent JSON API / dropped error causes on config read.
|
||||
**Spec:** 2 partial gaps (duplex vs separate default devices; broken-JSON wording) + 2 scope-creep items (Docker lab; audio-open fatal copy) — worst issue is duplex audio vs the specified separate default input/output devices.
|
||||
|
||||
### Follow-up (2026-10-07)
|
||||
|
||||
Hard Standards and Spec gaps addressed in code where possible:
|
||||
|
||||
| Finding | Status |
|
||||
| --- | --- |
|
||||
| `software.go` → `encoding/json/v2` | Fixed |
|
||||
| Config / path errors wrap with `%w`; US 10 wording `the file could not be read` | Fixed |
|
||||
| Test helpers use `t.Fatal` instead of `panic` | Fixed |
|
||||
| Separate default Capture + Playback devices (not Duplex) | Fixed |
|
||||
| Docker lab scope creep | Left in place — intentional engineering aid, not a product story |
|
||||
| Audio-open `Fail("could not open the audio device")` | Left — spec has no copy; stopping without audio is still required UX |
|
||||
+63
-23
@@ -1,29 +1,42 @@
|
||||
# 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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 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.
|
||||
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 |
|
||||
| --- | --- |
|
||||
| Listen port | **5300** TCP **and** UDP (upstream default; FM-Funknetz production uses 5308) |
|
||||
| Config | `svxlink-docker/config/svxreflector.conf` → `/etc/svxlink/svxreflector.conf` |
|
||||
| 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 (current upstream bootstraps a local PKI even for AUTH_KEY clients) |
|
||||
| Protocol 3 PKI | Material under `/var/lib/svxlink/pki` (compose volume `data/`) |
|
||||
| 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) |
|
||||
|
||||
Point `~/.funknetz.json` at the lab (values match `.env` / the mounted conf; secrets are local-test placeholders):
|
||||
### 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
|
||||
{
|
||||
@@ -39,6 +52,39 @@ Point `~/.funknetz.json` at the lab (values match `.env` / the mounted conf; sec
|
||||
|
||||
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`.
|
||||
@@ -53,11 +99,11 @@ The binary does not know “Funknetz” or “HamLink.” It admits a station by
|
||||
| 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.
|
||||
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 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.
|
||||
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:
|
||||
|
||||
@@ -104,18 +150,12 @@ User messages (type ≥ 100) matter only after the session is connected. Unknown
|
||||
|
||||
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
|
||||
## Reflector linking (lab context)
|
||||
|
||||
`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 |
|
||||
| Concept | What it is in this lab |
|
||||
| --- | --- |
|
||||
| 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.
|
||||
| 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. |
|
||||
|
||||
Reference in New Issue
Block a user