From 5b1e39cfca3932852c8f09ccbdb0c44d23994a65 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Tue, 9 Dec 2025 08:35:15 +0100 Subject: [PATCH] Show all connected Tailscale peers with connection methods Replace single connection method display with individual sub-service rows for each online Tailscale peer. Each peer shows hostname and connection type (direct, relay, or idle) allowing monitoring of all connected devices and their connection quality. Query tailscale status --json to enumerate all online peers and display each as a separate sub-service under tailscaled. --- Cargo.lock | 6 +-- agent/Cargo.toml | 2 +- agent/src/collectors/systemd.rs | 70 ++++++++++++++++++++------------- dashboard/Cargo.toml | 2 +- shared/Cargo.toml | 2 +- 5 files changed, 48 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 314f25a..70d4551 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cm-dashboard" -version = "0.1.261" +version = "0.1.262" dependencies = [ "anyhow", "chrono", @@ -301,7 +301,7 @@ dependencies = [ [[package]] name = "cm-dashboard-agent" -version = "0.1.261" +version = "0.1.262" dependencies = [ "anyhow", "async-trait", @@ -325,7 +325,7 @@ dependencies = [ [[package]] name = "cm-dashboard-shared" -version = "0.1.261" +version = "0.1.262" dependencies = [ "chrono", "serde", diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 76d1d97..6dc9212 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-agent" -version = "0.1.262" +version = "0.1.263" edition = "2021" [dependencies] diff --git a/agent/src/collectors/systemd.rs b/agent/src/collectors/systemd.rs index 9a30322..4fc0db7 100644 --- a/agent/src/collectors/systemd.rs +++ b/agent/src/collectors/systemd.rs @@ -217,14 +217,15 @@ impl SystemdCollector { } if service_name == "tailscaled" && status_info.active_state == "active" { - // Add Tailscale connection method as sub-service - if let Some(conn_method) = self.get_tailscale_connection_method() { + // Add Tailscale peers with their connection methods as sub-services + let peers = self.get_tailscale_peers(); + for (peer_name, conn_method) in peers { let metrics = Vec::new(); sub_services.push(SubServiceData { - name: format!("Connection: {}", conn_method), + name: format!("{}: {}", peer_name, conn_method), service_status: Status::Info, metrics, - service_type: "tailscale_connection".to_string(), + service_type: "tailscale_peer".to_string(), }); } } @@ -936,50 +937,63 @@ impl SystemdCollector { None } - /// Get Tailscale connection method (direct, relay, or proxy) - fn get_tailscale_connection_method(&self) -> Option { + /// Get Tailscale connected peers with their connection methods + /// Returns a list of (device_name, connection_method) tuples + fn get_tailscale_peers(&self) -> Vec<(String, String)> { match Command::new("timeout") .args(["2", "tailscale", "status", "--json"]) .output() { Ok(output) if output.status.success() => { let json_str = String::from_utf8_lossy(&output.stdout); + let mut peers = Vec::new(); if let Ok(json_data) = serde_json::from_str::(&json_str) { // Look for the self peer (current node) in the peer list - if let Some(peers) = json_data["Peer"].as_object() { - // Find the first active peer connection to determine connection method - for (_peer_id, peer_data) in peers { - if peer_data["Active"].as_bool().unwrap_or(false) { + if let Some(peer_map) = json_data["Peer"].as_object() { + // Iterate through all peers + for (_peer_id, peer_data) in peer_map { + // Only include active/online peers + if !peer_data["Online"].as_bool().unwrap_or(false) { + continue; + } + + // Get peer hostname or DNS name + let peer_name = peer_data["HostName"] + .as_str() + .or_else(|| peer_data["DNSName"].as_str()) + .unwrap_or("unknown") + .trim_end_matches('.') + .to_string(); + + // Determine connection method + let connection_method = if peer_data["Active"].as_bool().unwrap_or(false) { // Check if using relay let relay_node = peer_data["Relay"].as_str().unwrap_or(""); if !relay_node.is_empty() { - return Some("relay".to_string()); - } - - // Check if using direct connection - if let Some(endpoints) = peer_data["CurAddr"].as_str() { - if !endpoints.is_empty() { - return Some("direct".to_string()); + "relay" + } else if let Some(cur_addr) = peer_data["CurAddr"].as_str() { + // Check if using direct connection + if !cur_addr.is_empty() { + "direct" + } else { + "unknown" } + } else { + "unknown" } - } - } - } + } else { + "idle" + }; - // Check if using proxy from backend state - if let Some(backend_state) = json_data["BackendState"].as_str() { - if backend_state == "Running" { - // If we're running but have no direct or relay, might be proxy - // This is a fallback heuristic - return Some("unknown".to_string()); + peers.push((peer_name, connection_method.to_string())); } } } - None + peers } - _ => None, + _ => Vec::new(), } } diff --git a/dashboard/Cargo.toml b/dashboard/Cargo.toml index f03b199..680d50c 100644 --- a/dashboard/Cargo.toml +++ b/dashboard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard" -version = "0.1.262" +version = "0.1.263" edition = "2021" [dependencies] diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 1f9e531..484b134 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-dashboard-shared" -version = "0.1.262" +version = "0.1.263" edition = "2021" [dependencies]