Merge branch 'lab/svxlink-docker'
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd svxlink-docker
|
||||||
|
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` |
|
||||||
|
| 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/`) |
|
||||||
|
| 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):
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
|
## 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. Upstream’s man page treats AUTH_KEY as deprecated for “modern” reflectors; that is a different target than the live network’s written instruction.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
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**.
|
||||||
|
|
||||||
|
## Linking and talk-group sharing
|
||||||
|
|
||||||
|
`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 |
|
||||||
|
| --- | --- |
|
||||||
|
| 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.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package station_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.neitzel.de/konrad/funknetz/station"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lab defaults match svxlink-docker/.env and config/svxreflector.conf.
|
||||||
|
const (
|
||||||
|
labHost = "127.0.0.1"
|
||||||
|
labPort = 5300
|
||||||
|
labCallsign = "DL0FN-1"
|
||||||
|
labAuthKey = "local-lab-secret-change-me"
|
||||||
|
labTalkGroup = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConnectsToLabReflector(t *testing.T) {
|
||||||
|
if os.Getenv("FUNKNETZ_LAB") == "0" {
|
||||||
|
t.Skip("FUNKNETZ_LAB=0")
|
||||||
|
}
|
||||||
|
addr := net.JoinHostPort(labHost, strconv.Itoa(labPort))
|
||||||
|
conn, err := net.DialTimeout("tcp", addr, 500*time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("lab reflector not listening on %s: %v", addr, err)
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
path := writeConfigFile(t, configJSON(labHost, labPort, labCallsign, labAuthKey, labTalkGroup))
|
||||||
|
st, err := station.New(station.Options{ConfigPath: path})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
go func() { _ = st.Run(ctx) }()
|
||||||
|
|
||||||
|
state, err := st.Wait(ctx, func(s station.State) bool {
|
||||||
|
return s.Connection == "Connected"
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("did not reach Connected: %v (last state %#v)", err, st.State())
|
||||||
|
}
|
||||||
|
if state.Callsign != labCallsign {
|
||||||
|
t.Fatalf("callsign = %q, want %q", state.Callsign, labCallsign)
|
||||||
|
}
|
||||||
|
if state.TalkGroup != labTalkGroup {
|
||||||
|
t.Fatalf("talkGroup = %d, want %d", state.TalkGroup, labTalkGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
logs/
|
||||||
|
data/
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
logs/*
|
||||||
|
!logs/.gitkeep
|
||||||
|
data/*
|
||||||
|
!data/.gitkeep
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
@@ -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"]
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
Executable
+113
@@ -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"
|
||||||
Reference in New Issue
Block a user