Skip to main content

ts_elixir/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{
4    collections::HashMap,
5    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
6    str::FromStr,
7    sync::{Arc, LazyLock},
8    time::Duration,
9};
10
11use rustler::{Encoder, NifResult, ResourceArc, Term};
12
13mod config;
14mod device;
15mod serve;
16mod status;
17mod tcp;
18mod udp;
19
20use device::LoopbackHandleResource;
21use tcp::{TcpListener, TcpStream};
22use udp::UdpSocket;
23
24use crate::config::Keystate;
25
26mod atoms {
27    rustler::atoms! {
28        ok,
29        error,
30
31        ip4,
32        ip6,
33    }
34}
35
36struct Device {
37    inner: Arc<tailscale::Device>,
38}
39
40#[derive(rustler::NifStruct)]
41#[module = "Tailscale.NodeInfo"]
42struct NodeInfo<'a> {
43    id: i64,
44    stable_id: String,
45    hostname: String,
46    tailnet: Option<String>,
47    tags: Vec<String>,
48    tailnet_addresses: Vec<Term<'a>>,
49    derp_region: Option<u32>,
50    node_key: String,
51    disco_key: Option<String>,
52    machine_key: Option<String>,
53    underlay_addresses: Vec<Term<'a>>,
54}
55
56impl<'a> NodeInfo<'a> {
57    fn from_node(env: rustler::Env<'a>, value: tailscale::NodeInfo) -> Self {
58        Self {
59            id: value.id,
60            stable_id: value.stable_id.0,
61            hostname: value.hostname,
62            tailnet: value.tailnet,
63            tags: value.tags,
64            tailnet_addresses: vec![
65                ip_to_erl(env, value.tailnet_address.ipv4.addr()),
66                ip_to_erl(env, value.tailnet_address.ipv6.addr()),
67            ],
68            derp_region: value.derp_region.map(|x| x.0.get()),
69            node_key: value.node_key.to_string(),
70            disco_key: value.disco_key.as_ref().map(ToString::to_string),
71            machine_key: value.machine_key.as_ref().map(ToString::to_string),
72            underlay_addresses: value
73                .underlay_addresses
74                .into_iter()
75                .map(|x| (ip_to_erl(env, x.ip()), x.port()).encode(env))
76                .collect(),
77        }
78    }
79}
80
81type Result<T> = core::result::Result<T, Box<dyn core::error::Error + Send + Sync + 'static>>;
82
83#[rustler::resource_impl]
84impl rustler::Resource for Device {}
85
86static TOKIO_RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
87    let rt = tokio::runtime::Builder::new_multi_thread()
88        .enable_all()
89        .build()
90        .unwrap();
91
92    tracing::debug!("started tokio runtime");
93
94    rt
95});
96
97fn erl_result(env: rustler::Env, r: Result<impl Encoder>) -> Term {
98    match r {
99        Ok(t) => (atoms::ok(), t).encode(env),
100        Err(e) => (atoms::error(), e.to_string()).encode(env),
101    }
102}
103
104fn ok_arc<T>(t: T) -> Result<ResourceArc<T>>
105where
106    T: rustler::Resource,
107{
108    Ok(ResourceArc::new(t))
109}
110
111#[rustler::nif(schedule = "DirtyIo")]
112fn connect<'env>(
113    env: rustler::Env<'env>,
114    opts: HashMap<rustler::Atom, Term<'_>>,
115) -> NifResult<(rustler::Atom, Term<'env>)> {
116    let (config, auth_key) = config::config_from_erl(&opts)?;
117
118    let dev = TOKIO_RUNTIME.block_on(async move {
119        let dev = tailscale::Device::new(&config, auth_key).await?;
120
121        ok_arc(Device {
122            inner: Arc::new(dev),
123        })
124    });
125
126    match dev {
127        Ok(dev) => Ok((atoms::ok(), dev.encode(env))),
128        Err(e) => Err(rustler::Error::Term(Box::new(e.to_string()))),
129    }
130}
131
132#[rustler::nif(schedule = "DirtyIo")]
133fn load_key_file(env: rustler::Env, path: &str) -> impl Encoder {
134    let result = TOKIO_RUNTIME
135        .block_on(tailscale::config::load_key_file(path, Default::default()))
136        .map(Keystate::from)
137        .map_err(Into::into);
138
139    erl_result(env, result)
140}
141
142#[rustler::nif(schedule = "DirtyIo")]
143fn ipv4_addr(env: rustler::Env, dev: ResourceArc<Device>) -> impl Encoder {
144    let dev = dev.inner.clone();
145    let addr = TOKIO_RUNTIME.block_on(dev.ipv4_addr());
146
147    erl_result(env, addr.map(|ip| ip_to_erl(env, ip)).map_err(Into::into))
148}
149
150#[rustler::nif(schedule = "DirtyIo")]
151fn ipv6_addr(env: rustler::Env<'_>, dev: ResourceArc<Device>) -> impl Encoder {
152    let dev = dev.inner.clone();
153
154    match TOKIO_RUNTIME.block_on(dev.ipv6_addr()) {
155        Err(e) => (atoms::error(), e.to_string()).encode(env),
156        Ok(ip) => (atoms::ok(), ip_to_erl(env, ip)).encode(env),
157    }
158}
159
160#[rustler::nif(schedule = "DirtyIo")]
161fn peer_by_name(env: rustler::Env<'_>, dev: ResourceArc<Device>, name: &str) -> impl Encoder {
162    let dev = dev.inner.clone();
163    let name = name.to_owned();
164
165    match TOKIO_RUNTIME.block_on(async move { dev.peer_by_name(&name).await }) {
166        Err(e) => (atoms::error(), e.to_string()).encode(env),
167        Ok(None) => (atoms::ok(), Option::<()>::None).encode(env),
168        Ok(Some(peer)) => (atoms::ok(), NodeInfo::from_node(env, peer)).encode(env),
169    }
170}
171
172#[rustler::nif(schedule = "DirtyIo")]
173fn self_node(env: rustler::Env<'_>, dev: ResourceArc<Device>) -> impl Encoder {
174    let dev = dev.inner.clone();
175
176    match TOKIO_RUNTIME.block_on(async move { dev.self_node().await }) {
177        Err(e) => (atoms::error(), e.to_string()).encode(env),
178        Ok(peer) => (atoms::ok(), NodeInfo::from_node(env, peer)).encode(env),
179    }
180}
181
182#[rustler::nif(schedule = "DirtyIo")]
183fn peer_by_tailnet_ip(env: rustler::Env<'_>, dev: ResourceArc<Device>, ip: Term) -> impl Encoder {
184    let dev = dev.inner.clone();
185    let Some(ip) = ip_from_erl(ip) else {
186        return env.error_tuple("invalid ip");
187    };
188
189    match TOKIO_RUNTIME.block_on(async move { dev.peer_by_tailnet_ip(ip).await }) {
190        Err(e) => (atoms::error(), e.to_string()).encode(env),
191        Ok(None) => (atoms::ok(), Option::<()>::None).encode(env),
192        Ok(Some(peer)) => (atoms::ok(), NodeInfo::from_node(env, peer)).encode(env),
193    }
194}
195
196#[rustler::nif(schedule = "DirtyIo")]
197fn peers_with_route(env: rustler::Env<'_>, dev: ResourceArc<Device>, ip: Term) -> impl Encoder {
198    let dev = dev.inner.clone();
199    let Some(ip) = ip_from_erl(ip) else {
200        return env.error_tuple("invalid ip");
201    };
202
203    match TOKIO_RUNTIME.block_on(async move { dev.peers_with_route(ip).await }) {
204        Err(e) => (atoms::error(), e.to_string()).encode(env),
205        Ok(peers) => (
206            atoms::ok(),
207            peers
208                .into_iter()
209                .map(|x| NodeInfo::from_node(env, x))
210                .collect::<Vec<_>>(),
211        )
212            .encode(env),
213    }
214}
215
216#[rustler::nif(schedule = "DirtyIo")]
217fn resolve(env: rustler::Env<'_>, dev: ResourceArc<Device>, name: &str) -> impl Encoder {
218    let dev = dev.inner.clone();
219    let name = name.to_owned();
220
221    match TOKIO_RUNTIME.block_on(async move { dev.resolve(&name).await }) {
222        Err(e) => (atoms::error(), e.to_string()).encode(env),
223        Ok(None) => (atoms::ok(), Option::<()>::None).encode(env),
224        Ok(Some(ip)) => (atoms::ok(), ip_to_erl(env, ip)).encode(env),
225    }
226}
227
228fn ip_to_erl(env: rustler::Env, ip: impl Into<IpAddr>) -> Term {
229    match ip.into() {
230        IpAddr::V4(ip) => {
231            let octets = ip.octets();
232            (octets[0], octets[1], octets[2], octets[3]).encode(env)
233        }
234        IpAddr::V6(ip) => {
235            // rustler doesn't provide `impl Encoder` for 8-length tuples
236            let segments = ip.segments().map(|segment| segment.encode(env));
237
238            let tuple = rustler::types::tuple::make_tuple(env, &segments);
239            tuple.encode(env)
240        }
241    }
242}
243
244enum IpOrSelf {
245    Ip(IpAddr),
246    SelfV4,
247    SelfV6,
248}
249
250impl IpOrSelf {
251    pub fn new(ip: Term<'_>) -> Option<Self> {
252        if let Some(ip) = ip_from_erl(ip) {
253            return Some(Self::Ip(ip));
254        }
255
256        let atom = ip.decode::<rustler::Atom>().ok()?;
257        if atom == atoms::ip4() {
258            return Some(Self::SelfV4);
259        }
260
261        if atom == atoms::ip6() {
262            return Some(Self::SelfV6);
263        }
264
265        None
266    }
267
268    pub async fn resolve(&self, dev: &tailscale::Device) -> Result<IpAddr> {
269        match self {
270            IpOrSelf::Ip(ip) => Ok(*ip),
271            IpOrSelf::SelfV4 => dev.ipv4_addr().await.map(Into::into).map_err(Into::into),
272            IpOrSelf::SelfV6 => dev.ipv6_addr().await.map(Into::into).map_err(Into::into),
273        }
274    }
275}
276
277fn ip_from_erl(ip: Term) -> Option<IpAddr> {
278    if let Ok(tuple) = rustler::types::tuple::get_tuple(ip) {
279        if tuple.len() == 4 {
280            let mut octets = [0u8; 4];
281
282            for (i, elem) in tuple.into_iter().take(4).enumerate() {
283                octets[i] = elem.decode().ok()?;
284            }
285
286            return Some(Ipv4Addr::from_octets(octets).into());
287        }
288
289        if tuple.len() == 8 {
290            let mut segments = [0u16; 8];
291
292            for (i, elem) in tuple.into_iter().take(8).enumerate() {
293                segments[i] = elem.decode().ok()?;
294            }
295
296            return Some(Ipv6Addr::from_segments(segments).into());
297        }
298    }
299
300    if let Ok(s) = ip.decode::<&str>() {
301        return IpAddr::from_str(s).ok();
302    }
303
304    None
305}
306
307fn sockaddr_to_erl(env: rustler::Env, addr: SocketAddr) -> impl Encoder {
308    (ip_to_erl(env, addr.ip()), addr.port())
309}
310
311/// Decode a `{ip, port}` tuple (the same shape [`sockaddr_to_erl`] produces) into a [`SocketAddr`].
312fn sockaddr_from_erl(term: Term) -> Option<SocketAddr> {
313    let tuple = rustler::types::tuple::get_tuple(term).ok()?;
314    if tuple.len() != 2 {
315        return None;
316    }
317    let ip = ip_from_erl(tuple[0])?;
318    let port: u16 = tuple[1].decode().ok()?;
319    Some(SocketAddr::new(ip, port))
320}
321
322#[rustler::nif(schedule = "DirtyIo")]
323fn ping(env: rustler::Env, dev: ResourceArc<Device>, addr: Term, timeout_ms: u64) -> impl Encoder {
324    let dev = dev.inner.clone();
325    let Some(ip) = ip_from_erl(addr) else {
326        return env.error_tuple("invalid ip");
327    };
328    let timeout = Duration::from_millis(timeout_ms);
329
330    match TOKIO_RUNTIME.block_on(async move { dev.ping(ip, timeout).await }) {
331        Ok(rtt) => (atoms::ok(), rtt.as_secs_f64() * 1000.0).encode(env),
332        Err(e) => (atoms::error(), e.to_string()).encode(env),
333    }
334}
335
336fn load(env: rustler::Env, _term: Term) -> bool {
337    let ret = env.register::<UdpSocket>().is_ok()
338        && env.register::<Device>().is_ok()
339        && env.register::<TcpStream>().is_ok()
340        && env.register::<TcpListener>().is_ok()
341        && env.register::<LoopbackHandleResource>().is_ok();
342    if ret {
343        tracing::debug!("loaded tailscale nifs");
344    }
345
346    ret
347}
348
349rustler::init!("Elixir.Tailscale.Native", load = load);