Compare commits

..

2 Commits

Author SHA1 Message Date
8da4522d85 Fix Tailscale peer detection by parsing text output
All checks were successful
Build and Release / build-and-release (push) Successful in 1m13s
Replace JSON parsing with simpler text output parsing from tailscale
status command. The text format clearly shows hostname and connection
method (direct/relay/idle) making detection more reliable.

Fixes issues with incorrect hostname (localhost instead of actual name)
and incorrect connection method detection (showing relay when actually
using direct connection).
2025-12-09 10:34:55 +01:00
5b1e39cfca Show all connected Tailscale peers with connection methods
All checks were successful
Build and Release / build-and-release (push) Successful in 1m38s
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.
2025-12-09 08:35:15 +01:00
5 changed files with 55 additions and 43 deletions

6
Cargo.lock generated
View File

@@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "cm-dashboard"
version = "0.1.261"
version = "0.1.263"
dependencies = [
"anyhow",
"chrono",
@@ -301,7 +301,7 @@ dependencies = [
[[package]]
name = "cm-dashboard-agent"
version = "0.1.261"
version = "0.1.263"
dependencies = [
"anyhow",
"async-trait",
@@ -325,7 +325,7 @@ dependencies = [
[[package]]
name = "cm-dashboard-shared"
version = "0.1.261"
version = "0.1.263"
dependencies = [
"chrono",
"serde",

View File

@@ -1,6 +1,6 @@
[package]
name = "cm-dashboard-agent"
version = "0.1.262"
version = "0.1.264"
edition = "2021"
[dependencies]

View File

@@ -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,61 @@ impl SystemdCollector {
None
}
/// Get Tailscale connection method (direct, relay, or proxy)
fn get_tailscale_connection_method(&self) -> Option<String> {
/// 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"])
.args(["2", "tailscale", "status"])
.output()
{
Ok(output) if output.status.success() => {
let json_str = String::from_utf8_lossy(&output.stdout);
let status_output = String::from_utf8_lossy(&output.stdout);
let mut peers = Vec::new();
if let Ok(json_data) = serde_json::from_str::<serde_json::Value>(&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) {
// 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());
}
}
}
}
// Parse tailscale status output
// Format: IP hostname user os status
// Example: 100.110.98.3 wslbox cm@ linux active; direct 192.168.30.227:53757
for line in status_output.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 5 {
continue; // Skip invalid lines
}
// 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());
// parts[0] = IP
// parts[1] = hostname
// parts[2] = user
// parts[3] = OS
// parts[4+] = status (e.g., "active;", "direct", "192.168.30.227:53757" or "idle;" or "offline")
let hostname = parts[1];
let status_parts = &parts[4..];
// Determine connection method from status
let connection_method = if status_parts.is_empty() {
continue; // Skip if no status
} else {
let status_str = status_parts.join(" ");
if status_str.contains("offline") {
continue; // Skip offline peers
} else if status_str.contains("direct") {
"direct"
} else if status_str.contains("relay") {
"relay"
} else if status_str.contains("idle") {
"idle"
} else if status_str.contains("active") {
"active"
} else {
continue; // Skip unknown status
}
}
};
peers.push((hostname.to_string(), connection_method.to_string()));
}
None
peers
}
_ => None,
_ => Vec::new(),
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "cm-dashboard"
version = "0.1.262"
version = "0.1.264"
edition = "2021"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "cm-dashboard-shared"
version = "0.1.262"
version = "0.1.264"
edition = "2021"
[dependencies]