Phase 8c: cluster-peer-status + cluster-repair support --tailscale-addr #64
+63
-9
@@ -171,6 +171,20 @@ enum Cmd {
|
||||
/// Peer's RPC socket. Typically gossip_port + 1.
|
||||
#[arg(long)]
|
||||
rpc_addr: SocketAddr,
|
||||
/// Phase 8c (2026-07-14): optional Tailscale RPC socket.
|
||||
/// When set, `--rpc-addr` is tried first with a short
|
||||
/// deadline (`--lan-probe-ms`); on failure or timeout we
|
||||
/// fall through to this tailnet address. Roaming ops
|
||||
/// (laptop on LTE, in-flight wifi) get connectivity
|
||||
/// without hand-editing addresses per environment.
|
||||
#[arg(long)]
|
||||
tailscale_addr: Option<SocketAddr>,
|
||||
/// LAN probe deadline in milliseconds. Only used when
|
||||
/// `--tailscale-addr` is set. Default 200ms matches the
|
||||
/// arch doc — long enough for a live LAN handshake, short
|
||||
/// enough that roaming clients don't stall.
|
||||
#[arg(long, default_value_t = 200)]
|
||||
lan_probe_ms: u64,
|
||||
/// Directory holding this node's mTLS material
|
||||
/// (`ca.crt` + `node.crt` + `node.key` from `fleet-ca sign`).
|
||||
#[arg(long)]
|
||||
@@ -247,6 +261,14 @@ enum Cmd {
|
||||
/// Peer's RPC socket. Typically gossip_port + 1.
|
||||
#[arg(long)]
|
||||
rpc_addr: SocketAddr,
|
||||
/// Phase 8c (2026-07-14): optional Tailscale RPC socket
|
||||
/// for LAN-first-with-fallback routing. See
|
||||
/// `cluster-peer-status` for details.
|
||||
#[arg(long)]
|
||||
tailscale_addr: Option<SocketAddr>,
|
||||
/// LAN probe deadline (ms) when `--tailscale-addr` is set.
|
||||
#[arg(long, default_value_t = 200)]
|
||||
lan_probe_ms: u64,
|
||||
/// Directory holding this node's mTLS material.
|
||||
#[arg(long)]
|
||||
tls_dir: PathBuf,
|
||||
@@ -331,9 +353,11 @@ async fn main() -> Result<()> {
|
||||
Cmd::ClusterRepair {
|
||||
peer,
|
||||
rpc_addr,
|
||||
tailscale_addr,
|
||||
lan_probe_ms,
|
||||
tls_dir,
|
||||
dry_run,
|
||||
} => cmd_cluster_repair(&cfg, &peer, rpc_addr, &tls_dir, dry_run).await?,
|
||||
} => cmd_cluster_repair(&cfg, &peer, rpc_addr, tailscale_addr, lan_probe_ms, &tls_dir, dry_run).await?,
|
||||
Cmd::ClusterRefSweep {
|
||||
gitea_url,
|
||||
gitea_token,
|
||||
@@ -346,8 +370,10 @@ async fn main() -> Result<()> {
|
||||
Cmd::ClusterPeerStatus {
|
||||
peer,
|
||||
rpc_addr,
|
||||
tailscale_addr,
|
||||
lan_probe_ms,
|
||||
tls_dir,
|
||||
} => cmd_cluster_peer_status(&peer, rpc_addr, &tls_dir).await?,
|
||||
} => cmd_cluster_peer_status(&peer, rpc_addr, tailscale_addr, lan_probe_ms, &tls_dir).await?,
|
||||
Cmd::FleetCaInit { .. } | Cmd::FleetCaSign { .. } | Cmd::FleetCaTailscaleSign { .. } => {
|
||||
// Handled by the config-independent short-circuit above.
|
||||
unreachable!("fleet-ca commands short-circuit before config load");
|
||||
@@ -520,12 +546,14 @@ async fn cmd_cluster_repair(
|
||||
cfg: &Config,
|
||||
peer: &str,
|
||||
rpc_addr: SocketAddr,
|
||||
tailscale_addr: Option<SocketAddr>,
|
||||
lan_probe_ms: u64,
|
||||
tls_dir: &std::path::Path,
|
||||
dry_run: bool,
|
||||
) -> Result<()> {
|
||||
use cluster::blob::BlobStore;
|
||||
use cluster::rpc::{call_get_chunk, call_has_chunk};
|
||||
use cluster::transport::{NodeIdentity, QuicClient};
|
||||
use cluster::transport::{ConnectRoute, NodeIdentity, QuicClient};
|
||||
|
||||
let root = cfg
|
||||
.cluster
|
||||
@@ -581,14 +609,24 @@ async fn cmd_cluster_repair(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Phase 2: connect to the peer.
|
||||
// Phase 2: connect to the peer. Phase 8c: LAN-first with
|
||||
// optional tailnet fallback when the operator supplied one.
|
||||
let identity = NodeIdentity::from_pem_dir(tls_dir)
|
||||
.with_context(|| format!("loading identity from {}", tls_dir.display()))?;
|
||||
let client = QuicClient::new("0.0.0.0:0".parse()?, identity)?;
|
||||
let conn = client
|
||||
.connect(rpc_addr, peer)
|
||||
let (conn, route) = client
|
||||
.connect_lan_first(
|
||||
peer,
|
||||
Some(rpc_addr),
|
||||
tailscale_addr,
|
||||
std::time::Duration::from_millis(lan_probe_ms),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("connecting to {peer} at {rpc_addr}"))?;
|
||||
.with_context(|| format!("connecting to {peer}"))?;
|
||||
match route {
|
||||
ConnectRoute::Lan(a) => println!("route: LAN ({a})"),
|
||||
ConnectRoute::Tailscale(a) => println!("route: tailnet ({a})"),
|
||||
}
|
||||
|
||||
// Phase 3: repair. Fetcher probes HasChunk first (cheap) so a
|
||||
// peer that lacks the chunk is one round-trip, not a full pull
|
||||
@@ -835,15 +873,31 @@ async fn cmd_cluster_ref_sweep(
|
||||
async fn cmd_cluster_peer_status(
|
||||
peer: &str,
|
||||
rpc_addr: SocketAddr,
|
||||
tailscale_addr: Option<SocketAddr>,
|
||||
lan_probe_ms: u64,
|
||||
tls_dir: &std::path::Path,
|
||||
) -> Result<()> {
|
||||
use cluster::rpc::call_peer_status;
|
||||
use cluster::transport::{NodeIdentity, QuicClient};
|
||||
use cluster::transport::{ConnectRoute, NodeIdentity, QuicClient};
|
||||
|
||||
let identity = NodeIdentity::from_pem_dir(tls_dir)
|
||||
.with_context(|| format!("loading identity from {}", tls_dir.display()))?;
|
||||
let client = QuicClient::new("0.0.0.0:0".parse()?, identity)?;
|
||||
let conn = client.connect(rpc_addr, peer).await?;
|
||||
// Phase 8c: LAN-first with optional tailnet fallback. When the
|
||||
// caller didn't pass --tailscale-addr the behavior is
|
||||
// byte-identical to the pre-8c path (single-addr dial).
|
||||
let (conn, route) = client
|
||||
.connect_lan_first(
|
||||
peer,
|
||||
Some(rpc_addr),
|
||||
tailscale_addr,
|
||||
std::time::Duration::from_millis(lan_probe_ms),
|
||||
)
|
||||
.await?;
|
||||
match route {
|
||||
ConnectRoute::Lan(a) => println!("route: LAN ({a})"),
|
||||
ConnectRoute::Tailscale(a) => println!("route: tailnet ({a})"),
|
||||
}
|
||||
let status = call_peer_status(&conn).await?;
|
||||
|
||||
println!(
|
||||
|
||||
Reference in New Issue
Block a user