use std::ffi::{self, CString, c_char};
use std::path::PathBuf;
use tailscale::tsnet;
use crate::{TOKIO_RUNTIME, ffi_guard, into_c_string, net_types::sockaddr, util};
pub struct server(tsnet::Server);
pub struct local_client(tsnet::LocalClient);
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_server_new(
hostname: *const c_char,
auth_key: *const c_char,
control_url: *const c_char,
dir: *const c_char,
ephemeral: bool,
tags: *const *const c_char,
tags_len: usize,
) -> Option<Box<server>> {
ffi_guard(move || {
crate::ts_init_tracing();
let optional_text = |p: *const c_char| {
if p.is_null() {
Some(None)
} else {
unsafe { util::str(p) }.map(|value| Some(value.to_owned()))
}
};
let (Some(hostname), Some(auth_key), Some(control_url), Some(dir)) = (
optional_text(hostname),
optional_text(auth_key),
optional_text(control_url),
optional_text(dir),
) else {
tracing::error!("ts_server_new: configuration contains invalid UTF-8");
return None;
};
let mut s = tsnet::Server::new();
s.hostname = hostname;
s.auth_key = auth_key;
s.control_url = control_url;
s.ephemeral = ephemeral;
s.dir = dir.map(PathBuf::from);
if !tags.is_null() {
s.advertise_tags = unsafe { std::slice::from_raw_parts(tags, tags_len) }
.iter()
.filter_map(|&p| unsafe { util::str(p) }.map(ToOwned::to_owned))
.collect();
}
Some(Box::new(server(s)))
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_server_loopback(
srv: &server,
out_socks_addr: *mut sockaddr,
out_proxy_cred: *mut *mut c_char,
out_localapi_addr: *mut sockaddr,
out_localapi_cred: *mut *mut c_char,
) -> ffi::c_int {
ffi_guard(move || {
let outs = [
out_socks_addr as *const (),
out_proxy_cred as *const (),
out_localapi_addr as *const (),
out_localapi_cred as *const (),
];
if outs.iter().any(|p| p.is_null()) {
tracing::error!("server loopback: a NULL output pointer");
return -1;
}
if outs
.iter()
.enumerate()
.any(|(i, p)| outs[i + 1..].contains(p))
{
tracing::error!("server loopback: aliasing (duplicate) output pointers");
return -1;
}
let lb = match TOKIO_RUNTIME.block_on(srv.0.loopback()) {
Ok(lb) => lb,
Err(e) => {
tracing::error!(err = %e, "server loopback");
return -1;
}
};
let (proxy, localapi) = match (CString::new(lb.proxy_cred), CString::new(lb.local_api_cred))
{
(Ok(p), Ok(l)) => (p, l),
_ => {
tracing::error!("loopback credential contains interior NUL");
return -1;
}
};
unsafe {
*out_socks_addr = lb.address.into();
*out_localapi_addr = lb.local_api_address.into();
*out_proxy_cred = proxy.into_raw();
*out_localapi_cred = localapi.into_raw();
}
0
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_server_local_client(
srv: &server,
out_handle: *mut *mut local_client,
) -> ffi::c_int {
ffi_guard(move || match TOKIO_RUNTIME.block_on(srv.0.local_client()) {
Ok(lc) => {
unsafe { *out_handle = Box::into_raw(Box::new(local_client(lc))) };
0
}
Err(e) => {
tracing::error!(err = %e, "server local_client");
-1
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_local_client_status(
lc: &local_client,
out_json: *mut *mut c_char,
) -> ffi::c_int {
ffi_guard(move || match TOKIO_RUNTIME.block_on(lc.0.status()) {
Ok(body) => {
let json = into_c_string(String::from_utf8_lossy(&body).into_owned());
if json.is_null() {
return -1;
}
unsafe { *out_json = json };
0
}
Err(e) => {
tracing::error!(err = %e, "local_client status");
-1
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_local_client_get(
lc: &local_client,
path: *const c_char,
out_code: *mut u16,
out_body: *mut *mut c_char,
) -> ffi::c_int {
ffi_guard(move || {
let Some(path) = (unsafe { util::str(path) }) else {
tracing::error!("local_client get: path is NULL or not UTF-8");
return -1;
};
match TOKIO_RUNTIME.block_on(lc.0.get(path)) {
Ok((code, body)) => {
let body = into_c_string(String::from_utf8_lossy(&body).into_owned());
if body.is_null() {
return -1;
}
unsafe {
*out_code = code;
*out_body = body;
}
0
}
Err(e) => {
tracing::error!(err = %e, "local_client get");
-1
}
}
})
}
#[unsafe(no_mangle)]
pub extern "C" fn ts_local_client_address(lc: &local_client) -> sockaddr {
ffi_guard(move || lc.0.address().into())
}
#[unsafe(no_mangle)]
pub extern "C" fn ts_local_client_credential(lc: &local_client) -> *mut c_char {
ffi_guard(move || into_c_string(lc.0.credential().to_owned()))
}
#[unsafe(no_mangle)]
pub extern "C" fn ts_server_free(srv: Option<Box<server>>) {
ffi_guard(move || drop(srv))
}
#[unsafe(no_mangle)]
pub extern "C" fn ts_local_client_free(lc: Option<Box<local_client>>) {
ffi_guard(move || drop(lc))
}