use std::ffi::c_char;
use crate::{keys::persisted_key_state, util};
#[derive(Default)]
#[repr(C)]
pub struct config<'a> {
pub control_server_url: *const c_char,
pub hostname: *const c_char,
pub tags: *const *const c_char,
pub client_name: *const c_char,
pub key_state: Option<&'a mut persisted_key_state>,
pub accept_routes: bool,
pub exit_node: *const c_char,
pub advertise_routes: *const *const c_char,
pub advertise_exit_node: bool,
pub forward_tcp_ports: *const u16,
pub forward_tcp_ports_len: usize,
pub forward_udp_ports: *const u16,
pub forward_udp_ports_len: usize,
pub forward_all_ports: bool,
pub forward_exit_egress: bool,
}
impl config<'_> {
pub unsafe fn to_ts_config(&self) -> tailscale::Config {
let mut cfg = tailscale::Config::default();
let ctrl_url = unsafe { util::str(self.control_server_url) }.and_then(|u| u.parse().ok());
if let Some(u) = ctrl_url {
cfg.control_server_url = u;
}
if let Some(hostname) = unsafe { util::str(self.hostname) } {
cfg.requested_hostname = Some(hostname.to_string());
}
cfg.client_name = Some(
unsafe { util::str(self.client_name) }
.unwrap_or("ts_ffi")
.to_owned(),
);
if let Some(key_state) = &self.key_state {
cfg.key_state = (&**key_state).into();
}
cfg.requested_tags = unsafe {
load_sentinel_array(self.tags, |&tag| {
if tag.is_null() {
return None;
};
match util::str(tag) {
Some(tag_str) => Some(Some(tag_str.to_owned())),
None => {
tracing::error!("skipping invalid requested tag");
Some(None)
}
}
})
}
.collect();
cfg.accept_routes = self.accept_routes;
cfg.advertise_exit_node = self.advertise_exit_node;
cfg.forward_all_ports = self.forward_all_ports;
cfg.forward_exit_egress = self.forward_exit_egress;
cfg.exit_node = unsafe { util::str(self.exit_node) }
.and_then(|s| s.parse().ok());
cfg.advertise_routes = unsafe {
load_sentinel_array(self.advertise_routes, |&route| {
if route.is_null() {
return None;
}
match util::str(route).and_then(|s| s.parse().ok()) {
Some(net) => Some(Some(net)),
None => {
tracing::error!("skipping invalid advertised route");
Some(None)
}
}
})
}
.collect();
cfg.forward_tcp_ports =
unsafe { load_ports(self.forward_tcp_ports, self.forward_tcp_ports_len) };
cfg.forward_udp_ports =
unsafe { load_ports(self.forward_udp_ports, self.forward_udp_ports_len) };
cfg
}
}
unsafe fn load_ports(ports: *const u16, len: usize) -> Vec<u16> {
if ports.is_null() {
return Vec::new();
}
unsafe { core::slice::from_raw_parts(ports, len) }.to_vec()
}
unsafe fn load_sentinel_array<'t, T, It>(
mut ary: *const T,
elem_txfm: impl Fn(&T) -> Option<It> + 't,
) -> impl Iterator<Item = It::Item>
where
T: 't,
It: IntoIterator,
{
std::iter::from_fn(move || {
if ary.is_null() {
return None;
}
let it = match elem_txfm(unsafe { ary.as_ref().unwrap() }) {
Some(u) => u,
None => {
return None;
}
};
ary = unsafe { ary.offset(1) };
Some(it)
})
.flatten()
}
#[cfg(test)]
mod test {
use std::{ffi::CString, ptr::null};
use super::*;
#[test]
fn sentinel_array() {
let mut v = unsafe { load_sentinel_array::<u8, _>(null(), |_| Option::<[u8; 1]>::None) };
assert!(v.next().is_none());
let ary = [0u8, 1, 2, 3, 4, 5, 6, 128, 32];
let mut v =
unsafe { load_sentinel_array(&ary as *const u8, |_elt| Option::<Option<u8>>::None) };
assert!(v.next().is_none());
let v = unsafe {
load_sentinel_array(
&ary as *const u8,
|&elt| {
if elt < 10 { Some([elt]) } else { None }
},
)
}
.collect::<Vec<_>>();
assert!(!v.is_empty());
assert_eq!(v, ary[..=6].to_vec());
}
#[test]
fn tags() {
let tag_foo = CString::new("foo").unwrap();
let tag_bar = CString::new("bar").unwrap();
let config = config {
tags: &[tag_foo.as_ptr(), tag_bar.as_ptr(), null()] as *const *const c_char,
..Default::default()
};
let cfg = unsafe { config.to_ts_config() };
assert_eq!(cfg.requested_tags, vec!["foo", "bar"]);
}
#[test]
fn forwarding_defaults() {
let cfg = unsafe { config::default().to_ts_config() };
assert!(!cfg.accept_routes);
assert!(!cfg.advertise_exit_node);
assert!(!cfg.forward_all_ports);
assert!(!cfg.forward_exit_egress);
assert!(cfg.exit_node.is_none());
assert!(cfg.advertise_routes.is_empty());
assert!(cfg.forward_tcp_ports.is_empty());
assert!(cfg.forward_udp_ports.is_empty());
}
#[test]
fn forwarding_fields() {
let exit = CString::new("exit-host").unwrap();
let r1 = CString::new("10.0.0.0/24").unwrap();
let r2 = CString::new("192.168.1.0/24").unwrap();
let tcp_ports = [80u16, 443];
let udp_ports = [53u16];
let config = config {
accept_routes: true,
advertise_exit_node: true,
forward_all_ports: true,
forward_exit_egress: true,
exit_node: exit.as_ptr(),
advertise_routes: &[r1.as_ptr(), r2.as_ptr(), null()] as *const *const c_char,
forward_tcp_ports: tcp_ports.as_ptr(),
forward_tcp_ports_len: tcp_ports.len(),
forward_udp_ports: udp_ports.as_ptr(),
forward_udp_ports_len: udp_ports.len(),
..Default::default()
};
let cfg = unsafe { config.to_ts_config() };
assert!(cfg.accept_routes);
assert!(cfg.advertise_exit_node);
assert!(cfg.forward_all_ports);
assert!(cfg.forward_exit_egress);
assert_eq!(
cfg.exit_node,
Some(tailscale::ExitNodeSelector::Name("exit-host".into()))
);
assert_eq!(
cfg.advertise_routes,
vec![
"10.0.0.0/24".parse().unwrap(),
"192.168.1.0/24".parse().unwrap(),
]
);
assert_eq!(cfg.forward_tcp_ports, vec![80, 443]);
assert_eq!(cfg.forward_udp_ports, vec![53]);
}
}