use std::net::{Ipv4Addr, Ipv6Addr};
use anyhow::{Context, bail};
use system_configuration::{
core_foundation::{base::TCFType, dictionary::CFDictionary, string::CFString},
dynamic_store::{SCDynamicStore, SCDynamicStoreBuilder},
};
pub struct PrimaryV4 {
pub ifname: String,
pub router: Ipv4Addr,
pub service_id: String,
}
pub struct PrimaryV6 {
pub ifname: String,
pub router: Ipv6Addr,
}
fn store() -> SCDynamicStore {
SCDynamicStoreBuilder::new("io.geph.manager.vpn").build()
}
fn global_state(store: &SCDynamicStore, key: &str) -> Option<(String, String, String)> {
let dict = store
.get(CFString::new(key))?
.downcast_into::<CFDictionary>()?;
let get = |k: &str| -> Option<String> {
let v = dict.find(CFString::new(k).as_CFType().as_CFTypeRef() as *const _)?;
let s = unsafe { CFString::wrap_under_get_rule(*v as *const _) };
Some(s.to_string())
};
Some((
get("PrimaryInterface")?,
get("Router")?,
get("PrimaryService")?,
))
}
pub fn primary_v4() -> anyhow::Result<PrimaryV4> {
let store = store();
let (ifname, router, service_id) = global_state(&store, "State:/Network/Global/IPv4")
.context("no primary IPv4 service — not connected to a network?")?;
if super::is_tunnel_iface(&ifname) {
bail!("primary IPv4 interface is a tunnel ({ifname})");
}
let router: Ipv4Addr = router
.parse()
.with_context(|| format!("primary IPv4 router {router:?} is not an IPv4 address"))?;
Ok(PrimaryV4 {
ifname,
router,
service_id,
})
}
pub fn primary_v6() -> anyhow::Result<PrimaryV6> {
let store = store();
let (ifname, router, _service_id) = global_state(&store, "State:/Network/Global/IPv6")
.context("no primary IPv6 service")?;
if super::is_tunnel_iface(&ifname) {
bail!("primary IPv6 interface is a tunnel ({ifname})");
}
let addr = router.split('%').next().unwrap_or(&router);
let router: Ipv6Addr = addr
.parse()
.with_context(|| format!("primary IPv6 router {router:?} is not an IPv6 address"))?;
Ok(PrimaryV6 { ifname, router })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn live_primary_v4() {
match primary_v4() {
Ok(p) => {
assert!(!super::super::is_tunnel_iface(&p.ifname));
assert!(!p.ifname.is_empty());
}
Err(e) => eprintln!("no primary v4 ({e}); nothing to assert"),
}
}
}