use crate::config::Config;
use crate::{network, pause, tailscale};
use core_foundation::array::CFArray;
use core_foundation::runloop::{CFRunLoop, kCFRunLoopCommonModes};
use core_foundation::string::CFString;
use system_configuration::dynamic_store::{
SCDynamicStore, SCDynamicStoreBuilder, SCDynamicStoreCallBackContext,
};
const WATCHED_KEYS: [&str; 2] = ["State:/Network/Global/IPv4", "State:/Network/Global/IPv6"];
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
reconcile();
let store = SCDynamicStoreBuilder::new(crate::launchd::LABEL)
.callback_context(SCDynamicStoreCallBackContext {
callout: on_network_change,
info: (),
})
.build()
.ok_or("could not create SCDynamicStore session")?;
let cf_keys = WATCHED_KEYS.map(CFString::from_static_string);
let keys = CFArray::from_CFTypes(&cf_keys);
let patterns = CFArray::<CFString>::from_CFTypes(&[]);
if !store.set_notification_keys(&keys, &patterns) {
return Err("could not register SystemConfiguration notification keys".into());
}
let source = store
.create_run_loop_source()
.ok_or("could not create run loop source")?;
CFRunLoop::get_current().add_source(&source, unsafe { kCFRunLoopCommonModes });
println!("tailcloak: watching for network changes (Ctrl-C to stop)");
CFRunLoop::run_current(); Ok(())
}
pub fn run_once() -> Result<(), Box<dyn std::error::Error>> {
reconcile();
Ok(())
}
fn on_network_change(_store: SCDynamicStore, _changed_keys: CFArray<CFString>, _info: &mut ()) {
reconcile();
}
fn reconcile() {
if let Some(left) = pause::remaining() {
println!(
"tailcloak: paused ({} min left) — leaving Tailscale unchanged",
left.as_secs().div_ceil(60)
);
return;
}
let config = match Config::load_or_default() {
Ok(config) => config,
Err(e) => {
eprintln!("tailcloak: failed to load config: {e}");
return;
}
};
let gateway = network::current_mac_gateway();
let trusted = gateway.is_some_and(|mac| config.trusted_gateway_macs.contains(&mac));
let result = if trusted {
tailscale::down()
} else {
tailscale::up()
};
let gw = gateway.map_or_else(|| "none".to_string(), |mac| mac.to_string());
match result {
Ok(()) => println!(
"tailcloak: gateway {gw} {} -> tailscale {}",
if trusted { "trusted" } else { "untrusted" },
if trusted { "down" } else { "up" }
),
Err(e) => eprintln!("tailcloak: failed to toggle tailscale: {e}"),
}
}