Add Pi relay server bundle (server/)

This commit is contained in:
Ednunp
2026-05-13 17:05:41 +01:00
parent 17259438c6
commit a6542e38ef
7 changed files with 749 additions and 0 deletions
+7
View File
@@ -47,8 +47,15 @@ src/RemSound.Sender capture → mix → encode → UDP send
src/RemSound.Receiver UDP receive → ring buffer → drift-corrected playout → render
src/RemSound.Harness console test program (1 sender → 1 receiver, no UI)
src/RemSound.App WinForms UI (sender + receiver + heartbeat + discovery + updater)
server/ optional Raspberry Pi / systemd-Linux relay bundle (see below)
```
## Optional: running your own relay server
Two RemSound peers normally reach each other directly over your LAN, or via Tailscale across the internet. If neither of those work for your situation — for example one peer is behind a router that won't forward inbound UDP and you'd prefer not to use Tailscale — you can run a small Python relay on a publicly-reachable host (a Raspberry Pi at home with one UDP port forwarded works fine) and have both peers dial that.
The `server/` folder in this repo is a self-contained bundle: relay script, systemd unit, install / uninstall / smoke-test scripts, and a step-by-step README. See [`server/README.md`](server/README.md) for the setup walkthrough.
## Issues and feedback
Open an issue on the [GitHub issues page](https://github.com/Ednunp/RemSound/issues). If reporting an audio problem, please tick **File → Preferences → Enable logs**, reproduce the issue, then attach the latest log file from `logs\` next to `RemSound.exe`.
+217
View File
@@ -0,0 +1,217 @@
# RemSound UDP Relay — server bundle
This folder is a self-contained bundle for running the RemSound UDP relay on a Raspberry Pi (or any systemd-based Linux). Drop it on the Pi, run `install.sh`, open one router port, and you have a relay another RemSound peer can dial.
## Why this exists
RemSound is a Windows audio app that moves real-time audio between two PCs over plain UDP. The standard way two RemSound peers reach each other when one of them is behind a router that won't forward inbound UDP is to run Tailscale on both ends — Tailscale's WireGuard tunnel handles the NAT traversal. That works, but it adds encryption overhead and sometimes routes traffic through Tailscale's DERP relay rather than directly.
A relay is a tiny UDP reflector that sits on a publicly-reachable host. Both peers dial it, the relay learns each peer's apparent address from their first packet, and forwards datagrams between matched peers. RemSound peers stay behind their NATs — the relay is the only thing that needs to be reachable from the public internet.
Whether a relay improves perceived latency depends on geography. Running a relay close to one of the peers can give a measurable improvement over Tailscale's DERP fallback; running a relay far from both peers usually makes things worse than direct Tailscale. The relay is most useful when at least one peer is behind a router that won't let inbound UDP through at all (so direct peer-to-peer is impossible) and Tailscale is undesirable for some reason (encryption overhead, account requirement, mobile data plan).
## What's in this bundle
```
remsound-relay.py — the relay service itself (Python 3, stdlib only)
remsound-relay.service — systemd unit
install.sh — one-shot installer (copy + enable + start)
uninstall.sh — clean uninstaller
smoke-test.sh — health check after install
README.md — this file
```
No external dependencies. The script uses only the Python 3 standard library.
## What it does
- Listens on UDP port **47830**.
- Validates the 12-byte RemSound packet header (magic `RMND`, version 1) and silently drops anything else. **Never decodes audio. Never logs payload bytes.**
- Holds two peer slots. The first two distinct UDP endpoints to send valid RemSound packets claim the slots. Once both slots are filled, every valid packet from one slot is forwarded to the other.
- Slots that go silent for more than 60 seconds are replaced when a fresh endpoint arrives — so a stale pairing self-heals when one side restarts RemSound.
- Logs to `/var/log/remsound-relay.log` — startup, peer-joined / peer-paired / peer-dropped events, per-minute throughput stats. No payload bytes, ever.
## Install
1. Copy this whole folder onto the Pi. SSH or USB stick are both fine. Example over SSH from your laptop, replacing `pi@your-pi-host` with your actual Pi user@host:
```
scp -r server pi@your-pi-host:/home/pi/remsound-relay
```
2. SSH into the Pi:
```
ssh pi@your-pi-host
```
3. Run the installer:
```
cd /home/pi/remsound-relay
sudo ./install.sh
```
It copies the script to `/usr/local/sbin/`, installs the systemd unit, enables and starts the service, and prints the status + last few log lines.
4. Optional: run the smoke test to confirm everything works locally:
```
sudo ./smoke-test.sh
```
You should see `[ok] remsound-relay.service is active`, `[ok] listening on UDP 47830`, and `[ok] relay logged a peer_joined event for 127.0.0.1 — header validation works`.
## Open one router port
Without this step the relay only works on your LAN. To make it reachable from the internet:
1. Find the Pi's LAN IP (`hostname -I` on the Pi, take the first IPv4 address).
2. In your router's admin UI, add a port-forward rule:
- **Protocol:** UDP
- **WAN port:** 47830
- **Forward IP:** the Pi's LAN IP (from step 1)
- **Forward port:** 47830
3. Save / apply.
That's the same kind of rule any other "let an outside service reach a device on my LAN" flow needs. Different routers have it under different menus (Settings → NAT, Settings → Port Forwarding, Advanced → Virtual Server, etc.). The protocol must be **UDP**, not TCP.
If your router doesn't allow inbound port-forwards at all, this relay can't be reached from outside your LAN. In that case stay on Tailscale.
## Tell the other peer the address
The other end of RemSound needs to dial **your-public-host:47830**.
If you have a static public IP, your peer types it directly:
```
123.45.67.89:47830
```
If your IP is dynamic (most home connections are), you'll want a Dynamic-DNS hostname. Free options that work well with consumer routers:
- **Namecheap Dynamic DNS** (if you own a domain there) — set up a host record and run their updater on the Pi or in your router.
- **DuckDNS** (free, no domain needed) — your hostname looks like `something.duckdns.org`.
- **No-IP**, **DynDNS**, etc.
Whichever you pick, the result is a hostname like `mypi.example.com` that always points at your current public IP. The other peer dials `mypi.example.com:47830` and it just works.
## Use it from RemSound
In RemSound's "Add peer by IP or hostname" field, both ends type **just the hostname** — no port suffix needed:
```
your-public-host
```
RemSound defaults to port **47830** (the relay convention) when you don't type a `:port` suffix. Both ends must type the same address so they meet at the same relay.
If you do want LAN peer-to-peer (no relay), type the host with an explicit port:
```
192.168.1.42:47820
```
The build auto-detects relay-vs-LAN from the port: anything ≠ 47820 is treated as a relay endpoint and heartbeat shares the audio sender's UDP socket so both flows traverse the same NAT pinhole. No UI toggle needed.
## Verify it's working
Once both ends have dialled the relay, on the Pi you'll see in `/var/log/remsound-relay.log`:
```
event=peer_joined addr=A.B.C.D:NNNN slots_filled=1
event=peer_joined addr=W.X.Y.Z:NNNN slots_filled=2
event=peer_paired a=A.B.C.D:NNNN b=W.X.Y.Z:NNNN
event=stats forwarded=NNNNN dropped_unpaired=0 rejected_bad_header=0 ...
```
A live tail from the Pi:
```
sudo tail -f /var/log/remsound-relay.log
```
Stats lines come once a minute. `forwarded=N` is the count of packets the relay reflected between peers in that minute. RemSound at 10 ms Opus generates ~100 packets/sec per direction = ~12,000/minute total. PCM with Tight ASIO can be 10× that.
## Operational quick-reference
```
# Service control
sudo systemctl status remsound-relay
sudo systemctl restart remsound-relay
sudo systemctl stop remsound-relay
sudo systemctl start remsound-relay
# Logs
sudo tail -f /var/log/remsound-relay.log
sudo journalctl -u remsound-relay --since "30 minutes ago" --no-pager
# Listening socket
sudo ss -lunp | grep 47830
# Health check (re-runnable any time)
sudo /home/pi/remsound-relay/smoke-test.sh
```
## Update / re-install
To pick up a newer copy of the bundle, `git pull` (or re-download) the RemSound repo, copy the latest `server/` folder to the Pi, and run `sudo ./install.sh` again. It overwrites the script and restarts the service. No state to migrate.
## Uninstall
```
sudo ./uninstall.sh
```
Stops the service, disables it, removes the script and systemd unit. Leaves the log file alone — delete `/var/log/remsound-relay.log` yourself if you don't want it kept. Doesn't touch your router port-forward.
## Troubleshooting
**Service won't start.** `sudo systemctl status remsound-relay` will show an error. The most likely cause is something else already bound to UDP 47830 — `sudo ss -lunp | grep 47830` will show what. Pick a different port: edit `DEFAULT_PORT` in `/usr/local/sbin/remsound-relay.py`, restart with `sudo systemctl restart remsound-relay`, and tell the other peer the new port number.
**Relay running but the other peer can't connect.** Almost always the router port-forward. Check:
- The rule is **UDP** not TCP.
- The internal IP matches the Pi's actual LAN IP (run `hostname -I` to confirm).
- WAN port and forward port are both 47830.
- Some routers need a reboot after a new rule. Try one if nothing else helps.
To prove the port is open from outside, on a non-LAN machine (a phone on mobile data works) send a test packet:
```
python3 -c "import socket; s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.sendto(bytes([0x52,0x4D,0x4E,0x44,1,4,1,0,1,0,0,0]), ('your-public-host', 47830))"
```
Then on the Pi:
```
sudo tail -10 /var/log/remsound-relay.log
```
You should see `event=peer_joined` with the source address.
**Audio plays but is laggy / clicky.** That's not the relay's problem. The relay forwards packets blind — anything fancy belongs at the RemSound endpoints. Check the RemSound diagnostics: codec choice (PCM vs Opus, Opus 10 ms is the lowest-latency Opus), buffer smoothness, and either side's upload bandwidth. PCM with Tight ASIO is bandwidth-heavy (often 10+ Mbps); Opus 10 ms is around 200 kbps.
**Asymmetric packet rate in the stats line.** Like `peers=[X(rx=90000,tx=12000), Y(rx=12000,tx=90000)]`. That just means one side is sending many more packets than the other — usually because one side is on PCM-with-Tight-ASIO and the other is on Opus. Fine in itself, but if the high-rate side's home upload is saturated it'll cause jitter. Switch that side to Opus 10 ms.
## Wire format reference (for anyone reading the relay code)
Header is 12 bytes, little-endian:
```
uint32 magic 'RMND' (0x444E4D52)
uint8 version 1
uint8 type 1=Format 2=Audio 3=KeepAlive 4=Heartbeat
uint16 streamId
uint32 sequence
```
The relay validates magic and version, reads the type for logging, and **does not interpret anything past byte 6**. Everything after the header is opaque payload.
## Security note
The relay has no authentication and the audio between peers is plaintext UDP. The trade-off is explicit: this is a music-collaboration tool, not a confidential channel. If you really need privacy, the right answer is to run Tailscale on both ends — that gives you WireGuard encryption end-to-end, at the cost of the latency overhead a relay aims to avoid.
## Questions and issues
Open an issue at <https://github.com/Ednunp/RemSound/issues>.
+83
View File
@@ -0,0 +1,83 @@
#!/bin/bash
# install.sh — RemSound UDP relay installer for a stock Raspberry Pi (or any
# systemd Linux). Idempotent: re-running it just refreshes the files and
# restarts the service.
#
# Run with sudo from inside this folder:
# sudo ./install.sh
#
# What it does:
# 1. Sanity checks: systemd present, python3 present.
# 2. Copies remsound-relay.py to /usr/local/sbin/.
# 3. Copies remsound-relay.service to /etc/systemd/system/.
# 4. Creates an empty /var/log/remsound-relay.log if missing.
# 5. systemctl daemon-reload, enable + start the service.
# 6. Prints status and the last few log lines so you can see it's alive.
set -euo pipefail
if [[ $EUID -ne 0 ]]; then
echo "This installer must run as root. Try: sudo ./install.sh" >&2
exit 1
fi
# Resolve the directory this script lives in, regardless of where it was launched from.
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
if [[ ! -f "$SCRIPT_DIR/remsound-relay.py" || ! -f "$SCRIPT_DIR/remsound-relay.service" ]]; then
echo "Cannot find remsound-relay.py and/or remsound-relay.service next to install.sh." >&2
echo "Run the installer from inside the unzipped bundle folder." >&2
exit 1
fi
if ! command -v systemctl >/dev/null 2>&1; then
echo "systemctl not found. This bundle expects a systemd-based Linux (Raspberry Pi OS, Debian, Ubuntu, etc.)." >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 not found. Install it first: sudo apt-get install -y python3" >&2
exit 1
fi
echo "Installing relay script to /usr/local/sbin/remsound-relay.py ..."
install -o root -g root -m 755 "$SCRIPT_DIR/remsound-relay.py" /usr/local/sbin/remsound-relay.py
python3 -m py_compile /usr/local/sbin/remsound-relay.py
echo "Installing systemd unit to /etc/systemd/system/remsound-relay.service ..."
install -o root -g root -m 644 "$SCRIPT_DIR/remsound-relay.service" /etc/systemd/system/remsound-relay.service
echo "Ensuring log file exists ..."
touch /var/log/remsound-relay.log
chown root:root /var/log/remsound-relay.log
chmod 0644 /var/log/remsound-relay.log
echo "Reloading systemd, enabling, and starting remsound-relay ..."
systemctl daemon-reload
systemctl enable remsound-relay.service >/dev/null
systemctl restart remsound-relay.service
# Give the service a beat to come up before we try to read its state.
sleep 1
echo
echo "=== service status ==="
systemctl --no-pager --full status remsound-relay.service || true
echo
echo "=== last 10 log lines ==="
tail -n 10 /var/log/remsound-relay.log 2>/dev/null || echo "(log empty so far)"
echo
echo "=== listening socket check ==="
if command -v ss >/dev/null 2>&1; then
ss -lunp 2>/dev/null | grep ':47830' || echo "WARNING: no listener on UDP 47830 — see service status above."
else
echo "(ss not installed, skipping)"
fi
echo
echo "Install complete. The relay is running on UDP 47830."
echo "Next step: open a UDP 47830 port-forward on your router toward this Pi's LAN address,"
echo "then dial the Pi's public hostname:47830 from RemSound on each end."
echo "See README.md for full operational notes."
+301
View File
@@ -0,0 +1,301 @@
#!/usr/bin/env python3
"""
RemSound UDP relay.
Listens on a single UDP port and reflects RemSound packets between up to two
peer endpoints. Validates the 12-byte RemSound header (magic 'RMND', version 1)
and silently drops anything else. Never decodes audio.
Pairing model: pragmatic v1. The relay does not key on stream id (two RemSound
instances will use different per-sender stream ids, so keying on it would
prevent pairing). Instead the first two distinct UDP endpoints to send a valid
RemSound packet claim the two peer slots; subsequent valid packets from a
slot's endpoint are reflected to the other slot. Slots that go silent for more
than IDLE_TIMEOUT_SECONDS are replaced when a fresh endpoint arrives.
Spec and operational notes: see README.md alongside this script.
"""
from __future__ import annotations
import argparse
import logging
import logging.handlers
import select
import signal
import socket
import struct
import sys
import time
from dataclasses import dataclass
from typing import Optional
LISTEN_HOST = "0.0.0.0"
DEFAULT_PORT = 47830
RECV_BUFFER_BYTES = 2048
IDLE_TIMEOUT_SECONDS = 60
STATS_INTERVAL_SECONDS = 60
SOCKET_POLL_TIMEOUT_SECONDS = 1.0
DEFAULT_LOG_PATH = "/var/log/remsound-relay.log"
# Wire format constants — little-endian, see RemSound.Core.RemPacket.
HEADER_LEN = 12
MAGIC = b"RMND"
VERSION = 1
TYPE_FORMAT = 1
TYPE_AUDIO = 2
TYPE_KEEPALIVE = 3
TYPE_HEARTBEAT = 4
PACKET_TYPE_NAMES = {
TYPE_FORMAT: "Format",
TYPE_AUDIO: "Audio",
TYPE_KEEPALIVE: "KeepAlive",
TYPE_HEARTBEAT: "Heartbeat",
}
@dataclass
class PeerSlot:
addr: tuple[str, int]
last_seen: float
rx_packets: int = 0
tx_packets: int = 0
@dataclass
class RelayStats:
forwarded: int = 0
dropped_unpaired: int = 0
rejected_bad_header: int = 0
pair_changes: int = 0
def setup_logger(log_path: str) -> logging.Logger:
logger = logging.getLogger("remsound-relay")
logger.setLevel(logging.INFO)
fmt = logging.Formatter(
fmt="%(asctime)s level=%(levelname)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
try:
fh = logging.handlers.WatchedFileHandler(log_path, encoding="utf-8")
fh.setFormatter(fmt)
logger.addHandler(fh)
except OSError as e:
sys.stderr.write(f"remsound-relay: could not open {log_path}: {e}\n")
sh = logging.StreamHandler(sys.stderr)
sh.setFormatter(fmt)
logger.addHandler(sh)
return logger
def parse_header(data: bytes) -> Optional[tuple[int, int, int, int]]:
"""Validate the RemSound header. Returns (version, type, stream_id, sequence) or None."""
if len(data) < HEADER_LEN:
return None
if data[0:4] != MAGIC:
return None
version = data[4]
if version != VERSION:
return None
pkt_type = data[5]
stream_id = struct.unpack_from("<H", data, 6)[0]
sequence = struct.unpack_from("<I", data, 8)[0]
return version, pkt_type, stream_id, sequence
class Relay:
def __init__(self, sock: socket.socket, log: logging.Logger):
self.sock = sock
self.log = log
self.peers: list[PeerSlot] = []
self.stats = RelayStats()
self.last_stats_log = time.monotonic()
@staticmethod
def _fmt_addr(addr: tuple[str, int]) -> str:
return f"{addr[0]}:{addr[1]}"
def find_slot(self, addr: tuple[str, int]) -> Optional[int]:
for i, p in enumerate(self.peers):
if p.addr == addr:
return i
return None
def expire_idle(self, now: float) -> None:
if not self.peers:
return
kept: list[PeerSlot] = []
dropped: list[tuple[str, int]] = []
for p in self.peers:
if (now - p.last_seen) <= IDLE_TIMEOUT_SECONDS:
kept.append(p)
else:
dropped.append(p.addr)
if dropped:
self.peers = kept
for addr in dropped:
self.log.info(
"event=peer_dropped reason=idle addr=%s remaining=%d",
self._fmt_addr(addr),
len(self.peers),
)
self.stats.pair_changes += 1
def admit_or_replace(self, addr: tuple[str, int], now: float) -> int:
if len(self.peers) < 2:
self.peers.append(PeerSlot(addr=addr, last_seen=now))
self.log.info(
"event=peer_joined addr=%s slots_filled=%d",
self._fmt_addr(addr),
len(self.peers),
)
self.stats.pair_changes += 1
if len(self.peers) == 2:
self.log.info(
"event=peer_paired a=%s b=%s",
self._fmt_addr(self.peers[0].addr),
self._fmt_addr(self.peers[1].addr),
)
return len(self.peers) - 1
# Both slots occupied. Replace the stalest one if it has been idle.
oldest = 0 if self.peers[0].last_seen <= self.peers[1].last_seen else 1
if (now - self.peers[oldest].last_seen) > IDLE_TIMEOUT_SECONDS:
old_addr = self.peers[oldest].addr
self.peers[oldest] = PeerSlot(addr=addr, last_seen=now)
self.log.info(
"event=peer_replaced old=%s new=%s",
self._fmt_addr(old_addr),
self._fmt_addr(addr),
)
self.stats.pair_changes += 1
return oldest
return -1 # Both slots active — packet from a third endpoint is ignored.
def handle_packet(self, data: bytes, addr: tuple[str, int]) -> None:
parsed = parse_header(data)
if parsed is None:
self.stats.rejected_bad_header += 1
return
# We do not log per-packet detail (would flood the log). Stats covers it.
_version, _pkt_type, _stream_id, _sequence = parsed
now = time.monotonic()
idx = self.find_slot(addr)
if idx is None:
# Take the chance to age out idle slots first.
self.expire_idle(now)
idx = self.admit_or_replace(addr, now)
if idx < 0:
self.stats.dropped_unpaired += 1
return
peer = self.peers[idx]
peer.last_seen = now
peer.rx_packets += 1
if len(self.peers) == 2:
other = self.peers[1 - idx]
try:
self.sock.sendto(data, other.addr)
other.tx_packets += 1
self.stats.forwarded += 1
except OSError as e:
self.log.warning(
"event=send_failed to=%s err=%s",
self._fmt_addr(other.addr),
e,
)
else:
self.stats.dropped_unpaired += 1
def maybe_log_stats(self, now: float) -> None:
if (now - self.last_stats_log) < STATS_INTERVAL_SECONDS:
return
self.last_stats_log = now
s = self.stats
peers_summary = ", ".join(
f"{self._fmt_addr(p.addr)}(rx={p.rx_packets},tx={p.tx_packets})"
for p in self.peers
) or "none"
self.log.info(
"event=stats forwarded=%d dropped_unpaired=%d rejected_bad_header=%d pair_changes=%d peers=[%s]",
s.forwarded,
s.dropped_unpaired,
s.rejected_bad_header,
s.pair_changes,
peers_summary,
)
# Reset counters so the next stats line shows a per-minute rate.
self.stats = RelayStats()
for p in self.peers:
p.rx_packets = 0
p.tx_packets = 0
def main() -> int:
parser = argparse.ArgumentParser(description="RemSound UDP relay")
parser.add_argument(
"--port",
type=int,
default=DEFAULT_PORT,
help=f"UDP port to listen on (default {DEFAULT_PORT})",
)
parser.add_argument(
"--host",
default=LISTEN_HOST,
help=f"Bind address (default {LISTEN_HOST})",
)
parser.add_argument(
"--log-path",
default=DEFAULT_LOG_PATH,
help=f"Log file path (default {DEFAULT_LOG_PATH})",
)
args = parser.parse_args()
log = setup_logger(args.log_path)
log.info("event=startup version=1 listen=%s:%d", args.host, args.port)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind((args.host, args.port))
except OSError as e:
log.error("event=bind_failed err=%s", e)
return 1
relay = Relay(sock, log)
stop_flag = {"stop": False}
def _stop_signal(_signum, _frame):
stop_flag["stop"] = True
signal.signal(signal.SIGTERM, _stop_signal)
signal.signal(signal.SIGINT, _stop_signal)
try:
while not stop_flag["stop"]:
try:
ready, _, _ = select.select([sock], [], [], SOCKET_POLL_TIMEOUT_SECONDS)
except InterruptedError:
continue
now = time.monotonic()
if ready:
try:
data, addr = sock.recvfrom(RECV_BUFFER_BYTES)
except OSError as e:
log.warning("event=recv_failed err=%s", e)
continue
relay.handle_packet(data, addr)
relay.expire_idle(now)
relay.maybe_log_stats(now)
finally:
log.info("event=shutdown")
sock.close()
return 0
if __name__ == "__main__":
sys.exit(main())
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=RemSound UDP relay
Documentation=file:///usr/local/sbin/remsound-relay.py
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/sbin/remsound-relay.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
# smoke-test.sh — Quick health check for the RemSound relay.
#
# Run after install. Confirms the service is running, listening on UDP 47830,
# and accepts a valid RemSound header (silently dropping it as unpaired, which
# is the expected behaviour with no real peers connected).
#
# Run as a regular user; only the log read at the end needs sudo and that
# step degrades gracefully if you don't have it.
set -u
GREEN=$'\033[0;32m'
RED=$'\033[0;31m'
YELLOW=$'\033[0;33m'
RESET=$'\033[0m'
ok() { printf "%s[ok]%s %s\n" "$GREEN" "$RESET" "$*"; }
fail() { printf "%s[FAIL]%s %s\n" "$RED" "$RESET" "$*"; }
warn() { printf "%s[warn]%s %s\n" "$YELLOW" "$RESET" "$*"; }
failures=0
# 1. Service active?
if systemctl is-active --quiet remsound-relay.service; then
ok "remsound-relay.service is active"
else
fail "remsound-relay.service is not active. Try: sudo systemctl status remsound-relay"
failures=$((failures + 1))
fi
# 2. Listening on UDP 47830?
if command -v ss >/dev/null 2>&1; then
if ss -lun 2>/dev/null | grep -q ':47830'; then
ok "listening on UDP 47830"
else
fail "no UDP 47830 listener visible to ss"
failures=$((failures + 1))
fi
else
warn "ss not installed, skipping listener check"
fi
# 3. Send a synthetic valid RemSound header from this machine to localhost,
# confirm the relay accepts it (it'll be admitted to slot 1 and then
# dropped as unpaired, which logs an event=peer_joined line).
if command -v python3 >/dev/null 2>&1; then
python3 - <<'PY'
import socket
# Magic 'RMND' (little-endian 0x444E4D52), version 1, type 4 (Heartbeat),
# stream id 1, sequence 1.
header = bytes([0x52, 0x4D, 0x4E, 0x44, 1, 4, 1, 0, 1, 0, 0, 0])
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(header, ("127.0.0.1", 47830))
s.close()
PY
ok "sent synthetic valid RemSound header to 127.0.0.1:47830"
else
warn "python3 not installed, skipping header send"
fi
# 4. Look for the resulting peer_joined event in the relay log.
sleep 1
if [[ -r /var/log/remsound-relay.log ]]; then
if tail -20 /var/log/remsound-relay.log 2>/dev/null | grep -q "event=peer_joined.*127.0.0.1"; then
ok "relay logged a peer_joined event for 127.0.0.1 — header validation works"
else
warn "no peer_joined event for 127.0.0.1 found in last 20 log lines (may have aged out if you ran this twice)"
fi
elif sudo -n test -r /var/log/remsound-relay.log 2>/dev/null; then
if sudo tail -20 /var/log/remsound-relay.log 2>/dev/null | grep -q "event=peer_joined.*127.0.0.1"; then
ok "relay logged a peer_joined event for 127.0.0.1 — header validation works"
else
warn "no peer_joined event for 127.0.0.1 found in last 20 log lines"
fi
else
warn "cannot read /var/log/remsound-relay.log (try: sudo $0 to also read the log)"
fi
echo
if (( failures == 0 )); then
echo "${GREEN}All checks passed.${RESET}"
exit 0
else
echo "${RED}${failures} check(s) failed.${RESET}"
exit 1
fi
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# uninstall.sh — Remove the RemSound UDP relay cleanly.
#
# Run with sudo:
# sudo ./uninstall.sh
#
# Stops the service, disables it, removes the script and unit file, and
# leaves the log file in place (rename or delete it yourself if you don't
# want it kept). Does NOT touch your router port-forward — that's separate.
set -euo pipefail
if [[ $EUID -ne 0 ]]; then
echo "This uninstaller must run as root. Try: sudo ./uninstall.sh" >&2
exit 1
fi
if systemctl list-unit-files remsound-relay.service >/dev/null 2>&1; then
echo "Stopping remsound-relay ..."
systemctl stop remsound-relay.service || true
echo "Disabling remsound-relay ..."
systemctl disable remsound-relay.service >/dev/null 2>&1 || true
fi
if [[ -f /etc/systemd/system/remsound-relay.service ]]; then
echo "Removing /etc/systemd/system/remsound-relay.service ..."
rm -f /etc/systemd/system/remsound-relay.service
fi
if [[ -f /usr/local/sbin/remsound-relay.py ]]; then
echo "Removing /usr/local/sbin/remsound-relay.py ..."
rm -f /usr/local/sbin/remsound-relay.py
fi
systemctl daemon-reload
echo
echo "Uninstall complete."
echo "Note: /var/log/remsound-relay.log is left in place — delete it yourself if you don't want it."
echo "Note: any router port-forward you added for UDP 47830 is unchanged — remove that yourself if you no longer need it."