First Version

This commit is contained in:
2026-07-10 15:02:09 +01:00
commit 38494a85a9
562 changed files with 196313 additions and 0 deletions
@@ -0,0 +1,89 @@
use std::net::{Ipv4Addr, SocketAddr};
use std::sync::OnceLock;
use anyhow::{Context, Result, anyhow};
use async_trait::async_trait;
use tokio::sync::Mutex;
use tracing::{info, warn};
use core_api::remote::RemoteAccess;
pub struct TailscaleConfig {
/// Tailscale auth key (tskey-auth-...). Required for first join; reused from key file afterwards.
pub auth_key: String,
/// Hostname this node will request on the tailnet.
pub hostname: String,
/// Path where tailscale-rs persists node keys between restarts.
pub key_file: String,
}
pub struct TailscaleEmbeddedProvider {
device: Mutex<Option<tailscale::Device>>,
ip: OnceLock<Ipv4Addr>,
hostname: String,
}
impl TailscaleEmbeddedProvider {
pub async fn new(cfg: TailscaleConfig) -> Result<Self> {
let mut ts_cfg = tailscale::Config::default_with_key_file(&cfg.key_file)
.await
.context("loading tailscale key file")?;
ts_cfg.requested_hostname = Some(cfg.hostname.clone());
let auth_key = if cfg.auth_key.is_empty() { None } else { Some(cfg.auth_key) };
let device = tailscale::Device::new(&ts_cfg, auth_key)
.await
.context("creating tailscale device")?;
let ip = device.ipv4_addr().await.context("getting tailscale IPv4")?;
info!(provider = "tailscale", %ip, hostname = %cfg.hostname, "mesh device ready");
Ok(Self {
device: Mutex::new(Some(device)),
ip: OnceLock::from(ip),
hostname: cfg.hostname,
})
}
/// Create an Axum-compatible listener bound to the mesh IP on `port`.
/// The caller is responsible for spawning an `axum::serve` task with the result.
pub async fn axum_listener(&self, port: u16) -> Result<tailscale::axum::Listener> {
let ip = self.ip.get().copied().ok_or_else(|| anyhow!("device not ready"))?;
let addr = SocketAddr::from((ip, port));
let guard = self.device.lock().await;
let device = guard.as_ref().ok_or_else(|| anyhow!("device shut down"))?;
let net_listener = device
.tcp_listen(addr)
.await
.with_context(|| format!("tcp_listen on {addr}"))?;
Ok(tailscale::axum::Listener::from(net_listener))
}
}
#[async_trait]
impl RemoteAccess for TailscaleEmbeddedProvider {
fn provider_name(&self) -> &str { "tailscale" }
async fn device_ip(&self) -> Result<Ipv4Addr> {
self.ip.get().copied().ok_or_else(|| anyhow!("device not ready"))
}
fn is_connected(&self) -> bool { self.ip.get().is_some() }
async fn shutdown(&self) {
let device = self.device.lock().await.take();
if let Some(dev) = device {
let clean = dev.shutdown(Some(std::time::Duration::from_secs(5))).await;
if clean {
info!(provider = "tailscale", hostname = %self.hostname, "mesh device shut down cleanly");
} else {
warn!(provider = "tailscale", hostname = %self.hostname, "mesh device shutdown timed out");
}
}
}
}