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#[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 #[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#[pyclass(frozen, module = "tailscale")]
98pub struct Device {
99 dev: Arc<ts::Device>,
100}
101
102#[pymethods]
103impl Device {
104 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}