#![allow(non_camel_case_types)]
use std::{
ffi::{self, CStr, CString, c_char},
net::SocketAddr,
sync::{LazyLock, Once},
time::Duration,
};
use tracing::level_filters::LevelFilter;
mod capture;
mod config;
mod keys;
mod loopback;
mod net_types;
mod panic_guard;
mod status;
mod taildrop;
mod tcp;
mod tls;
mod udp;
mod util;
pub use capture::{ts_capture_pcap, ts_stop_capture};
pub use loopback::{loopback_handle, ts_loopback, ts_loopback_stop};
pub use net_types::{
AF_INET, AF_INET6, in_addr_t, in6_addr_t, sa_family_t, sockaddr, sockaddr_data, sockaddr_in,
sockaddr_in6,
};
pub(crate) use panic_guard::ffi_guard;
pub use status::{status_node, status_visitor, ts_status, ts_whois};
pub use taildrop::{
ts_taildrop_delete_file, ts_taildrop_file_size, ts_taildrop_save_file,
ts_taildrop_waiting_files,
};
pub use tcp::{
tcp_listener, tcp_stream, ts_connect_by_name, ts_tcp_close, ts_tcp_close_listener,
ts_tcp_connect, ts_tcp_listen, ts_tcp_listener_local_addr, ts_tcp_local_addr, ts_tcp_recv,
ts_tcp_remote_addr, ts_tcp_send,
};
pub use tls::{
serve_config, serve_target, service_mode, ts_get_certificate, ts_listen_service, ts_listen_tls,
};
pub use udp::{ts_udp_bind, ts_udp_close, ts_udp_recvfrom, ts_udp_sendto, udp_socket};
static TOKIO_RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
tracing::info!("started tokio runtime");
rt
});
pub struct device(tailscale::Device);
static TRACING_ONCE: Once = Once::new();
#[unsafe(no_mangle)]
pub extern "C" fn ts_init_tracing() {
ffi_guard(move || {
TRACING_ONCE.call_once(|| {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init();
});
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_init(
config: Option<&config::config>,
auth_token: *const c_char,
) -> Option<Box<device>> {
ffi_guard(move || {
ts_init_tracing();
let config = match config {
Some(cfg) => unsafe { cfg.to_ts_config() },
None => Default::default(),
};
let auth_token = if auth_token.is_null() {
None
} else {
unsafe { util::str(auth_token).map(ToOwned::to_owned) }
};
match TOKIO_RUNTIME.block_on(tailscale::Device::new(&config, auth_token)) {
Ok(dev) => Some(Box::new(device(dev))),
Err(e) => {
tracing::error!(err = %e, "ts_init failed");
None
}
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_init_from_key_file(
key_file: *const c_char,
auth_token: *const c_char,
) -> Option<Box<device>> {
ffi_guard(move || {
let mut state = keys::persisted_key_state::default();
if unsafe { keys::ts_load_key_file(key_file, false, &mut state) } < 0 {
return None;
}
let config = config::config {
key_state: Some(&mut state),
..Default::default()
};
unsafe { ts_init(Some(&config), auth_token) }
})
}
#[unsafe(no_mangle)]
pub extern "C" fn ts_deinit(dev: Box<device>) {
ffi_guard(move || drop(dev))
}
#[unsafe(no_mangle)]
pub extern "C" fn ts_ipv4_addr(dev: &device, dst: &mut in_addr_t) -> ffi::c_int {
ffi_guard(move || {
let addr = match TOKIO_RUNTIME.block_on(dev.0.ipv4_addr()) {
Ok(addr) => addr,
Err(e) => {
tracing::error!(error = %e, "getting ipv4");
return -1;
}
};
dst.0 = addr.octets();
0
})
}
#[unsafe(no_mangle)]
pub extern "C" fn ts_ipv6_addr(dev: &device, dst: &mut in6_addr_t) -> ffi::c_int {
ffi_guard(move || {
let addr = match TOKIO_RUNTIME.block_on(dev.0.ipv6_addr()) {
Ok(addr) => addr,
Err(e) => {
tracing::error!(error = %e, "getting ipv6");
return -1;
}
};
dst.0 = addr.segments();
0
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_peer_ipv4_addr(
dev: &device,
peer_name: *const c_char,
addr: &mut in_addr_t,
) -> ffi::c_int {
ffi_guard(move || {
unsafe {
_peer_by_addr(dev, peer_name, |n| {
*addr = n.tailnet_address.ipv4.addr().into();
})
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_peer_ipv6_addr(
dev: &device,
peer_name: *const c_char,
addr: &mut in6_addr_t,
) -> ffi::c_int {
ffi_guard(move || {
unsafe {
_peer_by_addr(dev, peer_name, |n| {
*addr = n.tailnet_address.ipv6.addr().into();
})
}
})
}
unsafe fn _peer_by_addr(
dev: &device,
peer_name: *const c_char,
on_node_info: impl FnOnce(&tailscale::NodeInfo),
) -> ffi::c_int {
let name = unsafe { CStr::from_ptr(peer_name) };
let Ok(name) = name.to_str() else {
tracing::error!("peer name: invalid utf-8");
return -1;
};
match TOKIO_RUNTIME.block_on(dev.0.peer_by_name(name)) {
Ok(Some(node)) => {
on_node_info(&node);
1
}
Ok(None) => 0,
Err(e) => {
tracing::error!(error = %e, "looking up peer");
-1
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_resolve(
dev: &device,
name: *const c_char,
addr: &mut in_addr_t,
) -> ffi::c_int {
ffi_guard(move || {
let Some(name) = (unsafe { util::str(name) }) else {
tracing::error!("resolve: name is null or invalid utf-8");
return -1;
};
match TOKIO_RUNTIME.block_on(dev.0.resolve(name)) {
Ok(Some(ipv4)) => {
*addr = ipv4.into();
1
}
Ok(None) => 0,
Err(e) => {
tracing::error!(error = %e, "resolve");
-1
}
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_ping(
dev: &device,
dst: &sockaddr,
timeout_ms: u64,
rtt_ms: &mut u64,
) -> ffi::c_int {
ffi_guard(move || {
let Ok(dst): Result<SocketAddr, _> = dst.try_into() else {
tracing::error!("ping: invalid sockaddr");
return -1;
};
match TOKIO_RUNTIME.block_on(dev.0.ping(dst.ip(), Duration::from_millis(timeout_ms))) {
Ok(rtt) => {
*rtt_ms = rtt.as_millis() as u64;
0
}
Err(e) => {
tracing::error!(err = %e, "ping");
-1
}
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_string_free(s: *mut c_char) {
ffi_guard(move || {
if s.is_null() {
return;
}
drop(unsafe { CString::from_raw(s) });
})
}
pub(crate) fn into_c_string(s: String) -> *mut c_char {
match CString::new(s) {
Ok(c) => c.into_raw(),
Err(e) => {
tracing::error!(err = %e, "string contains interior NUL");
std::ptr::null_mut()
}
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_fetch_id_token(
dev: &device,
audience: *const c_char,
out: *mut *mut c_char,
) -> ffi::c_int {
ffi_guard(move || {
let Some(audience) = (unsafe { util::str(audience) }) else {
tracing::error!("fetch_id_token: audience is null or invalid utf-8");
return -1;
};
match TOKIO_RUNTIME.block_on(dev.0.fetch_id_token(audience)) {
Ok(jwt) => {
let ptr = into_c_string(jwt);
if ptr.is_null() {
return -1;
}
unsafe { *out = ptr };
0
}
Err(e) => {
tracing::error!(err = %e, "fetch_id_token");
unsafe { *out = std::ptr::null_mut() };
-1
}
}
})
}
#[unsafe(no_mangle)]
pub extern "C" fn ts_metrics(dev: &device) -> *mut c_char {
ffi_guard(move || into_c_string(dev.0.metrics()))
}
#[unsafe(no_mangle)]
pub extern "C" fn ts_self_key_expiry_unix(
dev: &device,
out_unix: &mut i64,
out_has: &mut ffi::c_int,
) -> ffi::c_int {
ffi_guard(
move || match TOKIO_RUNTIME.block_on(dev.0.self_key_expiry_unix()) {
Ok(Some(unix)) => {
*out_unix = unix;
*out_has = 1;
0
}
Ok(None) => {
*out_has = 0;
0
}
Err(e) => {
tracing::error!(err = %e, "self_key_expiry_unix");
-1
}
},
)
}
#[unsafe(no_mangle)]
pub extern "C" fn ts_self_key_expired(dev: &device, out: &mut ffi::c_int) -> ffi::c_int {
ffi_guard(
move || match TOKIO_RUNTIME.block_on(dev.0.self_key_expired()) {
Ok(expired) => {
*out = expired as ffi::c_int;
0
}
Err(e) => {
tracing::error!(err = %e, "self_key_expired");
-1
}
},
)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_tka_status(dev: &device, out: *mut *mut c_char) -> ffi::c_int {
ffi_guard(move || {
match TOKIO_RUNTIME.block_on(dev.0.tka_status()) {
Ok(status) => {
let json = match status {
Some(s) => format!(
"{{\"enabled\":{},\"disabled\":{},\"head\":{:?}}}",
s.is_enabled(),
s.disabled,
s.head
),
None => "null".to_owned(),
};
let ptr = into_c_string(json);
if ptr.is_null() {
return -1;
}
unsafe { *out = ptr };
0
}
Err(e) => {
tracing::error!(err = %e, "tka_status");
unsafe { *out = std::ptr::null_mut() };
-1
}
}
})
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ts_send_file(
dev: &device,
peer_name: *const c_char,
file_name: *const c_char,
src_path: *const c_char,
) -> ffi::c_int {
ffi_guard(move || {
let (Some(peer_name), Some(file_name), Some(src_path)) = (unsafe {
(
util::str(peer_name),
util::str(file_name),
util::str(src_path),
)
}) else {
tracing::error!("send_file: a string argument is null or invalid utf-8");
return -1;
};
let peer = match TOKIO_RUNTIME.block_on(dev.0.peer_by_name(peer_name)) {
Ok(Some(peer)) => peer,
Ok(None) => return 1,
Err(e) => {
tracing::error!(err = %e, "send_file: peer lookup");
return -1;
}
};
TOKIO_RUNTIME.block_on(async {
let file = match tokio::fs::File::open(src_path).await {
Ok(f) => f,
Err(e) => {
tracing::error!(err = %e, "send_file: open source");
return -1;
}
};
let len = match file.metadata().await {
Ok(m) => m.len(),
Err(e) => {
tracing::error!(err = %e, "send_file: stat source");
return -1;
}
};
match dev.0.send_file(&peer, file_name, len, file).await {
Ok(()) => 0,
Err(e) => {
tracing::error!(err = %e, "send_file");
-1
}
}
})
})
}