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