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