Compare commits
4
Commits
110c0e1d49
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b1fb005f6 | ||
|
|
be9d5c301b | ||
|
|
b73c97dd5b | ||
|
|
6276c3feea |
@@ -126,12 +126,12 @@ script/install.sh
|
||||
```
|
||||
|
||||
This builds in release mode, installs `trx-server`, `trx-client`, and
|
||||
`trx-configurator` to `/usr/local/bin` (using `sudo` only if needed), seeds
|
||||
`trx-configurator` to `~/.local/bin`, seeds
|
||||
`~/.config/trx-rs/trx-rs.toml` from the example (existing config is never
|
||||
overwritten), and installs `~/.config/systemd/user/{trx-server,trx-client}.service`.
|
||||
|
||||
```bash
|
||||
script/install.sh --prefix ~/.local # install to ~/.local/bin instead
|
||||
script/install.sh --prefix /opt/trx-rs # choose another installation prefix
|
||||
script/install.sh --no-sdr # build without SoapySDR support
|
||||
script/install.sh --enable-now # also enable + start the services now
|
||||
script/install.sh --help # all options
|
||||
@@ -149,6 +149,11 @@ Serial (`/dev/ttyUSB*`) and audio access require your user to be in the
|
||||
`dialout` and `audio` groups. Remove everything with `script/uninstall.sh`
|
||||
(add `--purge` to also delete the config).
|
||||
|
||||
For an unattended or remotely accessible installation, follow the
|
||||
[safe deployment guide](docs/Deployment.md). It covers a dedicated service
|
||||
account, device permissions, authentication, firewall and reverse-proxy
|
||||
boundaries, verification, upgrades, and rollback.
|
||||
|
||||
## How It Works
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
# Safe deployment
|
||||
|
||||
This guide deploys trx-rs on Linux as a dedicated, unprivileged user with
|
||||
systemd user services. It keeps radio-device access, configuration, and runtime
|
||||
data separate from an administrator's account.
|
||||
|
||||
The examples use `trx-rs` as the account name and `/opt/trx-rs/bin` for
|
||||
root-owned executables. Adapt group names and firewall commands to your Linux
|
||||
distribution.
|
||||
|
||||
## 1. Decide what must be reachable
|
||||
|
||||
Only expose listeners that another machine actually needs:
|
||||
|
||||
| Listener | Typical port | Recommended exposure |
|
||||
| --- | ---: | --- |
|
||||
| Server control | TCP 4530 | Loopback or trusted radio LAN only |
|
||||
| Server audio | TCP 4531 and per-rig ports | Trusted radio LAN only |
|
||||
| Client web UI | TCP 8080 or a chosen port | Loopback behind HTTPS proxy |
|
||||
| Client rigctl | Per-rig TCP ports | Loopback or trusted LAN only |
|
||||
| Client JSON | Configured TCP port | Loopback or trusted LAN only |
|
||||
|
||||
`127.0.0.1` accepts connections only from the same host. Use a specific LAN
|
||||
address when possible, or `0.0.0.0` when the listener must accept connections
|
||||
on every IPv4 interface. Binding a socket does not configure the host firewall.
|
||||
|
||||
Do not expose unauthenticated control, audio, rigctl, or JSON listeners to the
|
||||
public Internet. Prefer a VPN for links between radio sites. Put the web UI
|
||||
behind an HTTPS reverse proxy when it is remotely accessible.
|
||||
|
||||
## 2. Create the service account
|
||||
|
||||
Create a non-login service account with a home directory:
|
||||
|
||||
```bash
|
||||
sudo useradd --create-home --shell /usr/sbin/nologin trx-rs
|
||||
sudo chmod 0750 /home/trx-rs
|
||||
```
|
||||
|
||||
Add only the hardware groups required on this host. Common group names are
|
||||
`dialout` for serial devices and `audio` for sound devices:
|
||||
|
||||
```bash
|
||||
sudo usermod -aG dialout,audio trx-rs
|
||||
```
|
||||
|
||||
SDR USB access is distribution- and device-specific. Install the vendor's udev
|
||||
rules or add a narrowly scoped rule for the device's USB vendor/product IDs.
|
||||
Avoid making all USB devices world-writable. After reconnecting the device,
|
||||
verify access as the service account:
|
||||
|
||||
```bash
|
||||
sudo -u trx-rs test -r /dev/ttyUSB0
|
||||
sudo -u trx-rs test -w /dev/ttyUSB0
|
||||
sudo -u trx-rs SoapySDRUtil --find
|
||||
```
|
||||
|
||||
Run only the checks relevant to the configured hardware. Group membership and
|
||||
udev-rule changes normally require reconnecting the device or restarting the
|
||||
service.
|
||||
|
||||
## 3. Build and install immutable binaries
|
||||
|
||||
Build from a reviewed revision as a normal development user, not as root:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stanislawgrams/trx-rs.git
|
||||
cd trx-rs
|
||||
git switch --detach <reviewed-tag-or-commit>
|
||||
cargo build --release -p trx-server -p trx-client -p trx-configurator
|
||||
```
|
||||
|
||||
Install root-owned binaries into a directory the service user cannot modify:
|
||||
|
||||
```bash
|
||||
sudo install -d -o root -g root -m 0755 /opt/trx-rs/bin
|
||||
sudo install -o root -g root -m 0755 \
|
||||
target/release/trx-server \
|
||||
target/release/trx-client \
|
||||
target/release/trx-configurator \
|
||||
/opt/trx-rs/bin/
|
||||
```
|
||||
|
||||
If SDR support is not needed, build `trx-server` with
|
||||
`--no-default-features`. Keep the source revision and Rust toolchain used for
|
||||
the build in deployment records.
|
||||
|
||||
## 4. Install and validate configuration
|
||||
|
||||
Create private configuration and state directories, then seed the example:
|
||||
|
||||
```bash
|
||||
sudo install -d -o trx-rs -g trx-rs -m 0700 \
|
||||
/home/trx-rs/.config/trx-rs \
|
||||
/home/trx-rs/.config/systemd/user
|
||||
sudo install -o trx-rs -g trx-rs -m 0600 \
|
||||
trx-rs.toml.example /home/trx-rs/.config/trx-rs/trx-rs.toml
|
||||
sudoedit /home/trx-rs/.config/trx-rs/trx-rs.toml
|
||||
```
|
||||
|
||||
At minimum:
|
||||
|
||||
- remove unused example rigs and remotes;
|
||||
- select the correct serial, TCP, or SDR device;
|
||||
- give every enabled rig a unique ID and audio port;
|
||||
- use `127.0.0.1` for same-host connections;
|
||||
- use a LAN address or `0.0.0.0` only for deliberately remote listeners;
|
||||
- enable authentication before exposing server control or the web UI;
|
||||
- replace every example password and token;
|
||||
- keep credential files mode `0600` and owned by `trx-rs`;
|
||||
- set `cookie_secure = true` when the web UI is served through HTTPS.
|
||||
|
||||
For a remote server, control and each per-rig audio listener need an explicit
|
||||
non-loopback address:
|
||||
|
||||
```toml
|
||||
[trx-server.listen]
|
||||
enabled = true
|
||||
listen = "0.0.0.0"
|
||||
port = 4530
|
||||
|
||||
[trx-server.listen.auth]
|
||||
tokens_file = "/home/trx-rs/.config/trx-rs/server-tokens"
|
||||
|
||||
[[trx-server.rigs]]
|
||||
id = "station-hf"
|
||||
|
||||
[trx-server.rigs.audio]
|
||||
enabled = true
|
||||
listen = "0.0.0.0"
|
||||
port = 4531
|
||||
```
|
||||
|
||||
When using several `[[trx-server.rigs]]` entries, configure audio under each
|
||||
`[trx-server.rigs.audio]` section. Do not rely on the legacy flat
|
||||
`[trx-server.audio]` section.
|
||||
|
||||
For a web UI behind a reverse proxy, keep the backend on loopback and choose an
|
||||
unused port. The proxy upstream must use the same address and port:
|
||||
|
||||
```toml
|
||||
[trx-client.frontends.http]
|
||||
enabled = true
|
||||
listen = "127.0.0.1"
|
||||
port = 7345
|
||||
|
||||
[trx-client.frontends.http.auth]
|
||||
enabled = true
|
||||
users_file = "/home/trx-rs/.config/trx-rs/http-users.json"
|
||||
cookie_secure = true
|
||||
```
|
||||
|
||||
Validate the configuration before starting either daemon:
|
||||
|
||||
```bash
|
||||
sudo -u trx-rs /opt/trx-rs/bin/trx-server \
|
||||
--check-config --config /home/trx-rs/.config/trx-rs/trx-rs.toml
|
||||
sudo -u trx-rs /opt/trx-rs/bin/trx-client \
|
||||
--check-config --config /home/trx-rs/.config/trx-rs/trx-rs.toml
|
||||
```
|
||||
|
||||
Treat unknown-key and deprecated-section warnings as deployment errors. They
|
||||
often mean a setting is not applied where expected.
|
||||
|
||||
## 5. Install the systemd user services
|
||||
|
||||
Render the packaged units with the immutable binary directory:
|
||||
|
||||
```bash
|
||||
sed 's|@BINDIR@|/opt/trx-rs/bin|g' packaging/systemd/trx-server.service \
|
||||
| sudo tee /home/trx-rs/.config/systemd/user/trx-server.service >/dev/null
|
||||
sed 's|@BINDIR@|/opt/trx-rs/bin|g' packaging/systemd/trx-client.service \
|
||||
| sudo tee /home/trx-rs/.config/systemd/user/trx-client.service >/dev/null
|
||||
sudo chown trx-rs:trx-rs \
|
||||
/home/trx-rs/.config/systemd/user/trx-server.service \
|
||||
/home/trx-rs/.config/systemd/user/trx-client.service
|
||||
sudo chmod 0644 \
|
||||
/home/trx-rs/.config/systemd/user/trx-server.service \
|
||||
/home/trx-rs/.config/systemd/user/trx-client.service
|
||||
```
|
||||
|
||||
Enable lingering so the user manager runs without an interactive login, then
|
||||
start it:
|
||||
|
||||
```bash
|
||||
sudo loginctl enable-linger trx-rs
|
||||
trx_uid=$(id -u trx-rs)
|
||||
sudo systemctl start "user@${trx_uid}.service"
|
||||
```
|
||||
|
||||
Manage the user services through that user's runtime directory:
|
||||
|
||||
```bash
|
||||
trx_uid=$(id -u trx-rs)
|
||||
sudo -u trx-rs XDG_RUNTIME_DIR="/run/user/${trx_uid}" \
|
||||
systemctl --user daemon-reload
|
||||
sudo -u trx-rs XDG_RUNTIME_DIR="/run/user/${trx_uid}" \
|
||||
systemctl --user enable --now trx-server.service trx-client.service
|
||||
```
|
||||
|
||||
The client unit has ordering, but not activation, on `trx-server.service`. If
|
||||
both are enabled locally, the client starts after the server process is
|
||||
started. If the local server is disabled because all remotes are external, the
|
||||
client does not start it. The client retries remote connections; systemd cannot
|
||||
order startup against a service on another host.
|
||||
|
||||
## 6. Firewall and reverse proxy
|
||||
|
||||
Allow only required ports and sources. For example, permit server control and
|
||||
audio only from the trusted radio subnet, not from every interface. Exact
|
||||
commands differ between nftables, firewalld, and ufw.
|
||||
|
||||
For the web UI, terminate TLS in a maintained reverse proxy and send traffic to
|
||||
the loopback backend. Preserve WebSocket upgrade headers if the proxy requires
|
||||
them. Example nginx location:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:7345;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
```
|
||||
|
||||
Test the backend directly before debugging a `502 Bad Gateway` response:
|
||||
|
||||
```bash
|
||||
curl --fail --show-error http://127.0.0.1:7345/
|
||||
```
|
||||
|
||||
A refused connection means nothing is listening at the proxy's configured
|
||||
address and port. Check for port conflicts with `ss -ltnp` and confirm the
|
||||
application log reports the same bind address as the proxy upstream.
|
||||
|
||||
## 7. Verify the deployment
|
||||
|
||||
```bash
|
||||
trx_uid=$(id -u trx-rs)
|
||||
sudo -u trx-rs XDG_RUNTIME_DIR="/run/user/${trx_uid}" \
|
||||
systemctl --user status trx-server.service trx-client.service
|
||||
sudo journalctl _UID="${trx_uid}" \
|
||||
-u trx-server.service -u trx-client.service --since today
|
||||
sudo ss -ltnp
|
||||
```
|
||||
|
||||
Verify all of the following:
|
||||
|
||||
- both required services remain active without a restart loop;
|
||||
- radio hardware opens successfully;
|
||||
- logs show the intended control, audio, and frontend bind addresses;
|
||||
- the client connects to every configured control and audio endpoint;
|
||||
- only intended interfaces expose listeners;
|
||||
- authentication works and anonymous access is rejected where configured;
|
||||
- the HTTPS proxy returns the UI and supports live updates;
|
||||
- the service account cannot write `/opt/trx-rs/bin`.
|
||||
|
||||
Do not rely only on `systemctl` reporting `active`: individual frontend tasks
|
||||
can fail after the main client process starts. Always inspect startup logs and
|
||||
probe each required endpoint.
|
||||
|
||||
## 8. Upgrade and roll back
|
||||
|
||||
Build and validate a new revision before replacing the installed executables.
|
||||
Back up configuration and state first. Stop the services, install all binaries
|
||||
from the same build, and restart:
|
||||
|
||||
```bash
|
||||
trx_uid=$(id -u trx-rs)
|
||||
sudo -u trx-rs XDG_RUNTIME_DIR="/run/user/${trx_uid}" \
|
||||
systemctl --user stop trx-client.service trx-server.service
|
||||
sudo install -o root -g root -m 0755 \
|
||||
target/release/trx-server \
|
||||
target/release/trx-client \
|
||||
target/release/trx-configurator \
|
||||
/opt/trx-rs/bin/
|
||||
sudo -u trx-rs XDG_RUNTIME_DIR="/run/user/${trx_uid}" \
|
||||
systemctl --user start trx-server.service trx-client.service
|
||||
```
|
||||
|
||||
Keep the previous binaries or package revision available for rollback. If the
|
||||
new version fails, stop both services, restore the complete previous binary
|
||||
set, restore configuration only if its format changed, and start the services
|
||||
again.
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
# Install with `script/install.sh` (which substitutes @BINDIR@ and copies this
|
||||
# into ~/.config/systemd/user/), or by hand:
|
||||
# sed 's|@BINDIR@|/usr/local/bin|' trx-client.service \
|
||||
# sed "s|@BINDIR@|$HOME/.local/bin|" trx-client.service \
|
||||
# > ~/.config/systemd/user/trx-client.service
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user enable --now trx-client.service
|
||||
@@ -33,9 +33,6 @@ RestartSec=2
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=15
|
||||
NoNewPrivileges=true
|
||||
# The client opens outbound TCP connections to remote servers and binds TCP
|
||||
# listeners for its configured HTTP, JSON, rigctl, and audio frontends.
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#
|
||||
# Install with `script/install.sh` (which substitutes @BINDIR@ and copies this
|
||||
# into ~/.config/systemd/user/), or by hand:
|
||||
# sed 's|@BINDIR@|/usr/local/bin|' trx-server.service \
|
||||
# sed "s|@BINDIR@|$HOME/.local/bin|" trx-server.service \
|
||||
# > ~/.config/systemd/user/trx-server.service
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user enable --now trx-server.service
|
||||
@@ -33,9 +33,6 @@ RestartSec=2
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=15
|
||||
NoNewPrivileges=true
|
||||
# Permit local IPC plus IPv4/IPv6 listening and outbound connections. Ports
|
||||
# are configuration-defined, so SocketBindAllow= cannot safely narrow them.
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BINARIES=(trx-server trx-client trx-configurator)
|
||||
|
||||
# --- Defaults (override via flags or environment) ---------------------------
|
||||
PREFIX="${PREFIX:-/usr/local}"
|
||||
PREFIX="${PREFIX:-$HOME/.local}"
|
||||
BINDIR="${BINDIR:-}" # derived from PREFIX unless set explicitly
|
||||
NO_SDR=0
|
||||
DO_BUILD=1
|
||||
@@ -33,7 +33,7 @@ Usage: script/install.sh [options]
|
||||
Build trx-rs (release) and install it.
|
||||
|
||||
Options:
|
||||
--prefix DIR Install prefix; binaries go to DIR/bin (default: /usr/local)
|
||||
--prefix DIR Install prefix; binaries go to DIR/bin (default: \$HOME/.local)
|
||||
--bindir DIR Install binaries directly to DIR (overrides --prefix)
|
||||
--no-sdr Build without SoapySDR support (--no-default-features)
|
||||
--no-build Skip cargo build; install existing target/release binaries
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ set -euo pipefail
|
||||
|
||||
BINARIES=(trx-server trx-client trx-configurator)
|
||||
|
||||
PREFIX="${PREFIX:-/usr/local}"
|
||||
PREFIX="${PREFIX:-$HOME/.local}"
|
||||
BINDIR="${BINDIR:-}"
|
||||
PURGE=0
|
||||
|
||||
@@ -23,7 +23,7 @@ usage() {
|
||||
Usage: script/uninstall.sh [options]
|
||||
|
||||
Options:
|
||||
--prefix DIR Install prefix binaries were installed under (default: /usr/local)
|
||||
--prefix DIR Install prefix binaries were installed under (default: \$HOME/.local)
|
||||
--bindir DIR Directory binaries were installed to (overrides --prefix)
|
||||
--purge Also delete the user config directory ($CONFIG_DIR)
|
||||
-h, --help Show this help
|
||||
|
||||
@@ -922,11 +922,10 @@ fn bound_sockets(cli: &Cli, cfg: &ServerConfig, rigs: &[RigInstanceConfig]) -> V
|
||||
"[listen]",
|
||||
));
|
||||
}
|
||||
let audio_ip = cli.listen.unwrap_or(cfg.audio.listen);
|
||||
for rig in rigs {
|
||||
if rig.audio.enabled {
|
||||
sockets.push(BoundSocket::new(
|
||||
audio_ip,
|
||||
cli.listen.unwrap_or(rig.audio.listen),
|
||||
rig.audio.port,
|
||||
format!("rig \"{}\" [audio]", rig.id),
|
||||
));
|
||||
@@ -1276,9 +1275,9 @@ async fn main() -> DynResult<()> {
|
||||
}
|
||||
}));
|
||||
|
||||
// Spawn audio stack.
|
||||
// listen_override priority: --listen CLI flag > global [audio].listen > per-rig default.
|
||||
let audio_listen_override = cli.listen.or(Some(cfg.audio.listen));
|
||||
// Spawn audio stack. --listen overrides every configured bind address;
|
||||
// otherwise each rig keeps its own [rigs.audio].listen value.
|
||||
let audio_listen_override = cli.listen;
|
||||
#[cfg(feature = "soapysdr")]
|
||||
let audio_vchan_manager = sdr_vchan_manager.clone();
|
||||
#[cfg(not(feature = "soapysdr"))]
|
||||
@@ -1417,6 +1416,40 @@ mod tests {
|
||||
assert!(parse_serial_addr(" ").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bound_sockets_preserves_per_rig_audio_addresses() {
|
||||
let cli = Cli::parse_from(["trx-server"]);
|
||||
let cfg = ServerConfig::default();
|
||||
let mut local = RigInstanceConfig::default();
|
||||
local.id = "local".into();
|
||||
local.audio.listen = "127.0.0.1".parse().unwrap();
|
||||
local.audio.port = 4531;
|
||||
let mut remote = RigInstanceConfig::default();
|
||||
remote.id = "remote".into();
|
||||
remote.audio.listen = "0.0.0.0".parse().unwrap();
|
||||
remote.audio.port = 4532;
|
||||
|
||||
let sockets = bound_sockets(&cli, &cfg, &[local, remote]);
|
||||
let local = sockets.iter().find(|s| s.port == 4531).unwrap();
|
||||
let remote = sockets.iter().find(|s| s.port == 4532).unwrap();
|
||||
assert_eq!(local.addr, "127.0.0.1".parse::<IpAddr>().unwrap());
|
||||
assert_eq!(remote.addr, "0.0.0.0".parse::<IpAddr>().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bound_sockets_applies_cli_listen_to_every_audio_listener() {
|
||||
let cli = Cli::parse_from(["trx-server", "--listen", "192.0.2.10"]);
|
||||
let cfg = ServerConfig::default();
|
||||
let mut rig = RigInstanceConfig::default();
|
||||
rig.id = "remote".into();
|
||||
rig.audio.listen = "127.0.0.1".parse().unwrap();
|
||||
rig.audio.port = 4532;
|
||||
|
||||
let sockets = bound_sockets(&cli, &cfg, &[rig]);
|
||||
let audio = sockets.iter().find(|s| s.port == 4532).unwrap();
|
||||
assert_eq!(audio.addr, "192.0.2.10".parse::<IpAddr>().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_audio_bandwidth_for_mode_table() {
|
||||
assert_eq!(default_audio_bandwidth_for_mode(&RigMode::USB), 3_000);
|
||||
|
||||
Reference in New Issue
Block a user