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