Skip to content

Commit 136febd

Browse files
authored
fix rejoin error (#37)
Signed-off-by: kerthcet <kerthcet@gmail.com>
1 parent 66d7df8 commit 136febd

3 files changed

Lines changed: 74 additions & 24 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,5 @@ We welcome any kind of contributions, feedback, and suggestions! See [DEVELOP.md
185185

186186
## License
187187

188-
Apache-2.0 — see [LICENSE](./LICENSE).
188+
Apache-2.0 — see [LICENSE](./LICENSE). Third-party components (e.g. Tailscale, used by
189+
tunnel mode) are listed in [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md).

THIRD_PARTY_NOTICES.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Third-party notices
2+
3+
SandD is licensed under Apache-2.0 (see [LICENSE](./LICENSE)). It relies on the
4+
following third-party software, which is licensed separately.
5+
6+
## Tailscale
7+
8+
Tunnel mode uses the [Tailscale](https://github.com/tailscale/tailscale) client
9+
(`tailscale`/`tailscaled`), © Tailscale Inc., licensed under
10+
[BSD-3-Clause](https://github.com/tailscale/tailscale/blob/main/LICENSE).
11+
12+
SandD invokes it as a separate process and fetches it at runtime (the install script
13+
and tunnel Dockerfiles pull the official binaries) — it is not linked as a library or
14+
included in SandD's source. If you build and redistribute an image with Tailscale
15+
baked in, retain its copyright notice per BSD-3-Clause.

sandd/src/main.rs

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,29 @@ async fn main() -> Result<()> {
133133
info!("Labels: {:?}", labels);
134134
}
135135

136-
// Handle tunnel mode
137136
if args.tunnel {
138137
info!("Tunnel mode enabled");
139-
setup_tunnel(&args).await?;
140138
}
141139

142-
// Main connection loop with reconnection
140+
// Main connection loop with reconnection.
143141
loop {
142+
// In tunnel mode, (re)establish the mesh on EVERY iteration before dialing
143+
// the controller. setup_tunnel is idempotent (see its body): first pass it
144+
// starts tailscaled + joins; on a reconnect it re-runs `tailscale up`, which
145+
// re-registers the node if headscale reaped it. Without this, a reaped daemon
146+
// loops forever dialing the controller through a dead tunnel and never rejoins
147+
// (container stays Running, node stays gone from headscale). On failure, log
148+
// and fall through to the backoff sleep rather than crash — a transient mesh
149+
// failure must not kill a long-lived daemon.
150+
if args.tunnel {
151+
if let Err(e) = setup_tunnel(&args).await {
152+
error!("Failed to (re)establish tunnel: {}; retrying", e);
153+
warn!("Reconnecting in {} seconds...", args.reconnect_interval);
154+
tokio::time::sleep(Duration::from_secs(args.reconnect_interval)).await;
155+
continue;
156+
}
157+
}
158+
144159
match connect_and_serve(
145160
&args.server_url,
146161
&daemon_id,
@@ -793,30 +808,49 @@ async fn setup_tunnel(args: &Args) -> Result<()> {
793808
));
794809
}
795810

796-
info!("Starting tailscaled...");
811+
// setup_tunnel is IDEMPOTENT: main()'s reconnect loop calls it before every
812+
// connect attempt so a dropped/reaped node re-establishes the mesh (not just the
813+
// WebSocket). tailscaled is a long-lived singleton — spawning a second one would
814+
// collide on the SOCKS5 port and the state lock — so we only start it if it isn't
815+
// already up. `tailscale up`, by contrast, ALWAYS re-runs: it is idempotent when
816+
// already connected (cheap no-op) and is exactly what re-activates the node with
817+
// headscale after an ephemeral reap. This is the fix for "container Running but
818+
// node gone from headscale": before, tailscale up ran once at startup only, so a
819+
// reaped daemon looped forever dialing the controller through a dead tunnel and
820+
// never re-registered.
821+
let tailscaled_running = Command::new("tailscale")
822+
.arg("status")
823+
.output()
824+
.map(|o| o.status.success())
825+
.unwrap_or(false);
797826

798-
// Start tailscaled in background.
799-
//
800-
// --socks5-server is what makes tunnel mode actually work: with
801-
// --tun=userspace-networking there is no TUN device and thus no kernel route
802-
// to the tailnet (100.64.0.0/10), so a plain socket to the controller's mesh
803-
// address always fails. The SOCKS5 proxy is the entry point INTO tailscaled's
804-
// userspace network stack; connect_and_serve dials the controller through it
805-
// (see TUNNEL_SOCKS_PROXY) so the WebSocket rides the mesh. Bound to localhost
806-
// so only this container's daemon can use it.
807-
let _tailscaled = Command::new("tailscaled")
808-
.arg("--tun=userspace-networking")
809-
.arg(format!("--socks5-server={}", TUNNEL_SOCKS_PROXY))
810-
.arg("--state=/var/lib/tailscale/tailscaled.state")
811-
.spawn()
812-
.context("Failed to start tailscaled")?;
813-
814-
// Give tailscaled time to start
815-
tokio::time::sleep(Duration::from_secs(2)).await;
827+
if tailscaled_running {
828+
info!("tailscaled already running; re-joining mesh");
829+
} else {
830+
info!("Starting tailscaled...");
831+
// --socks5-server is what makes tunnel mode actually work: with
832+
// --tun=userspace-networking there is no TUN device and thus no kernel route
833+
// to the tailnet (100.64.0.0/10), so a plain socket to the controller's mesh
834+
// address always fails. The SOCKS5 proxy is the entry point INTO tailscaled's
835+
// userspace network stack; connect_and_serve dials the controller through it
836+
// (see TUNNEL_SOCKS_PROXY) so the WebSocket rides the mesh. Bound to localhost
837+
// so only this container's daemon can use it.
838+
Command::new("tailscaled")
839+
.arg("--tun=userspace-networking")
840+
.arg(format!("--socks5-server={}", TUNNEL_SOCKS_PROXY))
841+
.arg("--state=/var/lib/tailscale/tailscaled.state")
842+
.spawn()
843+
.context("Failed to start tailscaled")?;
844+
845+
// Give tailscaled time to start
846+
tokio::time::sleep(Duration::from_secs(2)).await;
847+
}
816848

817849
info!("Joining mesh network...");
818850

819-
// Join mesh
851+
// Join mesh. Always run (even when tailscaled was already up): if the node was
852+
// reaped by headscale this re-registers it; if it's still a valid member this is
853+
// an idempotent no-op.
820854
let output = Command::new("tailscale")
821855
.arg("up")
822856
.arg(format!("--authkey={}", authkey))

0 commit comments

Comments
 (0)