Skip to main content

tailscale/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    net::{IpAddr, SocketAddr},
5    sync::{Arc, Once},
6};
7
8use pyo3::{exceptions::PyValueError, prelude::*};
9use pyo3_async_runtimes::tokio::future_into_py;
10use tracing_subscriber::filter::LevelFilter;
11
12use crate::ip_or_str::IpRepr;
13
14extern crate tailscale as ts;
15
16type PyFut<'p> = PyResult<Bound<'p, PyAny>>;
17
18mod ip_or_str;
19mod key_state;
20mod node_info;
21mod tcp;
22mod udp;
23
24use key_state::Keystate;
25use node_info::NodeInfo;
26
27/// Tailscale API.
28#[pymodule]
29pub mod _internal {
30    use super::*;
31    #[pymodule_export]
32    use crate::{
33        Device, Keystate,
34        tcp::{TcpListener, TcpStream},
35        udp::UdpSocket,
36    };
37
38    /// Connect to tailscale using the specified parameters.
39    #[pyfunction]
40    #[pyo3(signature = (key_file_path: "str | None" = None , /, auth_key: "str | None" = None, *, control_server_url: "str | None" = None, hostname: "str | None" = None, tags: "list[str] | None" = None, keys: "Keystate | None" = None, ephemeral: "bool" = false) -> "Awaitable[Device]")]
41    pub fn connect(
42        py: Python<'_>,
43        key_file_path: Option<String>,
44        auth_key: Option<String>,
45        control_server_url: Option<String>,
46        hostname: Option<String>,
47        tags: Option<Vec<String>>,
48        keys: Option<Keystate>,
49        ephemeral: bool,
50    ) -> PyFut<'_> {
51        static TRACING_ONCE: Once = Once::new();
52        TRACING_ONCE.call_once(|| {
53            tracing_subscriber::fmt()
54                .with_env_filter(
55                    tracing_subscriber::EnvFilter::builder()
56                        .with_default_directive(LevelFilter::INFO.into())
57                        .from_env_lossy(),
58                )
59                .init();
60        });
61
62        future_into_py(py, async move {
63            let mut config = if let Some(key_file_path) = key_file_path {
64                ts::Config::default_with_key_file(key_file_path)
65                    .await
66                    .map_err(py_value_err)?
67            } else {
68                ts::Config::default()
69            };
70
71            config.client_name = Some("ts_python".to_owned());
72            if let Some(control_server_url) = control_server_url {
73                config.control_server_url = control_server_url.parse().map_err(py_value_err)?;
74            }
75
76            if let Some(hostname) = hostname {
77                config.requested_hostname = Some(hostname);
78            }
79
80            if let Some(tags) = tags {
81                config.requested_tags = tags;
82            }
83
84            if let Some(keys) = &keys {
85                config.key_state = keys.try_into().map_err(|_| py_value_err("invalid keys"))?;
86            }
87
88            config.ephemeral = ephemeral;
89
90            let dev = ts::Device::new(&config, auth_key)
91                .await
92                .map_err(py_value_err)?;
93
94            Ok(Device { dev: Arc::new(dev) })
95        })
96    }
97}
98
99/// Tailscale client.
100#[pyclass(frozen, module = "tailscale")]
101pub struct Device {
102    dev: Arc<ts::Device>,
103}
104
105#[pymethods]
106impl Device {
107    /// Bind a new UDP socket on the given `addr`.
108    ///
109    /// `addr` must be given as (host, port). Presently, `host` must be an IP.
110    #[pyo3(signature = (addr: "tuple[IPv4Address | IPv6Address | str, int]") -> "Awaitable[UdpSocket]")]
111    pub fn udp_bind<'p>(&self, py: Python<'p>, addr: (IpRepr, u16)) -> PyFut<'p> {
112        let dev = self.dev.clone();
113        let ip: Result<IpAddr, _> = addr.0.try_into();
114
115        future_into_py(py, async move {
116            let ip = ip?;
117
118            let sock = dev
119                .udp_bind((ip, addr.1).into())
120                .await
121                .map_err(py_value_err)?;
122
123            Ok(udp::UdpSocket {
124                sock: Arc::new(sock),
125            })
126        })
127    }
128
129    /// Bind a new TCP listen socket on the given `addr` and `port`.
130    ///
131    /// `addr` must be given as (host, port). Presently, `host` must be an IP.
132    #[pyo3(signature = (addr: "tuple[IPv4Address | IPv6Address | str, int]") -> "Awaitable[TcpListener]")]
133    pub fn tcp_listen<'p>(&self, py: Python<'p>, addr: (IpRepr, u16)) -> PyFut<'p> {
134        let dev = self.dev.clone();
135        let ip: Result<IpAddr, _> = addr.0.try_into();
136
137        future_into_py(py, async move {
138            let ip = ip?;
139
140            let listener = dev
141                .tcp_listen((ip, addr.1).into())
142                .await
143                .map_err(py_value_err)?;
144
145            Ok(tcp::TcpListener {
146                listener: Arc::new(listener),
147            })
148        })
149    }
150
151    /// Create a new TCP connection to the given `addr`.
152    ///
153    /// `addr` must be given as (host, port). Presently, `host` must be an IP.
154    #[pyo3(signature = (addr: "tuple[IPv4Address | IPv6Address | str, int]") -> "Awaitable[TcpStream]")]
155    pub fn tcp_connect<'p>(&self, py: Python<'p>, addr: (IpRepr, u16)) -> PyFut<'p> {
156        let dev = self.dev.clone();
157        let ip: Result<IpAddr, _> = addr.0.try_into();
158
159        future_into_py(py, async move {
160            let ip = ip?;
161
162            let sock = dev
163                .tcp_connect((ip, addr.1).into())
164                .await
165                .map_err(|e| PyValueError::new_err(e.to_string()))?;
166
167            Ok(tcp::TcpStream {
168                sock: Arc::new(sock),
169            })
170        })
171    }
172
173    /// Get the device's IPv4 tailnet address.
174    #[pyo3(signature = () -> "Awaitable[IPv4Address]")]
175    pub fn ipv4_addr<'p>(&self, py: Python<'p>) -> PyFut<'p> {
176        let dev = self.dev.clone();
177
178        future_into_py(py, async move {
179            let ip = dev.ipv4_addr().await.map_err(py_value_err)?;
180            Ok(ip)
181        })
182    }
183
184    /// Get the device's IPv6 tailnet address.
185    #[pyo3(signature = () -> "Awaitable[IPv6Address]")]
186    pub fn ipv6_addr<'p>(&self, py: Python<'p>) -> PyFut<'p> {
187        let dev = self.dev.clone();
188
189        future_into_py(py, async move {
190            let ip = dev.ipv6_addr().await.map_err(py_value_err)?;
191            Ok(ip)
192        })
193    }
194
195    /// Look up info about a peer by its name.
196    ///
197    /// `name` may be an unqualified hostname or a fully-qualified name.
198    #[pyo3(signature = (name: "str") -> "Awaitable[dict[str, Any]]")]
199    pub fn peer_by_name<'p>(&self, py: Python<'p>, name: String) -> PyFut<'p> {
200        let dev = self.dev.clone();
201
202        future_into_py(py, async move {
203            let node = dev.peer_by_name(&name).await.map_err(py_value_err)?;
204
205            Ok(node.map(|node| NodeInfo::from(&node)))
206        })
207    }
208
209    /// Get this device's node info.
210    #[pyo3(signature = () -> "Awaitable[dict[str, Any]]")]
211    pub fn self_node<'p>(&self, py: Python<'p>) -> PyFut<'p> {
212        let dev = self.dev.clone();
213
214        future_into_py(py, async move {
215            let node = dev.self_node().await.map_err(py_value_err)?;
216            Ok(NodeInfo::from(&node))
217        })
218    }
219
220    /// Look up a peer by its tailnet IP address.
221    #[pyo3(signature = (ip: "IPv4Address | IPv6Address | str") -> "Awaitable[dict[str, Any]]")]
222    pub fn peer_by_tailnet_ip<'p>(&self, py: Python<'p>, ip: IpRepr) -> PyFut<'p> {
223        let dev = self.dev.clone();
224
225        future_into_py(py, async move {
226            let ip = ip.try_into().map_err(py_value_err)?;
227            let node = dev.peer_by_tailnet_ip(ip).await.map_err(py_value_err)?;
228
229            Ok(node.map(|node| NodeInfo::from(&node)))
230        })
231    }
232
233    /// Look up peer(s) with the most specific route match for the given address.
234    ///
235    /// If more than one peer has the same route covering the same address, more than one
236    /// result may be returned.
237    #[pyo3(signature = (ip: "IPv4Address | IPv6Address | str") -> "Awaitable[list[dict[str, Any]]]")]
238    pub fn peers_with_route<'p>(&self, py: Python<'p>, ip: IpRepr) -> PyFut<'p> {
239        let dev = self.dev.clone();
240
241        future_into_py(py, async move {
242            let ip = ip.try_into().map_err(py_value_err)?;
243            let nodes = dev.peers_with_route(ip).await.map_err(py_value_err)?;
244
245            Ok(nodes
246                .into_iter()
247                .map(|node| NodeInfo::from(&node))
248                .collect::<Vec<_>>())
249        })
250    }
251}
252
253fn sockaddr_as_tuple(s: SocketAddr) -> (IpAddr, u16) {
254    (s.ip(), s.port())
255}
256
257fn py_value_err(e: impl ToString) -> PyErr {
258    PyValueError::new_err(e.to_string())
259}