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>
This commit is contained in:
2026-10-05 20:13:25 +02:00
co-authored by Cursor
parent ec5f660fdd
commit eda79e6662
11 changed files with 455 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
logs/
data/
.git
.gitignore
*.md
*.log
.DS_Store
+18
View File
@@ -0,0 +1,18 @@
# Copy to .env and adjust. Auth secrets must match config/svxreflector.conf.
TZ=Europe/Berlin
LISTEN_PORT=5300
START_SVXLINK=0
START_REMOTETRX=0
START_SVXREFLECTOR=1
SVXREFLECTOR_CONF=/etc/svxlink/svxreflector.conf
SVXREFLECTOR_ARGS=--logfile=/var/log/svxlink/svxreflector.log
FUNKNETZ_HOST=127.0.0.1
FUNKNETZ_PORT=5300
FUNKNETZ_CALLSIGN=DL0FN-1
FUNKNETZ_AUTH_KEY=local-lab-secret-change-me
FUNKNETZ_TALK_GROUP=9
+6
View File
@@ -0,0 +1,6 @@
logs/*
!logs/.gitkeep
data/*
!data/.gitkeep
.env
*.log
+77
View File
@@ -0,0 +1,77 @@
# Build and run SvxReflector for the Funknetz local lab.
# Provenance: patterned on f4hlv/svxlink-docker (community); builds from upstream sm0svx/svxlink.
# =========================================================
# STAGE 1 — BUILD
# =========================================================
FROM debian:trixie-slim AS builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install --no-install-recommends \
git ca-certificates \
cmake g++ make dpkg-dev \
libsigc++-2.0-dev libgsm1-dev libpopt-dev tcl8.6-dev \
libgcrypt20-dev libspeex-dev libasound2-dev \
libopus-dev librtlsdr-dev libjsoncpp-dev \
libcurl4-openssl-dev libgpiod-dev libogg-dev \
ladspa-sdk libssl-dev \
&& rm -rf /var/lib/apt/lists/*
ARG GIT_URL=https://github.com/sm0svx/svxlink.git
ARG GIT_BRANCH=master
ARG NUM_CORES=4
WORKDIR /build
RUN git clone --recursive --depth 1 --branch "${GIT_BRANCH}" "${GIT_URL}" svxlink
WORKDIR /build/svxlink
RUN set -eux; \
SVX_VER="$(awk -F= '$1=="SVXSERVER"{gsub(/[[:space:]]/,"",$2); print $2}' src/versions)"; \
echo "SVXSERVER version from src/versions: ${SVX_VER}"; \
rm -rf build && mkdir -p build && cd build; \
cmake \
-DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_INSTALL_SYSCONFDIR=/etc \
-DCMAKE_INSTALL_LOCALSTATEDIR=/var \
-DUSE_QT=OFF \
-DCMAKE_BUILD_TYPE=Release \
-DCPACK_GENERATOR=DEB \
-DCPACK_PACKAGE_VERSION="${SVX_VER}" \
-DCPACK_DEBIAN_PACKAGE_VERSION="${SVX_VER}" \
../src; \
make -j"${NUM_CORES}"; \
make package; \
ls -lh *.deb
# =========================================================
# STAGE 2 — RUNTIME
# =========================================================
FROM debian:trixie-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install --no-install-recommends \
adduser init-system-helpers ca-certificates openssl \
libasyncaudio1.6t64 libasynccore1.6t64 libasynccpp1.6t64 libcurl4t64 \
libgcrypt20 libgsm1 libjsoncpp26 libpopt0 librtlsdr0 \
libsigc++-2.0-0v5 libtcl8.6 libgcc-s1 libstdc++6 \
libasound2 libgpiod3 libogg0 libopus0 libspeex1 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/svxlink/build/*.deb /tmp/svxlink.deb
RUN apt-get update && \
apt-get -y install --no-install-recommends /tmp/svxlink.deb && \
rm -f /tmp/svxlink.deb && \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p /etc/svxlink /var/log/svxlink /var/lib/svxlink && \
useradd -r -u 1000 -m -d /home/svx svx || true
ENV SVXLINK_CONF=/etc/svxlink/svxlink.conf \
REMOTETRX_CONF=/etc/svxlink/remotetrx.conf \
SVXREFLECTOR_CONF=/etc/svxlink/svxreflector.conf
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
+24
View File
@@ -0,0 +1,24 @@
###################################################################
# Local lab SvxReflector for Funknetz (protocol 2.0 / ReflectorV2)
# Secrets here are placeholders — do not reuse production keys.
###################################################################
[GLOBAL]
TIMESTAMP_FORMAT="%c"
LISTEN_PORT=5300
TG_FOR_V1_CLIENTS=999
# CODECS=OPUS
COMMAND_PTY=/dev/shm/reflector_ctrl
# Newer SvxReflector bootstraps a local PKI even for protocol 2.0 AUTH_KEY clients.
# COMMON_NAME is required so the server certificate can be generated on first start.
[SERVER_CERT]
COMMON_NAME=funknetz-lab.local
SUBJECT_ALT_NAME=DNS:funknetz-lab.local,DNS:localhost,IP:127.0.0.1
[USERS]
DL0FN-1=LocalTest
DL0FN-2=LocalTest
[PASSWORDS]
LocalTest="local-lab-secret-change-me"
View File
+33
View File
@@ -0,0 +1,33 @@
# Local SvxReflector lab for Funknetz.
# Build once, then: docker compose up -d
# Connect the app with host/port/callsign/authKey from .env (see FUNKNETZ_*).
services:
svxreflector:
build:
context: .
dockerfile: Dockerfile
args:
GIT_URL: https://github.com/sm0svx/svxlink.git
GIT_BRANCH: master
image: funknetz-svxreflector:local
container_name: funknetz-svxreflector
restart: unless-stopped
env_file:
- .env
ports:
# Host LISTEN_PORT → container 5300 (must match config/svxreflector.conf).
# Control is TCP; audio and UDP keepalives use the same number.
- "${LISTEN_PORT:-5300}:5300/tcp"
- "${LISTEN_PORT:-5300}:5300/udp"
environment:
TZ: ${TZ:-Europe/Berlin}
START_SVXLINK: ${START_SVXLINK:-0}
START_REMOTETRX: ${START_REMOTETRX:-0}
START_SVXREFLECTOR: ${START_SVXREFLECTOR:-1}
SVXREFLECTOR_CONF: ${SVXREFLECTOR_CONF:-/etc/svxlink/svxreflector.conf}
SVXREFLECTOR_ARGS: ${SVXREFLECTOR_ARGS:-}
volumes:
- ./config/svxreflector.conf:/etc/svxlink/svxreflector.conf:ro
- ./logs:/var/log/svxlink
- ./data:/var/lib/svxlink
+113
View File
@@ -0,0 +1,113 @@
#!/bin/sh
set -eu
SVXLINK_CONF="${SVXLINK_CONF:-/etc/svxlink/svxlink.conf}"
REMOTETRX_CONF="${REMOTETRX_CONF:-/etc/svxlink/remotetrx.conf}"
SVXREFLECTOR_CONF="${SVXREFLECTOR_CONF:-/etc/svxlink/svxreflector.conf}"
START_SVXLINK="${START_SVXLINK:-0}"
START_REMOTETRX="${START_REMOTETRX:-0}"
START_SVXREFLECTOR="${START_SVXREFLECTOR:-1}"
SVXLINK_ARGS="${SVXLINK_ARGS:-}"
REMOTETRX_ARGS="${REMOTETRX_ARGS:-}"
SVXREFLECTOR_ARGS="${SVXREFLECTOR_ARGS:-}"
pids=""
log() {
echo "[$(date -Iseconds)] $*"
}
die() {
log "ERROR: $*"
exit 1
}
need_file_if_enabled() {
enabled="$1"
file="$2"
name="$3"
if [ "$enabled" = "1" ] && [ ! -f "$file" ]; then
die "Config for $name not found: $file (expected volume mount)"
fi
}
start_bg() {
name="$1"
shift
log "Starting: $name -> $*"
"$@" &
pid="$!"
pids="$pids $pid"
log "$name started (pid=$pid)"
}
stop_all() {
log "Stop requested; stopping: $pids"
for pid in $pids; do
kill -TERM "$pid" 2>/dev/null || true
done
i=0
while [ $i -lt 10 ]; do
alive=0
for pid in $pids; do
if kill -0 "$pid" 2>/dev/null; then
alive=1
fi
done
[ "$alive" -eq 0 ] && break
i=$((i + 1))
sleep 1
done
for pid in $pids; do
kill -KILL "$pid" 2>/dev/null || true
done
log "Stop complete."
}
trap 'stop_all; exit 0' INT TERM
if [ "$START_SVXLINK" = "1" ] && ! command -v svxlink >/dev/null 2>&1; then
die "svxlink binary not found"
fi
if [ "$START_REMOTETRX" = "1" ] && ! command -v remotetrx >/dev/null 2>&1; then
die "remotetrx binary not found"
fi
if [ "$START_SVXREFLECTOR" = "1" ] && ! command -v svxreflector >/dev/null 2>&1; then
die "svxreflector binary not found"
fi
need_file_if_enabled "$START_SVXLINK" "$SVXLINK_CONF" "svxlink"
need_file_if_enabled "$START_REMOTETRX" "$REMOTETRX_CONF" "remotetrx"
need_file_if_enabled "$START_SVXREFLECTOR" "$SVXREFLECTOR_CONF" "svxreflector"
if [ "$START_SVXREFLECTOR" = "1" ]; then
start_bg "svxreflector" svxreflector --config "$SVXREFLECTOR_CONF" $SVXREFLECTOR_ARGS
fi
if [ "$START_REMOTETRX" = "1" ]; then
start_bg "remotetrx" remotetrx --config "$REMOTETRX_CONF" $REMOTETRX_ARGS
fi
if [ "$START_SVXLINK" = "1" ]; then
start_bg "svxlink" svxlink --config "$SVXLINK_CONF" $SVXLINK_ARGS
fi
if ! echo "$pids" | grep -q '[0-9]'; then
die "No service enabled. Set START_SVXREFLECTOR=1 (lab default) and/or START_SVXLINK / START_REMOTETRX"
fi
log "Requested services are running. Waiting…"
exit_code=0
for pid in $pids; do
if ! wait "$pid"; then
exit_code=1
fi
done
log "Exit (exit_code=$exit_code)"
exit "$exit_code"
View File