tailscale_localapi/
lib.rs1use 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
24pub mod types;
26
27#[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
44pub type Result<T> = std::result::Result<T, Error>;
46
47pub trait LocalApiClient: Clone {
49 fn get(&self, uri: Uri) -> impl Future<Output = Result<Response<Incoming>>> + Send;
50}
51
52#[derive(Clone)]
54pub struct LocalApi<T: LocalApiClient> {
55 client: T,
57}
58
59#[cfg(unix)]
60impl LocalApi<UnixStreamClient> {
61 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 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 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 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 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 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#[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#[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#[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}