use system_configuration::{
core_foundation::{
array::CFArray,
base::{CFType, TCFType},
dictionary::CFDictionary,
string::CFString,
},
dynamic_store::{SCDynamicStore, SCDynamicStoreBuilder},
};
const STATE_DNS_PATTERN: &str = "State:/Network/Service/.*/DNS";
const SERVER_ADDRESSES: &str = "ServerAddresses";
pub type DnsBackup = Vec<(String, Option<Vec<String>>)>;
fn store() -> SCDynamicStore {
SCDynamicStoreBuilder::new("io.geph.manager.dns").build()
}
fn state_to_setup_path(state_path: &str) -> Option<String> {
state_path
.strip_prefix("State:/")
.map(|rest| format!("Setup:/{rest}"))
}
fn all_dns_paths(store: &SCDynamicStore) -> Vec<String> {
let mut paths: Vec<String> = store
.get_keys(STATE_DNS_PATTERN)
.map(|keys| keys.iter().map(|k| k.to_string()).collect())
.unwrap_or_default();
let setups: Vec<String> = paths
.iter()
.filter_map(|p| state_to_setup_path(p))
.collect();
paths.extend(setups);
paths
}
fn server_addresses(store: &SCDynamicStore, path: &str) -> Option<Vec<String>> {
let dict = store
.get(CFString::new(path))?
.downcast_into::<CFDictionary>()?;
let v = dict.find(CFString::new(SERVER_ADDRESSES).as_CFType().as_CFTypeRef() as *const _)?;
let arr = unsafe { CFArray::<CFString>::wrap_under_get_rule(*v as *const _) };
Some(arr.iter().map(|s| s.to_string()).collect())
}
fn dns_dict(servers: &[&str]) -> CFDictionary<CFString, CFType> {
let addrs = CFArray::from_CFTypes(
&servers
.iter()
.map(|s| CFString::new(s))
.collect::<Vec<_>>(),
);
CFDictionary::from_CFType_pairs(&[(CFString::new(SERVER_ADDRESSES), addrs.as_CFType())])
}
fn set_servers(store: &SCDynamicStore, path: &str, servers: &[&str]) -> bool {
store.set(CFString::new(path), dns_dict(servers).to_untyped())
}
pub fn set_sentinel(v4: &str, v6: &str) -> DnsBackup {
let store = store();
let mut backup = Vec::new();
for path in all_dns_paths(&store) {
let prior = server_addresses(&store, &path)
.filter(|servers| !servers.iter().any(|s| s == v4 || s == v6));
set_servers(&store, &path, &[v4, v6]);
backup.push((path, prior));
}
backup
}
pub fn reassert_sentinel(v4: &str, v6: &str) {
let store = store();
for path in all_dns_paths(&store) {
set_servers(&store, &path, &[v4, v6]);
}
}
pub fn restore(backup: &DnsBackup) {
let store = store();
for (path, prior) in backup {
match prior {
Some(servers) => {
let refs: Vec<&str> = servers.iter().map(|s| s.as_str()).collect();
set_servers(&store, path, &refs);
}
None => {
let _ = store.remove(CFString::new(path));
}
}
}
}
pub fn cleanup_stale(v4: &str, v6: &str) {
let store = store();
for path in all_dns_paths(&store) {
if let Some(servers) = server_addresses(&store, &path)
&& servers.iter().any(|s| s == v4 || s == v6)
{
let _ = store.remove(CFString::new(&path));
}
}
}
#[cfg(test)]
mod tests {
use super::state_to_setup_path;
#[test]
fn setup_path_derivation() {
assert_eq!(
state_to_setup_path("State:/Network/Service/ABC-123/DNS").as_deref(),
Some("Setup:/Network/Service/ABC-123/DNS")
);
assert_eq!(state_to_setup_path("Setup:/Network/Service/X/DNS"), None);
}
}