Compare commits
3
Commits
110c0e1d49
...
be9d5c301b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be9d5c301b | ||
|
|
b73c97dd5b | ||
|
|
6276c3feea |
@@ -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`
|
`dialout` and `audio` groups. Remove everything with `script/uninstall.sh`
|
||||||
(add `--purge` to also delete the config).
|
(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
|
## How It Works
|
||||||
|
|
||||||
```mermaid
|
```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.
|
||||||
@@ -33,9 +33,6 @@ RestartSec=2
|
|||||||
KillSignal=SIGINT
|
KillSignal=SIGINT
|
||||||
TimeoutStopSec=15
|
TimeoutStopSec=15
|
||||||
NoNewPrivileges=true
|
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]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ RestartSec=2
|
|||||||
KillSignal=SIGINT
|
KillSignal=SIGINT
|
||||||
TimeoutStopSec=15
|
TimeoutStopSec=15
|
||||||
NoNewPrivileges=true
|
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]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
|
|||||||
@@ -922,11 +922,10 @@ fn bound_sockets(cli: &Cli, cfg: &ServerConfig, rigs: &[RigInstanceConfig]) -> V
|
|||||||
"[listen]",
|
"[listen]",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let audio_ip = cli.listen.unwrap_or(cfg.audio.listen);
|
|
||||||
for rig in rigs {
|
for rig in rigs {
|
||||||
if rig.audio.enabled {
|
if rig.audio.enabled {
|
||||||
sockets.push(BoundSocket::new(
|
sockets.push(BoundSocket::new(
|
||||||
audio_ip,
|
cli.listen.unwrap_or(rig.audio.listen),
|
||||||
rig.audio.port,
|
rig.audio.port,
|
||||||
format!("rig \"{}\" [audio]", rig.id),
|
format!("rig \"{}\" [audio]", rig.id),
|
||||||
));
|
));
|
||||||
@@ -1276,9 +1275,9 @@ async fn main() -> DynResult<()> {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Spawn audio stack.
|
// Spawn audio stack. --listen overrides every configured bind address;
|
||||||
// listen_override priority: --listen CLI flag > global [audio].listen > per-rig default.
|
// otherwise each rig keeps its own [rigs.audio].listen value.
|
||||||
let audio_listen_override = cli.listen.or(Some(cfg.audio.listen));
|
let audio_listen_override = cli.listen;
|
||||||
#[cfg(feature = "soapysdr")]
|
#[cfg(feature = "soapysdr")]
|
||||||
let audio_vchan_manager = sdr_vchan_manager.clone();
|
let audio_vchan_manager = sdr_vchan_manager.clone();
|
||||||
#[cfg(not(feature = "soapysdr"))]
|
#[cfg(not(feature = "soapysdr"))]
|
||||||
@@ -1417,6 +1416,40 @@ mod tests {
|
|||||||
assert!(parse_serial_addr(" ").is_err());
|
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]
|
#[test]
|
||||||
fn default_audio_bandwidth_for_mode_table() {
|
fn default_audio_bandwidth_for_mode_table() {
|
||||||
assert_eq!(default_audio_bandwidth_for_mode(&RigMode::USB), 3_000);
|
assert_eq!(default_audio_bandwidth_for_mode(&RigMode::USB), 3_000);
|
||||||
|
|||||||
Reference in New Issue
Block a user