1use std::collections::HashMap;
22use std::io::{Read, Write};
23use std::net::{IpAddr, SocketAddr};
24use std::sync::atomic::{AtomicBool, Ordering};
25use std::sync::{Arc, Mutex};
26use std::time::{Duration, Instant};
27
28use serde_json::Value;
29
30use super::http::{Peer, Request};
31use crate::error::{Error, Result};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Whois {
36 pub login: String,
39 pub node: String,
41 pub tags: Vec<String>,
42}
43
44pub type WhoisFetcher = Arc<dyn Fn(SocketAddr) -> std::result::Result<Whois, String> + Send + Sync>;
46
47pub const LOCALAPI_SOCKETS: &[&str] = &[
49 "/var/run/tailscale/tailscaled.sock",
50 "/run/tailscale/tailscaled.sock",
51];
52
53const CLIS: &[&str] = &[
55 "tailscale",
56 "/Applications/Tailscale.app/Contents/MacOS/Tailscale",
57];
58
59const TTL: Duration = Duration::from_secs(60);
60const FAIL_TTL: Duration = Duration::from_secs(5);
61const MAX_CACHED: usize = 4096;
62
63pub fn is_tailnet_ip(ip: IpAddr) -> bool {
65 match ip {
66 IpAddr::V4(v) => {
67 let o = v.octets();
68 o[0] == 100 && (o[1] & 0xc0) == 64
69 }
70 IpAddr::V6(v) => match v.to_ipv4_mapped() {
71 Some(v4) => is_tailnet_ip(IpAddr::V4(v4)),
72 None => {
73 let s = v.segments();
74 s[0] == 0xfd7a && s[1] == 0x115c && s[2] == 0xa1e0
75 }
76 },
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Default)]
82pub struct AllowList {
83 pub logins: Vec<String>,
84 pub tags: Vec<String>,
85}
86
87impl AllowList {
88 pub fn parse(list: &str) -> Result<AllowList> {
90 let mut a = AllowList::default();
91 for e in list.split(',').map(str::trim).filter(|e| !e.is_empty()) {
92 if let Some(t) = e.strip_prefix("tag:") {
93 if t.is_empty()
94 || !t
95 .bytes()
96 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_'))
97 {
98 return Err(Error::invalid(format!(
99 "--superadmin-tailnet: bad tag {e:?}"
100 )));
101 }
102 a.tags.push(e.to_ascii_lowercase());
103 } else if e.contains('@') && !e.contains(char::is_whitespace) {
104 a.logins.push(e.to_ascii_lowercase());
105 } else {
106 return Err(Error::invalid(format!(
107 "--superadmin-tailnet: {e:?} is neither a login name (someone@example.com) nor a tag (tag:name)"
108 )));
109 }
110 }
111 if a.logins.is_empty() && a.tags.is_empty() {
112 return Err(Error::invalid(
113 "--superadmin-tailnet needs at least one login name or tag",
114 ));
115 }
116 Ok(a)
117 }
118
119 pub fn admits(&self, w: &Whois) -> bool {
121 if w.tags.is_empty() {
122 self.logins.iter().any(|l| l.eq_ignore_ascii_case(&w.login))
123 } else {
124 w.tags
125 .iter()
126 .any(|t| self.tags.iter().any(|a| a.eq_ignore_ascii_case(t)))
127 }
128 }
129
130 pub fn entries(&self) -> Vec<String> {
132 self.logins.iter().chain(&self.tags).cloned().collect()
133 }
134}
135
136pub struct Tailnet {
138 allow: AllowList,
139 fetch: WhoisFetcher,
140 hosts: Vec<String>,
142 cache: Mutex<HashMap<IpAddr, (Instant, Option<Whois>)>>,
143 warned: AtomicBool,
144}
145
146impl std::fmt::Debug for Tailnet {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 f.debug_struct("Tailnet")
149 .field("allow", &self.allow)
150 .field("hosts", &self.hosts)
151 .finish()
152 }
153}
154
155impl Tailnet {
156 pub fn new(allow: AllowList, fetch: WhoisFetcher, hosts: Vec<String>) -> Tailnet {
157 let mut hosts: Vec<String> = hosts
158 .into_iter()
159 .map(|h| host_only(&h))
160 .filter(|h| !h.is_empty())
161 .collect();
162 hosts.sort();
163 hosts.dedup();
164 Tailnet {
165 allow,
166 fetch,
167 hosts,
168 cache: Mutex::new(HashMap::new()),
169 warned: AtomicBool::new(false),
170 }
171 }
172
173 pub fn allow(&self) -> &AllowList {
174 &self.allow
175 }
176
177 pub fn hosts(&self) -> &[String] {
178 &self.hosts
179 }
180
181 pub fn identify(&self, req: &Request) -> Option<Whois> {
186 let Peer::Tcp(peer) = &req.peer else {
187 return None;
188 };
189 if !is_tailnet_ip(peer.ip()) {
190 return None;
191 }
192 let host = req.header("host").map(host_only).unwrap_or_default();
193 if !self.hosts.contains(&host) {
194 eprintln!(
195 "isb serve: tailnet peer {peer} sent Host {host:?}, not one of this server's names; not identified"
196 );
197 return None;
198 }
199 self.whois(*peer)
200 }
201
202 pub fn superadmin(&self, req: &Request) -> Option<Whois> {
205 let w = self.identify(req)?;
206 if self.allow.admits(&w) { Some(w) } else { None }
207 }
208
209 fn whois(&self, peer: SocketAddr) -> Option<Whois> {
210 let now = Instant::now();
211 if let Some((at, w)) = self.lock().get(&peer.ip()) {
212 let ttl = if w.is_some() { TTL } else { FAIL_TTL };
213 if now.duration_since(*at) < ttl {
214 return w.clone();
215 }
216 }
217 let got = match (self.fetch)(peer) {
218 Ok(w) => Some(w),
219 Err(e) => {
220 if !self.warned.swap(true, Ordering::Relaxed) {
221 eprintln!(
222 "isb serve: tailnet identities are unavailable until tailscaled answers: {e}"
223 );
224 }
225 None
226 }
227 };
228 let mut c = self.lock();
229 if c.len() >= MAX_CACHED {
230 c.clear();
231 }
232 c.insert(peer.ip(), (now, got.clone()));
233 got
234 }
235
236 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<IpAddr, (Instant, Option<Whois>)>> {
237 self.cache.lock().unwrap_or_else(|e| e.into_inner())
238 }
239}
240
241pub fn host_only(h: &str) -> String {
244 let h = h.trim().trim_end_matches('.').to_ascii_lowercase();
245 if h.starts_with('[') {
246 return match h.find(']') {
247 Some(i) => h[..=i].to_string(),
248 None => h,
249 };
250 }
251 if h.matches(':').count() > 1 {
253 return format!("[{h}]");
254 }
255 match h.rsplit_once(':') {
256 Some((host, port)) if port.bytes().all(|b| b.is_ascii_digit()) => {
257 host.trim_end_matches('.').to_string()
258 }
259 _ => h,
260 }
261}
262
263pub fn parse_whois(v: &Value) -> std::result::Result<Whois, String> {
265 let login = v["UserProfile"]["LoginName"]
266 .as_str()
267 .filter(|s| !s.is_empty())
268 .ok_or("whois: no UserProfile.LoginName")?
269 .to_string();
270 let node = v["Node"]["Name"]
271 .as_str()
272 .or_else(|| v["Node"]["ComputedName"].as_str())
273 .unwrap_or("")
274 .trim_end_matches('.')
275 .to_string();
276 let tags = v["Node"]["Tags"]
277 .as_array()
278 .map(|a| {
279 a.iter()
280 .filter_map(Value::as_str)
281 .map(str::to_ascii_lowercase)
282 .collect()
283 })
284 .unwrap_or_default();
285 Ok(Whois { login, node, tags })
286}
287
288fn localapi_get(socket: &str, path: &str) -> std::result::Result<Value, String> {
291 use std::os::unix::net::UnixStream;
292 let mut s = UnixStream::connect(socket).map_err(|e| format!("{socket}: {e}"))?;
293 let t = Some(Duration::from_secs(3));
294 let _ = s.set_read_timeout(t);
295 let _ = s.set_write_timeout(t);
296 write!(
297 s,
298 "GET {path} HTTP/1.0\r\nHost: local-tailscaled.sock\r\n\r\n"
299 )
300 .map_err(|e| format!("{socket}: {e}"))?;
301 let mut buf = Vec::new();
302 s.take(4 << 20)
303 .read_to_end(&mut buf)
304 .map_err(|e| format!("{socket}: {e}"))?;
305 let text = String::from_utf8_lossy(&buf);
306 let (head, body) = text
307 .split_once("\r\n\r\n")
308 .ok_or_else(|| format!("{socket}: a malformed answer"))?;
309 let status = head.split_whitespace().nth(1).unwrap_or("");
310 if status != "200" {
311 return Err(format!(
312 "{socket}: LocalAPI {path} answered {status}: {}",
313 body.trim().chars().take(200).collect::<String>()
314 ));
315 }
316 serde_json::from_str(body).map_err(|e| format!("{socket}: {e}"))
317}
318
319fn cli_json(args: &[&str]) -> std::result::Result<Value, String> {
321 use std::process::{Command, Stdio};
322 let mut last = String::from("no tailscale CLI found");
323 for cli in CLIS {
324 let child = Command::new(cli)
325 .args(args)
326 .stdin(Stdio::null())
327 .stdout(Stdio::piped())
328 .stderr(Stdio::piped())
329 .spawn();
330 let mut child = match child {
331 Ok(c) => c,
332 Err(e) => {
333 last = format!("{cli}: {e}");
334 continue;
335 }
336 };
337 let deadline = Instant::now() + Duration::from_secs(5);
338 let status = loop {
339 match child.try_wait() {
340 Ok(Some(s)) => break s,
341 Ok(None) if Instant::now() < deadline => {
342 std::thread::sleep(Duration::from_millis(20))
343 }
344 _ => {
345 let _ = child.kill();
346 let _ = child.wait();
347 return Err(format!("{cli} {}: timed out", args.join(" ")));
348 }
349 }
350 };
351 let mut out = Vec::new();
352 if let Some(mut o) = child.stdout.take() {
353 let _ = o.read_to_end(&mut out);
354 }
355 if !status.success() {
356 let mut err = String::new();
357 if let Some(mut e) = child.stderr.take() {
358 let _ = e.read_to_string(&mut err);
359 }
360 return Err(format!("{cli} {}: {}", args.join(" "), err.trim()));
361 }
362 return serde_json::from_slice(&out).map_err(|e| format!("{cli}: {e}"));
363 }
364 Err(last)
365}
366
367fn localapi_or_cli(path: &str, cli_args: &[&str]) -> std::result::Result<Value, String> {
368 let mut errs = Vec::new();
369 for s in LOCALAPI_SOCKETS {
370 if !std::path::Path::new(s).exists() {
371 continue;
372 }
373 match localapi_get(s, path) {
374 Ok(v) => return Ok(v),
375 Err(e) => errs.push(e),
376 }
377 }
378 match cli_json(cli_args) {
379 Ok(v) => Ok(v),
380 Err(e) => {
381 errs.push(e);
382 Err(errs.join("; "))
383 }
384 }
385}
386
387pub fn system_fetcher() -> WhoisFetcher {
389 Arc::new(|peer: SocketAddr| {
390 let addr = peer.to_string();
391 let v = localapi_or_cli(
392 &format!("/localapi/v0/whois?addr={}", urlencode(&addr)),
393 &["whois", "--json", &addr],
394 )?;
395 parse_whois(&v)
396 })
397}
398
399pub fn self_names() -> Vec<String> {
402 let Ok(v) = localapi_or_cli(
403 "/localapi/v0/status?peers=false",
404 &["status", "--json", "--peers=false"],
405 ) else {
406 return Vec::new();
407 };
408 let mut out = Vec::new();
409 if let Some(d) = v["Self"]["DNSName"].as_str() {
410 let d = d.trim_end_matches('.').to_ascii_lowercase();
411 if let Some((short, _)) = d.split_once('.') {
412 out.push(short.to_string());
413 }
414 if !d.is_empty() {
415 out.push(d);
416 }
417 }
418 if let Some(h) = v["Self"]["HostName"].as_str() {
419 out.push(h.to_ascii_lowercase());
420 }
421 out
422}
423
424fn urlencode(s: &str) -> String {
425 s.bytes()
426 .map(|b| match b {
427 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'.' | b'-' | b'_' => (b as char).to_string(),
428 _ => format!("%{b:02X}"),
429 })
430 .collect()
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436 use std::sync::atomic::AtomicUsize;
437
438 fn req(peer: &str, host: &str, headers: &[(&str, &str)]) -> Request {
439 let mut h = vec![("Host".to_string(), host.to_string())];
440 h.extend(headers.iter().map(|(k, v)| (k.to_string(), v.to_string())));
441 Request {
442 method: "GET".into(),
443 path: "/api/v1/auth/me".into(),
444 query: None,
445 headers: h,
446 body: Vec::new(),
447 peer: Peer::Tcp(peer.parse().unwrap()),
448 }
449 }
450
451 fn who(login: &str, tags: &[&str]) -> Whois {
452 Whois {
453 login: login.into(),
454 node: "laptop.tail1.ts.net".into(),
455 tags: tags.iter().map(|t| t.to_string()).collect(),
456 }
457 }
458
459 #[test]
460 fn tailnet_ranges() {
461 for ip in [
462 "100.64.0.1",
463 "100.127.255.254",
464 "100.86.22.100",
465 "fd7a:115c:a1e0::1",
466 "::ffff:100.100.1.1",
467 ] {
468 assert!(is_tailnet_ip(ip.parse().unwrap()), "{ip}");
469 }
470 for ip in [
471 "100.63.255.255",
472 "100.128.0.1",
473 "127.0.0.1",
474 "10.0.0.1",
475 "fd7a:115c:a1e1::1",
476 "::1",
477 ] {
478 assert!(!is_tailnet_ip(ip.parse().unwrap()), "{ip}");
479 }
480 }
481
482 #[test]
483 fn allow_lists() {
484 assert!(AllowList::parse("").is_err());
485 assert!(AllowList::parse(" , ").is_err());
486 assert!(AllowList::parse("bob").is_err());
487 assert!(AllowList::parse("tag:").is_err());
488 assert!(AllowList::parse("tag:a b").is_err());
489 let a = AllowList::parse("Someone@Example.com, tag:agents").unwrap();
490 assert_eq!(a.entries(), vec!["someone@example.com", "tag:agents"]);
491 assert!(a.admits(&who("someone@example.com", &[])));
492 assert!(!a.admits(&who("other@example.com", &[])));
493 assert!(a.admits(&who("tagged-devices", &["tag:agents"])));
494 assert!(!a.admits(&who("tagged-devices", &["tag:other"])));
495 let l = AllowList::parse("someone@example.com").unwrap();
497 assert!(!l.admits(&who("someone@example.com", &["tag:x"])));
498 }
499
500 #[test]
501 fn whois_json() {
502 let v = serde_json::json!({
503 "Node": {"Name": "titan.tail9.ts.net.", "Tags": ["tag:Agents"]},
504 "UserProfile": {"LoginName": "tagged-devices"}
505 });
506 let w = parse_whois(&v).unwrap();
507 assert_eq!(w.node, "titan.tail9.ts.net");
508 assert_eq!(w.tags, vec!["tag:agents"]);
509 assert!(parse_whois(&serde_json::json!({})).is_err());
510 }
511
512 #[test]
513 fn hosts() {
514 assert_eq!(host_only("Titan.tail9.ts.net.:18995"), "titan.tail9.ts.net");
515 assert_eq!(host_only("100.86.22.100:18995"), "100.86.22.100");
516 assert_eq!(host_only("[fd7a::1]:80"), "[fd7a::1]");
517 assert_eq!(host_only("fd7a::1"), "[fd7a::1]");
518 }
519
520 #[test]
521 fn gate_checks_peer_host_and_list_and_caches() {
522 let calls = Arc::new(AtomicUsize::new(0));
523 let c = calls.clone();
524 let fetch: WhoisFetcher = Arc::new(move |p: SocketAddr| {
525 c.fetch_add(1, Ordering::SeqCst);
526 match p.ip().to_string().as_str() {
527 "100.64.0.1" => Ok(who("me@example.com", &[])),
528 "100.64.0.2" => Ok(who("other@example.com", &[])),
529 _ => Err("tailscaled is not running".into()),
530 }
531 });
532 let t = Tailnet::new(
533 AllowList::parse("me@example.com").unwrap(),
534 fetch,
535 vec!["100.86.22.100:18995".into(), "titan.tail9.ts.net".into()],
536 );
537 let ok = req("100.64.0.1:5555", "100.86.22.100:18995", &[]);
538 assert_eq!(t.superadmin(&ok).unwrap().login, "me@example.com");
539 assert!(
541 t.superadmin(&req("100.64.0.1:6666", "titan.tail9.ts.net", &[]))
542 .is_some()
543 );
544 assert_eq!(calls.load(Ordering::SeqCst), 1);
545 assert!(
547 t.superadmin(&req("100.64.0.2:1", "100.86.22.100", &[]))
548 .is_none()
549 );
550 assert!(
552 t.superadmin(&req("100.64.0.1:1", "evil.example:18995", &[]))
553 .is_none()
554 );
555 assert!(t.superadmin(&req("100.64.0.1:1", "", &[])).is_none());
556 let spoof = req(
558 "127.0.0.1:1",
559 "100.86.22.100",
560 &[
561 ("Tailscale-User-Login", "me@example.com"),
562 ("X-Forwarded-For", "100.64.0.1"),
563 ],
564 );
565 assert!(t.superadmin(&spoof).is_none());
566 let spoof = req(
567 "10.1.2.3:1",
568 "100.86.22.100",
569 &[
570 ("Tailscale-User-Login", "me@example.com"),
571 ("X-Forwarded-For", "100.64.0.1"),
572 ],
573 );
574 assert!(t.superadmin(&spoof).is_none());
575 assert!(
577 t.superadmin(&req("100.64.0.9:1", "100.86.22.100", &[]))
578 .is_none()
579 );
580 let mut u = ok.clone();
582 u.peer = Peer::Unix { uid: None };
583 assert!(t.superadmin(&u).is_none());
584 }
585}