tailscale_localapi/
lib.rs

1use std::{
2    io,
3    net::{Ipv4Addr, SocketAddr},
4    path::{Path, PathBuf},
5};
6
7use async_trait::async_trait;
8use base64::Engine;
9use http::{
10    header::{AUTHORIZATION, HOST},
11    Request, Response, Uri,
12};
13use hyper::{body::Buf, Body};
14use tokio::net::{TcpSocket, UnixStream};
15pub use types::*;
16
17/// Definitions of types used in the tailscale API
18pub mod types;
19
20/// Error type for this crate
21#[derive(thiserror::Error, Debug)]
22pub enum Error {
23    #[error("connection failed")]
24    IoError(#[from] io::Error),
25    #[error("request failed")]
26    HyperError(#[from] hyper::Error),
27    #[error("http error")]
28    HttpError(#[from] hyper::http::Error),
29    #[error("unprocessible entity")]
30    UnprocessableEntity,
31    #[error("unable to parse json")]
32    ParsingError(#[from] serde_json::Error),
33    #[error("unable to parse certificate or key")]
34    UnknownCertificateOrKey,
35}
36
37/// Result type for this crate
38pub type Result<T> = std::result::Result<T, Error>;
39
40/// Abstract trait for the tailscale API client
41#[async_trait]
42pub trait LocalApiClient: Clone {
43    async fn get(&self, uri: Uri) -> Result<Response<Body>>;
44}
45
46/// Client for the local tailscaled socket
47#[derive(Clone)]
48pub struct LocalApi<T: LocalApiClient> {
49    /// Path to the tailscaled socket
50    client: T,
51}
52
53impl LocalApi<UnixStreamClient> {
54    /// Create a new client for the local tailscaled from the path to the
55    /// socket.
56    pub fn new_with_socket_path<P: AsRef<Path>>(socket_path: P) -> Self {
57        let socket_path = socket_path.as_ref().to_path_buf();
58        let client = UnixStreamClient { socket_path };
59        Self { client }
60    }
61}
62
63impl LocalApi<TcpWithPasswordClient> {
64    /// Create a new client for the local tailscaled from the TCP port and
65    /// password.
66    pub fn new_with_port_and_password<S: Into<String>>(port: u16, password: S) -> Self {
67        let password = password.into();
68        let client = TcpWithPasswordClient { port, password };
69        Self { client }
70    }
71}
72
73impl<T: LocalApiClient> LocalApi<T> {
74    /// Get the certificate and key for a domain. The domain should be one of
75    /// the valid domains for the local node.
76    pub async fn certificate_pair(&self, domain: &str) -> Result<(PrivateKey, Vec<Certificate>)> {
77        let response = self
78            .client
79            .get(
80                format!("/localapi/v0/cert/{domain}?type=pair")
81                    .parse()
82                    .unwrap(),
83            )
84            .await?;
85
86        let body = hyper::body::aggregate(response.into_body()).await?;
87        let items = rustls_pemfile::read_all(&mut body.reader())?;
88        let (certificates, mut private_keys) = items
89            .into_iter()
90            .map(|item| match item {
91                rustls_pemfile::Item::ECKey(data)
92                | rustls_pemfile::Item::PKCS8Key(data)
93                | rustls_pemfile::Item::RSAKey(data) => Ok((false, data)),
94                rustls_pemfile::Item::X509Certificate(data) => Ok((true, data)),
95                _ => Err(Error::UnknownCertificateOrKey),
96            })
97            .collect::<Result<Vec<_>>>()?
98            .into_iter()
99            .partition::<Vec<(bool, Vec<u8>)>, _>(|&(cert, _)| cert);
100
101        let certificates = certificates
102            .into_iter()
103            .map(|(_, data)| Certificate(data))
104            .collect();
105        let (_, private_key_data) = private_keys.pop().ok_or(Error::UnknownCertificateOrKey)?;
106        let private_key = PrivateKey(private_key_data);
107
108        Ok((private_key, certificates))
109    }
110
111    /// Get the status of the local node.
112    pub async fn status(&self) -> Result<Status> {
113        let response = self
114            .client
115            .get(Uri::from_static("/localapi/v0/status"))
116            .await?;
117        let body = hyper::body::aggregate(response.into_body()).await?;
118        let status = serde_json::de::from_reader(body.reader())?;
119
120        Ok(status)
121    }
122
123    /// Request whois information for an address in the tailnet.
124    pub async fn whois(&self, address: SocketAddr) -> Result<Whois> {
125        let response = self
126            .client
127            .get(
128                format!("/localapi/v0/whois?addr={address}")
129                    .parse()
130                    .unwrap(),
131            )
132            .await?;
133        let body = hyper::body::aggregate(response.into_body()).await?;
134        let whois = serde_json::de::from_reader(body.reader())?;
135
136        Ok(whois)
137    }
138}
139
140/// Client that connects to the local tailscaled over a unix socket. This is
141/// used on Linux and other Unix-like systems.
142#[derive(Clone)]
143pub struct UnixStreamClient {
144    socket_path: PathBuf,
145}
146
147#[async_trait]
148impl LocalApiClient for UnixStreamClient {
149    async fn get(&self, uri: Uri) -> Result<Response<Body>> {
150        let request = Request::builder()
151            .method("GET")
152            .header(HOST, "local-tailscaled.sock")
153            .uri(uri)
154            .body(Body::empty())?;
155
156        let response = self.request(request).await?;
157        Ok(response)
158    }
159}
160
161impl UnixStreamClient {
162    async fn request(&self, request: Request<Body>) -> Result<Response<Body>> {
163        let stream = UnixStream::connect(&self.socket_path).await?;
164        let (mut request_sender, connection) = hyper::client::conn::handshake(stream).await?;
165
166        tokio::spawn(async move {
167            if let Err(e) = connection.await {
168                eprintln!("Error in connection: {}", e);
169            }
170        });
171
172        let response = request_sender.send_request(request).await?;
173        if response.status() == 200 {
174            Ok(response)
175        } else {
176            Err(Error::UnprocessableEntity)
177        }
178    }
179}
180
181/// Client that connects to the local tailscaled over TCP with a password. This
182/// is used on Windows and macOS when sandboxing is enabled.
183#[derive(Clone)]
184pub struct TcpWithPasswordClient {
185    port: u16,
186    password: String,
187}
188
189#[async_trait]
190impl LocalApiClient for TcpWithPasswordClient {
191    async fn get(&self, uri: Uri) -> Result<Response<Body>> {
192        let request = Request::builder()
193            .method("GET")
194            .header(HOST, "local-tailscaled.sock")
195            .header(
196                AUTHORIZATION,
197                format!(
198                    "Basic {}",
199                    base64::engine::general_purpose::STANDARD_NO_PAD
200                        .encode(format!(":{}", self.password))
201                ),
202            )
203            .header("Sec-Tailscale", "localapi")
204            .uri(uri)
205            .body(Body::empty())?;
206
207        let response = self.request(request).await?;
208        Ok(response)
209    }
210}
211
212impl TcpWithPasswordClient {
213    async fn request(&self, request: Request<Body>) -> Result<Response<Body>> {
214        let stream = TcpSocket::new_v4()?
215            .connect((Ipv4Addr::LOCALHOST, self.port).into())
216            .await?;
217        let (mut request_sender, connection) = hyper::client::conn::handshake(stream).await?;
218
219        tokio::spawn(async move {
220            if let Err(e) = connection.await {
221                eprintln!("Error in connection: {}", e);
222            }
223        });
224
225        let response = request_sender.send_request(request).await?;
226        if response.status() == 200 {
227            Ok(response)
228        } else {
229            Err(Error::UnprocessableEntity)
230        }
231    }
232}