From cff65b42ee615b48e845bc763c0f7646689b27a7 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 22 Sep 2026 21:56:12 +0500 Subject: [PATCH 1/2] fix(bin): graceful shutdown across binaries Short-lived CLI commands dropped the iroh endpoint without close(), so iroh logged "Endpoint dropped without calling Endpoint::close" on every rds id/ticket. The agent and both server binaries had no SIGTERM handling at all: systemctl stop killed them mid-accept, peers saw an abrupt socket death instead of CONNECTION_CLOSE, and relayed websockets got a silent RST. - rds-cli: endpoint.close() before exit. - rds-agent: select accept loop vs SIGINT/SIGTERM, then close the endpoint so peers get CONNECTION_CLOSE. - rds-server/rds-relay: same signal handling, then iroh_relay::Server::shutdown() to close listener + client sockets. --- crates/rds-agent/src/main.rs | 26 +++++++++++++++++++++++++- crates/rds-cli/src/main.rs | 3 +++ crates/rds-relay/src/main.rs | 22 +++++++++++++++++++++- crates/rds-server/src/main.rs | 22 +++++++++++++++++++++- 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/crates/rds-agent/src/main.rs b/crates/rds-agent/src/main.rs index 1e51605..91479b6 100644 --- a/crates/rds-agent/src/main.rs +++ b/crates/rds-agent/src/main.rs @@ -166,5 +166,29 @@ async fn main() -> anyhow::Result<()> { if agent.policy.allow.is_empty() { eprintln!("warning: empty --allow list; every peer will be rejected"); } - agent.run().await + tokio::select! { + res = agent.run() => res?, + _ = shutdown_signal() => {} + } + // Close the endpoint so peers get CONNECTION_CLOSE instead of an + // abrupt socket death (and iroh does not log an ungraceful drop). + agent.endpoint.close().await; + Ok(()) +} + +/// SIGINT on every platform, SIGTERM on unix (systemd stop). +async fn shutdown_signal() { + #[cfg(unix)] + { + use tokio::signal::unix::{SignalKind, signal}; + let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler"); + tokio::select! { + _ = tokio::signal::ctrl_c() => {} + _ = term.recv() => {} + } + } + #[cfg(not(unix))] + { + let _ = tokio::signal::ctrl_c().await; + } } diff --git a/crates/rds-cli/src/main.rs b/crates/rds-cli/src/main.rs index 461cf35..bf6f3c4 100644 --- a/crates/rds-cli/src/main.rs +++ b/crates/rds-cli/src/main.rs @@ -269,6 +269,9 @@ async fn main() -> anyhow::Result<()> { ); } } + // Dropping the endpoint without close() makes iroh log an + // "ungraceful abort" error on every command exit. + endpoint.close().await; Ok(()) } diff --git a/crates/rds-relay/src/main.rs b/crates/rds-relay/src/main.rs index d1550b4..de091c2 100644 --- a/crates/rds-relay/src/main.rs +++ b/crates/rds-relay/src/main.rs @@ -39,6 +39,26 @@ async fn main() -> anyhow::Result<()> { "relay listening on http://{}", server.http_addr().expect("relay config enabled") ); - tokio::signal::ctrl_c().await?; + shutdown_signal().await; + // Graceful stop: close listener + client websockets instead of + // letting attached endpoints hit a silent RST. + let _ = server.shutdown().await; Ok(()) } + +/// SIGINT on every platform, SIGTERM on unix (systemd stop). +async fn shutdown_signal() { + #[cfg(unix)] + { + use tokio::signal::unix::{SignalKind, signal}; + let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler"); + tokio::select! { + _ = tokio::signal::ctrl_c() => {} + _ = term.recv() => {} + } + } + #[cfg(not(unix))] + { + let _ = tokio::signal::ctrl_c().await; + } +} diff --git a/crates/rds-server/src/main.rs b/crates/rds-server/src/main.rs index 75b2a85..39fdf47 100644 --- a/crates/rds-server/src/main.rs +++ b/crates/rds-server/src/main.rs @@ -106,6 +106,26 @@ async fn main() -> anyhow::Result<()> { let relay = rds_relay::serve(cli.relay_addr, allow).await?; info!(addr = %relay.http_addr().expect("relay config enabled"), "relay listening"); - tokio::signal::ctrl_c().await?; + shutdown_signal().await; + // Graceful stop: close listener + client websockets instead of + // letting attached endpoints hit a silent RST. + let _ = relay.shutdown().await; Ok(()) } + +/// SIGINT on every platform, SIGTERM on unix (systemd stop). +async fn shutdown_signal() { + #[cfg(unix)] + { + use tokio::signal::unix::{SignalKind, signal}; + let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler"); + tokio::select! { + _ = tokio::signal::ctrl_c() => {} + _ = term.recv() => {} + } + } + #[cfg(not(unix))] + { + let _ = tokio::signal::ctrl_c().await; + } +} From 4f89311a2534cdc4bb77f4feaa1caf5b46e23bc5 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 22 Sep 2026 21:56:20 +0500 Subject: [PATCH 2/2] docs(deploy): TLS terminator recipe, tunnel verdict, unit URLs - Document the nginx/haproxy TLS-terminator option in front of 3340 and why plaintext HTTP relaying is already safe (E2E payloads, EndpointId admission). - Record the Cloudflare Tunnel/WARP verdict: directory could be tunnel-fronted, relay probably but edge reconnects drop attached endpoints, and no tunnel carries endpoint QUIC/UDP. VPS preferred. - Fix stale Documentation= URL (rldyourmnd -> NDDev-OpenNetwork). --- deploy/systemd/rds-agent.service | 2 +- deploy/systemd/rds-server.service | 2 +- docs/deployment.md | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/deploy/systemd/rds-agent.service b/deploy/systemd/rds-agent.service index 3249ce5..6e4f639 100644 --- a/deploy/systemd/rds-agent.service +++ b/deploy/systemd/rds-agent.service @@ -1,6 +1,6 @@ [Unit] Description=rds-agent: remote-device-sync service host (ssh/tcp forward, desktop, sync) -Documentation=https://github.com/rldyourmnd/remote-device-sync +Documentation=https://github.com/NDDev-OpenNetwork/remote-device-sync After=network-online.target Wants=network-online.target diff --git a/deploy/systemd/rds-server.service b/deploy/systemd/rds-server.service index 9f25963..c538882 100644 --- a/deploy/systemd/rds-server.service +++ b/deploy/systemd/rds-server.service @@ -1,6 +1,6 @@ [Unit] Description=rds-server: relay + discovery directory for remote-device-sync -Documentation=https://github.com/rldyourmnd/remote-device-sync +Documentation=https://github.com/NDDev-OpenNetwork/remote-device-sync After=network-online.target Wants=network-online.target diff --git a/docs/deployment.md b/docs/deployment.md index 643788e..5e368ea 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -64,6 +64,29 @@ table inet rds { accepts direct paths opportunistically (hole-punched or via the endpoint's discovered addresses). +**TLS on the relay.** `rds-server` serves the iroh relay protocol +(WebSocket over HTTP) on 3340 in plaintext: relayed payloads are +end-to-end-encrypted QUIC the relay cannot read, and relay admission is +keyed by `EndpointId` signature challenge, so a network MITM can only +disrupt, not decrypt. For defence-in-depth on a public IP, terminate TLS +in front of 3340 with any TCP-level TLS terminator (nginx `stream`, +haproxy) and give endpoints `--relay https://:`; the +relay protocol rides WebSocket inside TLS unchanged. The embedded +iroh-relay also supports ACME natively — wiring `TlsConfig` through +`rds_relay::serve` is future work, not required for launch. + +**No tunnel/VPN dependency.** The design assumes only *outbound* +connectivity from endpoints: tcp/3340 (relay) + tcp/3341 (directory) + +udp for direct paths. A Cloudflare Tunnel could front the *directory* +(plain HTTP — works) and probably the relay (WebSocket — unverified, and +Cloudflare terminates long-lived proxied connections at the edge, so +attached endpoints would drop whenever `cloudflared` reconnects), but it +cannot carry the endpoints' QUIC/UDP data path at all — public hostnames +do not proxy UDP, and private-network UDP requires every device enrolled +in WARP/Zero Trust. WARP itself is a client VPN solving a problem the +relay already solves without a per-device client dependency. If the +services host has no public IP, prefer any small VPS over a tunnel. + ## Sandboxing Both units set `NoNewPrivileges`, `ProtectSystem=strict`,