Files
Skald-Circle/crates/plugin-tailscale-remote/src/tailscale_sys.rs
T
2026-07-10 15:02:09 +01:00

72 lines
2.2 KiB
Rust

use std::net::Ipv4Addr;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::{Context, Result};
use async_trait::async_trait;
use tokio::process::Command;
use core_api::remote::RemoteAccess;
/// Remote-access provider that reads the Tailscale IP from the system daemon.
///
/// Requires `tailscaled` to be installed and running; the `tailscale` CLI must
/// be on PATH. No embedded netstack — zero experimental dependencies.
pub struct TailscaleSystemProvider {
ip: OnceLock<Ipv4Addr>,
connected: AtomicBool,
}
impl TailscaleSystemProvider {
/// Runs `tailscale ip -4`, caches the result, and returns `Self` on success.
pub async fn new() -> Result<Self> {
let provider = Self {
ip: OnceLock::new(),
connected: AtomicBool::new(false),
};
let ip = provider.fetch_ip().await?;
let _ = provider.ip.set(ip);
provider.connected.store(true, Ordering::Relaxed);
Ok(provider)
}
async fn fetch_ip(&self) -> Result<Ipv4Addr> {
let output = Command::new("tailscale")
.args(["ip", "-4"])
.output()
.await
.context("tailscale command not found — is the Tailscale daemon installed and running?")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("tailscale ip -4 failed: {stderr}");
}
let stdout = String::from_utf8_lossy(&output.stdout);
let ip_str = stdout.trim();
ip_str
.parse::<Ipv4Addr>()
.with_context(|| format!("failed to parse tailscale IP: '{ip_str}'"))
}
}
#[async_trait]
impl RemoteAccess for TailscaleSystemProvider {
fn provider_name(&self) -> &str { "tailscale_sys" }
async fn device_ip(&self) -> Result<Ipv4Addr> {
self.ip
.get()
.copied()
.ok_or_else(|| anyhow::anyhow!("tailscale_sys: not connected"))
}
fn is_connected(&self) -> bool {
self.connected.load(Ordering::Relaxed)
}
async fn shutdown(&self) {
self.connected.store(false, Ordering::Relaxed);
}
}