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 node_info;
20mod tcp;
21mod udp;
22
23use node_info::NodeInfo;
24
25/// Tailscale API.
26#[pymodule]
27pub mod _internal {
28    use super::*;
29    #[pymodule_export]
30    use crate::{
31        Device,
32        tcp::{TcpListener, TcpStream},
33        udp::UdpSocket,
34    };
35
36    /// Connect to tailscale using the specified config file and optional auth key.
37    #[pyfunction]
38    #[pyo3(signature = (config_path, auth_key=None))]
39    pub fn connect(py: Python<'_>, config_path: String, auth_key: Option<String>) -> PyFut<'_> {
40        static TRACING_ONCE: Once = Once::new();
41        TRACING_ONCE.call_once(|| {
42            let env_filter = tracing_subscriber::EnvFilter::builder()
43                .with_default_directive(LevelFilter::INFO.into())
44                .from_env_lossy();
45
46            tracing_subscriber::fmt().with_env_filter(env_filter).init();
47        });
48
49        future_into_py(py, async move {
50            let config = ts::Config {
51                key_state: ts::load_key_file(config_path, Default::default())
52                    .await
53                    .map_err(py_value_err)?,
54                client_name: Some("ts_python".to_owned()),
55                ..Default::default()
56            };
57
58            let dev = ts::Device::new(&config, auth_key)
59                .await
60                .map_err(py_value_err)?;
61
62            Ok(Device { dev: Arc::new(dev) })
63        })
64    }
65}
66
67/// Tailscale client.
68#[pyclass(frozen, module = "tailscale")]
69pub struct Device {
70    dev: Arc<ts::Device>,
71}
72
73#[pymethods]
74impl Device {
75    /// Bind a new UDP socket on the given `addr`.
76    ///
77    /// `addr` must be given as (host, port). Presently, `host` must be an IP.
78    pub fn udp_bind<'p>(&self, py: Python<'p>, addr: (IpRepr, u16)) -> PyFut<'p> {
79        let dev = self.dev.clone();
80        let ip: Result<IpAddr, _> = addr.0.try_into();
81
82        future_into_py(py, async move {
83            let ip = ip?;
84
85            let sock = dev
86                .udp_bind((ip, addr.1).into())
87                .await
88                .map_err(py_value_err)?;
89
90            Ok(udp::UdpSocket {
91                sock: Arc::new(sock),
92            })
93        })
94    }
95
96    /// Bind a new TCP listen socket on the given `addr` and `port`.
97    ///
98    /// `addr` must be given as (host, port). Presently, `host` must be an IP.
99    pub fn tcp_listen<'p>(&self, py: Python<'p>, addr: (IpRepr, u16)) -> PyFut<'p> {
100        let dev = self.dev.clone();
101        let ip: Result<IpAddr, _> = addr.0.try_into();
102
103        future_into_py(py, async move {
104            let ip = ip?;
105
106            let listener = dev
107                .tcp_listen((ip, addr.1).into())
108                .await
109                .map_err(py_value_err)?;
110
111            Ok(tcp::TcpListener {
112                listener: Arc::new(listener),
113            })
114        })
115    }
116
117    /// Create a new TCP connection to the given `addr`.
118    ///
119    /// `addr` must be given as (host, port). Presently, `host` must be an IP.
120    pub fn tcp_connect<'p>(&self, py: Python<'p>, addr: (IpRepr, u16)) -> PyFut<'p> {
121        let dev = self.dev.clone();
122        let ip: Result<IpAddr, _> = addr.0.try_into();
123
124        future_into_py(py, async move {
125            let ip = ip?;
126
127            let sock = dev
128                .tcp_connect((ip, addr.1).into())
129                .await
130                .map_err(|e| PyValueError::new_err(e.to_string()))?;
131
132            Ok(tcp::TcpStream {
133                sock: Arc::new(sock),
134            })
135        })
136    }
137
138    /// Get the device's IPv4 tailnet address.
139    pub fn ipv4_addr<'p>(&self, py: Python<'p>) -> PyFut<'p> {
140        let dev = self.dev.clone();
141
142        future_into_py(py, async move {
143            let ip = dev.ipv4_addr().await.map_err(py_value_err)?;
144            Ok(ip)
145        })
146    }
147
148    /// Get the device's IPv6 tailnet address.
149    pub fn ipv6_addr<'p>(&self, py: Python<'p>) -> PyFut<'p> {
150        let dev = self.dev.clone();
151
152        future_into_py(py, async move {
153            let ip = dev.ipv6_addr().await.map_err(py_value_err)?;
154            Ok(ip)
155        })
156    }
157
158    /// Look up info about a peer by its name.
159    ///
160    /// `name` may be an unqualified hostname or a fully-qualified name.
161    pub fn peer_by_name<'p>(&self, py: Python<'p>, name: String) -> PyFut<'p> {
162        let dev = self.dev.clone();
163
164        future_into_py(py, async move {
165            let node = dev.peer_by_name(&name).await.map_err(py_value_err)?;
166
167            Ok(node.map(|node| NodeInfo::from(&node)))
168        })
169    }
170}
171
172fn sockaddr_as_tuple(s: SocketAddr) -> (IpAddr, u16) {
173    (s.ip(), s.port())
174}
175
176fn py_value_err(e: impl ToString) -> PyErr {
177    PyValueError::new_err(e.to_string())
178}