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