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=None, /, auth_key=None, *, control_server_url=None, hostname=None, tags=None, keys=None))]
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    pub fn udp_bind<'p>(&self, py: Python<'p>, addr: (IpRepr, u16)) -> PyFut<'p> {
108        let dev = self.dev.clone();
109        let ip: Result<IpAddr, _> = addr.0.try_into();
110
111        future_into_py(py, async move {
112            let ip = ip?;
113
114            let sock = dev
115                .udp_bind((ip, addr.1).into())
116                .await
117                .map_err(py_value_err)?;
118
119            Ok(udp::UdpSocket {
120                sock: Arc::new(sock),
121            })
122        })
123    }
124
125    /// Bind a new TCP listen socket on the given `addr` and `port`.
126    ///
127    /// `addr` must be given as (host, port). Presently, `host` must be an IP.
128    pub fn tcp_listen<'p>(&self, py: Python<'p>, addr: (IpRepr, u16)) -> PyFut<'p> {
129        let dev = self.dev.clone();
130        let ip: Result<IpAddr, _> = addr.0.try_into();
131
132        future_into_py(py, async move {
133            let ip = ip?;
134
135            let listener = dev
136                .tcp_listen((ip, addr.1).into())
137                .await
138                .map_err(py_value_err)?;
139
140            Ok(tcp::TcpListener {
141                listener: Arc::new(listener),
142            })
143        })
144    }
145
146    /// Create a new TCP connection to the given `addr`.
147    ///
148    /// `addr` must be given as (host, port). Presently, `host` must be an IP.
149    pub fn tcp_connect<'p>(&self, py: Python<'p>, addr: (IpRepr, u16)) -> PyFut<'p> {
150        let dev = self.dev.clone();
151        let ip: Result<IpAddr, _> = addr.0.try_into();
152
153        future_into_py(py, async move {
154            let ip = ip?;
155
156            let sock = dev
157                .tcp_connect((ip, addr.1).into())
158                .await
159                .map_err(|e| PyValueError::new_err(e.to_string()))?;
160
161            Ok(tcp::TcpStream {
162                sock: Arc::new(sock),
163            })
164        })
165    }
166
167    /// Get the device's IPv4 tailnet address.
168    pub fn ipv4_addr<'p>(&self, py: Python<'p>) -> PyFut<'p> {
169        let dev = self.dev.clone();
170
171        future_into_py(py, async move {
172            let ip = dev.ipv4_addr().await.map_err(py_value_err)?;
173            Ok(ip)
174        })
175    }
176
177    /// Get the device's IPv6 tailnet address.
178    pub fn ipv6_addr<'p>(&self, py: Python<'p>) -> PyFut<'p> {
179        let dev = self.dev.clone();
180
181        future_into_py(py, async move {
182            let ip = dev.ipv6_addr().await.map_err(py_value_err)?;
183            Ok(ip)
184        })
185    }
186
187    /// Look up info about a peer by its name.
188    ///
189    /// `name` may be an unqualified hostname or a fully-qualified name.
190    pub fn peer_by_name<'p>(&self, py: Python<'p>, name: String) -> PyFut<'p> {
191        let dev = self.dev.clone();
192
193        future_into_py(py, async move {
194            let node = dev.peer_by_name(&name).await.map_err(py_value_err)?;
195
196            Ok(node.map(|node| NodeInfo::from(&node)))
197        })
198    }
199
200    /// Get this device's node info.
201    pub fn self_node<'p>(&self, py: Python<'p>) -> PyFut<'p> {
202        let dev = self.dev.clone();
203
204        future_into_py(py, async move {
205            let node = dev.self_node().await.map_err(py_value_err)?;
206            Ok(NodeInfo::from(&node))
207        })
208    }
209
210    /// Look up a peer by its tailnet IP address.
211    pub fn peer_by_tailnet_ip<'p>(&self, py: Python<'p>, ip: IpRepr) -> PyFut<'p> {
212        let dev = self.dev.clone();
213
214        future_into_py(py, async move {
215            let ip = ip.try_into().map_err(py_value_err)?;
216            let node = dev.peer_by_tailnet_ip(ip).await.map_err(py_value_err)?;
217
218            Ok(node.map(|node| NodeInfo::from(&node)))
219        })
220    }
221
222    /// Look up peer(s) with the most specific route match for the given address.
223    ///
224    /// If more than one peer has the same route covering the same address, more than one
225    /// result may be returned.
226    pub fn peers_with_route<'p>(&self, py: Python<'p>, ip: IpRepr) -> PyFut<'p> {
227        let dev = self.dev.clone();
228
229        future_into_py(py, async move {
230            let ip = ip.try_into().map_err(py_value_err)?;
231            let nodes = dev.peers_with_route(ip).await.map_err(py_value_err)?;
232
233            Ok(nodes
234                .into_iter()
235                .map(|node| NodeInfo::from(&node))
236                .collect::<Vec<_>>())
237        })
238    }
239}
240
241fn sockaddr_as_tuple(s: SocketAddr) -> (IpAddr, u16) {
242    (s.ip(), s.port())
243}
244
245fn py_value_err(e: impl ToString) -> PyErr {
246    PyValueError::new_err(e.to_string())
247}