use crate::join::Peer;
use crate::seed::{TailscaleSeed, TunnelKind};
use crate::tunnel::{Identity, Tunnel, TunnelError, TunnelStatus};
pub const MISSING: &str = "a pure-Rust Tailscale coordination client — the ts2021 Noise-IK control protocol (machine key, RegisterRequest with the auth key), the MapRequest long-poll that yields the netmap, and a DERP client for relayed paths; none exists in the suite or the local crates mirror (2026-09-24), and tailscaled cannot run on an OS-less box";
pub struct Tailscale {
seed: TailscaleSeed,
authkey: Option<String>,
}
impl Tailscale {
pub fn new(seed: TailscaleSeed, authkey: Option<String>) -> Tailscale {
Tailscale { seed, authkey: authkey.filter(|k| !k.trim().is_empty()) }
}
pub fn control_url(&self) -> &str {
&self.seed.control_url
}
pub fn has_authkey(&self) -> bool {
self.authkey.is_some()
}
}
impl std::fmt::Debug for Tailscale {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tailscale").field("control_url", &self.seed.control_url).field("authkey", &self.authkey.as_ref().map(|_| "[fed]")).finish()
}
}
impl Tunnel for Tailscale {
fn kind(&self) -> TunnelKind {
TunnelKind::Tailscale
}
fn up(&mut self, _me: &Identity, _peers: &[Peer]) -> Result<(), TunnelError> {
if self.authkey.is_none() {
return Err(TunnelError::NotConfigured {
kind: TunnelKind::Tailscale,
what: format!("no auth key: the door feeds it as `{}` (never on the medium) — and even with one: missing {MISSING}", crate::seed::FEED_TAILSCALE_AUTHKEY),
});
}
Err(TunnelError::Unimplemented { kind: TunnelKind::Tailscale, missing: MISSING })
}
fn add_peer(&mut self, _peer: &Peer) -> Result<(), TunnelError> {
Err(TunnelError::Unimplemented { kind: TunnelKind::Tailscale, missing: MISSING })
}
fn status(&self) -> TunnelStatus {
TunnelStatus {
kind: TunnelKind::Tailscale,
up: false,
reachable: Vec::new(),
detail: format!("tailscale via {} · auth key {} · not up: no coordination client", self.seed.control_url, if self.authkey.is_some() { "fed" } else { "absent" }),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use std::net::Ipv4Addr;
fn me() -> Identity {
Identity { overlay: crate::OVERLAY_BOX_A, pubkey: String::new() }
}
fn front() -> Peer {
Peer { name: "front".into(), role: crate::Role::Front, tunnel_pubkey: String::new(), overlay: crate::OVERLAY_FRONT, endpoint: None, utility: None }
}
#[test]
fn with_an_auth_key_up_is_refused_as_unimplemented_naming_the_missing_client_and_status_is_never_up() {
let mut t = Tailscale::new(TailscaleSeed { control_url: "https://hs.example.net".into() }, Some("tskey-auth-SECRET".into()));
let e = t.up(&me(), &[front()]).unwrap_err();
assert_eq!(e, TunnelError::Unimplemented { kind: TunnelKind::Tailscale, missing: MISSING });
let text = e.to_string();
assert!(text.starts_with("REFUSED [tunnel-tailscale-unimplemented]") && text.contains("ts2021") && text.contains("DERP"), "{text}");
assert!(t.add_peer(&front()).is_err());
let s = t.status();
assert!(!s.up && s.reachable.is_empty(), "{s:?}");
assert!(s.detail.contains("hs.example.net") && s.detail.contains("fed"), "{}", s.detail);
assert!(!format!("{t:?}{}", s.detail).contains("SECRET"), "the auth key is never printed");
assert_eq!(t.public_key(), "", "the node key is the coordination server's business");
}
#[test]
fn without_an_auth_key_up_names_the_feed_key_first() {
let mut t = Tailscale::new(TailscaleSeed::default(), Some(" ".into()));
let e = t.up(&me(), &[]).unwrap_err().to_string();
assert!(e.starts_with("REFUSED [tunnel-tailscale-unconfigured]") && e.contains("tailscale_authkey") && e.contains("ts2021"), "{e}");
assert_eq!(t.control_url(), "https://controlplane.tailscale.com");
}
#[test]
fn driver_for_builds_it_from_the_seed_and_the_feed() {
let (mut seed, _) = crate::seed::tests::a_seed(crate::seed::Kind::Gunnar);
seed.tunnel = TunnelKind::Tailscale;
seed.tailscale = Some(TailscaleSeed { control_url: "https://hs.example.net".into() });
let text = seed.render().unwrap();
assert!(text.contains("[tailscale]") && !text.contains("tskey"), "the seed carries the URL, never the key:\n{text}");
let back = crate::seed::Seed::parse(&text).unwrap();
let mut feed = BTreeMap::new();
feed.insert(crate::seed::FEED_TAILSCALE_AUTHKEY.to_string(), "tskey-auth-x".to_string());
let mut d = crate::tunnel::driver_for(&back, &feed).unwrap();
assert_eq!(d.kind(), TunnelKind::Tailscale);
let e = d.up(&Identity { overlay: Ipv4Addr::new(10, 44, 0, 2), pubkey: String::new() }, &[]).unwrap_err().to_string();
assert!(e.starts_with("REFUSED [tunnel-tailscale-unimplemented]"), "{e}");
}
}