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;
17use tokio::net::{TcpSocket, UnixStream};
18pub use types::*;
19
20pub mod types;
22
23#[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
40pub type Result<T> = std::result::Result<T, Error>;
42
43pub trait LocalApiClient: Clone {
45 fn get(&self, uri: Uri) -> impl Future<Output = Result<Response<Incoming>>> + Send;
46}
47
48#[derive(Clone)]
50pub struct LocalApi<T: LocalApiClient> {
51 client: T,
53}
54
55impl LocalApi<UnixStreamClient> {
56 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 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 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 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 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#[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#[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}