Skip to main content

tailscale_localapi/
lib.rs

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