Skip to content

Commit b67f2bf

Browse files
committed
address comments
Signed-off-by: kerthcet <kerthcet@gmail.com>
1 parent 2246b2d commit b67f2bf

2 files changed

Lines changed: 72 additions & 27 deletions

File tree

sandd/src/main.rs

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,11 @@ async fn main() -> Result<()> {
196196
info!("Connection closed, reconnecting");
197197
stale_netmap = false;
198198
}
199-
// A dial/handshake error means we never reached the controller — the
200-
// likely cause is a stale netmap pointing at the controller's old IP, so
201-
// force a full refresh before the next attempt.
199+
// connect_and_serve only returns Err when the connection was never
200+
// ESTABLISHED (request build, SOCKS dial, or WebSocket handshake failed) —
201+
// post-handshake serve() errors are folded into Disconnected above. So we
202+
// never reached the controller; the likely cause is a stale netmap pointing
203+
// at its old IP, so force a full refresh before the next attempt.
202204
Err(e) => {
203205
error!("Connection error: {}", e);
204206
stale_netmap = true;
@@ -265,7 +267,9 @@ async fn connect_and_serve(
265267
.await
266268
.context("tunnel: WebSocket handshake over SOCKS5 failed")?;
267269
log_negotiated_protocol(&response);
268-
return serve(ws_stream, daemon_id, heartbeat_interval, labels, shutdown_signal()).await;
270+
return Ok(session_outcome(
271+
serve(ws_stream, daemon_id, heartbeat_interval, labels, shutdown_signal()).await,
272+
));
269273
}
270274

271275
let (ws_stream, response) = match tokio_tungstenite::connect_async(request).await {
@@ -276,7 +280,25 @@ async fn connect_and_serve(
276280
}
277281
};
278282
log_negotiated_protocol(&response);
279-
serve(ws_stream, daemon_id, heartbeat_interval, labels, shutdown_signal()).await
283+
Ok(session_outcome(
284+
serve(ws_stream, daemon_id, heartbeat_interval, labels, shutdown_signal()).await,
285+
))
286+
}
287+
288+
/// Collapse a serve() result into a ServeOutcome for the POST-handshake path. Once the
289+
/// WebSocket is up the mesh path is proven good, so a serve() error is a post-connect
290+
/// failure (registration send, serde, socket reset mid-session) — NOT an unreachable
291+
/// controller. Map it to Disconnected (logged) so main() reconnects WITHOUT forcing a
292+
/// netmap refresh; that keeps an Err from connect_and_serve meaning only "failed to
293+
/// establish the connection", which is exactly the condition stale_netmap keys off of.
294+
fn session_outcome(result: Result<ServeOutcome>) -> ServeOutcome {
295+
match result {
296+
Ok(outcome) => outcome,
297+
Err(e) => {
298+
error!("Session error after connect: {}; reconnecting", e);
299+
ServeOutcome::Disconnected
300+
}
301+
}
280302
}
281303

282304
/// Log the WebSocket subprotocol the server negotiated (shared by both transports).
@@ -844,19 +866,31 @@ async fn setup_tunnel(args: &Args, force_refresh: bool) -> Result<()> {
844866
// reaped daemon looped forever dialing the controller through a dead tunnel and
845867
// never re-registered.
846868
//
847-
// Readiness is probed by connecting to the SOCKS5 port, NOT by `tailscale status`.
848-
// connect_and_serve dials the controller THROUGH that proxy, so the listener being
849-
// up is the exact invariant that matters. A bare `tailscale status` check would
850-
// pass for ANY running tailscaled — including a system/sidecar one started without
851-
// --socks5-server — and we'd then skip our spawn, leaving the proxy absent so every
852-
// connect fails and the loop never recovers. Probing the port instead means: proxy
853-
// reachable => our tunnel is truly up, skip; not reachable => (re)start our own
854-
// tailscaled with the SOCKS listener, even if some other tailscaled exists.
869+
// Readiness needs BOTH checks — each covers the other's blind spot:
870+
// 1. The SOCKS5 port is reachable. connect_and_serve dials the controller THROUGH
871+
// this proxy, so the listener being up is the exact invariant that matters. But
872+
// a raw connect is a false positive if ANY process squats on 127.0.0.1:1055 —
873+
// we'd skip our spawn and then `tailscale up` fails/retries forever against a
874+
// proxy that isn't tailscaled's.
875+
// 2. `tailscale status` succeeds. This confirms a functioning tailscaled is
876+
// actually running (not a squatter, not a half-dead daemon). Alone it is also
877+
// insufficient: it passes for ANY tailscaled — including a system/sidecar one
878+
// started WITHOUT --socks5-server — so the proxy could still be absent.
879+
// Together they mean: proxy reachable AND owned by a live tailscaled => our tunnel is
880+
// truly up, skip. Otherwise (re)start our own tailscaled with the SOCKS listener; if
881+
// a foreign process holds the port, our spawn can't bind it and the poll below fails
882+
// with a clear error rather than looping silently.
855883
let socks_reachable = tokio::net::TcpStream::connect(TUNNEL_SOCKS_PROXY)
856884
.await
857885
.is_ok();
858-
859-
if socks_reachable {
886+
let tailscaled_healthy = socks_reachable
887+
&& Command::new("tailscale")
888+
.arg("status")
889+
.output()
890+
.map(|o| o.status.success())
891+
.unwrap_or(false);
892+
893+
if tailscaled_healthy {
860894
info!("tailscaled SOCKS5 proxy already listening on {}; re-joining mesh", TUNNEL_SOCKS_PROXY);
861895
} else {
862896
info!("Starting tailscaled...");
@@ -880,9 +914,20 @@ async fn setup_tunnel(args: &Args, force_refresh: bool) -> Result<()> {
880914
// fail with a clear, actionable error instead of falling through to an opaque
881915
// "failed to reach controller through SOCKS5 proxy" on every connect. main()'s
882916
// loop then retries setup_tunnel after its backoff, so a slow start recovers.
917+
// Ready only when the port is reachable AND `tailscale status` succeeds — the
918+
// same two-part check as above. A bare port connect would accept a foreign
919+
// process squatting on 1055; requiring status confirms it is OUR tailscaled that
920+
// came up, so we never fall through to a `tailscale up` that can't work.
883921
let mut ready = false;
884922
for _ in 0..20 {
885-
if tokio::net::TcpStream::connect(TUNNEL_SOCKS_PROXY).await.is_ok() {
923+
let socks_up = tokio::net::TcpStream::connect(TUNNEL_SOCKS_PROXY).await.is_ok();
924+
if socks_up
925+
&& Command::new("tailscale")
926+
.arg("status")
927+
.output()
928+
.map(|o| o.status.success())
929+
.unwrap_or(false)
930+
{
886931
ready = true;
887932
break;
888933
}
@@ -891,8 +936,8 @@ async fn setup_tunnel(args: &Args, force_refresh: bool) -> Result<()> {
891936
if !ready {
892937
return Err(anyhow::anyhow!(
893938
"tailscaled SOCKS5 proxy never came up on {} after starting tailscaled \
894-
(is another tailscaled holding /var/lib/tailscale/tailscaled.state, or is \
895-
the port in use?)",
939+
(is another process holding the port, or another tailscaled holding \
940+
/var/lib/tailscale/tailscaled.state?)",
896941
TUNNEL_SOCKS_PROXY
897942
));
898943
}

server/src/lib.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -779,20 +779,20 @@ async fn setup_tunnel_controller(config: &TunnelConfig, verbose: bool) -> anyhow
779779

780780
tracing::info!("Starting tailscaled...");
781781

782-
// Start tailscaled in background (if not already running). The SAME `verbose` flag
783-
// that gates sandd's own logging also gates tailscaled's: when off, mute its routine
784-
// chatter (magicsock/netmap/health lines) with --verbose=-1 and send its stdout/stderr
785-
// to null, so it doesn't flood a `kubectl exec` REPL. Fatal startup failures still
786-
// surface via the `tailscale up` result below.
782+
// Start tailscaled in the background. The SAME `verbose` flag that gates sandd's own
783+
// logging also gates tailscaled's routine chatter: when off, we pass --verbose=-1 to
784+
// silence its per-packet magicsock/netmap/health lines and discard its STDOUT, so it
785+
// doesn't flood a `kubectl exec` REPL. STDERR is deliberately KEPT: --verbose=-1
786+
// already mutes the routine noise there, but a fatal startup failure (bad flag,
787+
// permission denied, or another tailscaled holding the state lock) is reported on
788+
// stderr and would otherwise be lost — `tailscale up` below only says it can't reach
789+
// the daemon, never WHY it exited. Keeping stderr makes those failures diagnosable.
787790
let mut tailscaled = Command::new("tailscaled");
788791
tailscaled
789792
.arg("--tun=userspace-networking")
790793
.arg("--state=/var/lib/tailscale/tailscaled.state");
791794
if !verbose {
792-
tailscaled
793-
.arg("--verbose=-1")
794-
.stdout(Stdio::null())
795-
.stderr(Stdio::null());
795+
tailscaled.arg("--verbose=-1").stdout(Stdio::null());
796796
}
797797
let _tailscaled = tailscaled.spawn().context("Failed to start tailscaled")?;
798798

0 commit comments

Comments
 (0)