#![cfg(target_os = "macos")]
use cidr::IpCidr;
use system_configuration::{
core_foundation::{
array::CFArray,
base::TCFType,
dictionary::{CFDictionary, CFDictionaryGetValue},
propertylist::{CFPropertyList, CFPropertyListSubClass},
string::CFString,
},
dynamic_store::SCDynamicStoreBuilder,
sys::dynamic_store::SCDynamicStoreCopyValue,
};
use crate::{ETC_RESOLV_CONF_FILE, TproxyArgs, TproxyStateInner, run_command};
use std::{net::IpAddr, str::FromStr};
pub(crate) async fn _tproxy_setup(tproxy_args: &TproxyArgs) -> std::io::Result<TproxyStateInner> {
flush_dns_cache()?;
let (original_gateway_0, orig_gw_iface, service_id, orig_iface_name) = get_default_iface_params()?;
let original_gateway = original_gateway_0.to_string();
let tun_ip = tproxy_args.tun_ip.to_string();
let tun_netmask = tproxy_args.tun_netmask.to_string();
let tun_gateway = tproxy_args.tun_gateway.to_string();
run_command("sysctl", &["-w", "net.inet.ip.forwarding=1"])?;
let args = &[&tproxy_args.tun_name, &tun_ip, &tun_gateway, "netmask", &tun_netmask];
run_command("ifconfig", args)?;
run_command("route", &["delete", "default"])?;
run_command("route", &["delete", "default", "-ifscope", &orig_gw_iface])?;
run_command("route", &["add", "default", &tun_gateway])?;
run_command("route", &["add", "default", &original_gateway, "-ifscope", &orig_gw_iface])?;
for bypass_ip in tproxy_args.bypass_ips.iter() {
let args = &["add", &bypass_ip.to_string(), &original_gateway];
run_command("route", args)?;
}
if tproxy_args.bypass_ips.is_empty() && !crate::is_private_ip(tproxy_args.proxy_addr.ip()) {
let bypass_ip = IpCidr::new_host(tproxy_args.proxy_addr.ip());
let args = &["add", &bypass_ip.to_string(), &original_gateway];
run_command("route", args)?;
}
let default_service_dns = match get_dns_servers(&service_id) {
Ok(servers) => servers,
Err(msg) => {
log::error!("failed to get DNS servers of default interface: {msg}");
None
}
};
let restore_resolvconf_content = Some(std::fs::read(ETC_RESOLV_CONF_FILE)?);
{
let file = std::fs::OpenOptions::new().write(true).truncate(true).open(ETC_RESOLV_CONF_FILE)?;
let mut writer = std::io::BufWriter::new(file);
use std::io::Write;
writeln!(writer, "nameserver {tun_gateway}\n")?;
let orig_iface_name = orig_iface_name.as_deref().unwrap_or("");
configure_dns_servers(orig_iface_name, &service_id, &[tproxy_args.tun_gateway])?;
}
let state = TproxyStateInner {
tproxy_args: Some(tproxy_args.clone()),
original_dns_servers: None,
gateway: Some(original_gateway_0),
gw_scope: Some(orig_gw_iface),
umount_resolvconf: false,
restore_resolvconf_content,
tproxy_removed_done: false,
default_service_id: Some(service_id),
default_service_dns,
orig_iface_name,
};
#[cfg(all(feature = "unsafe-state-file", any(target_os = "macos", target_os = "windows")))]
crate::store_intermediate_state(&state)?;
Ok(state)
}
pub(crate) async fn _tproxy_remove(state: &mut TproxyStateInner) -> std::io::Result<()> {
if state.tproxy_removed_done {
return Ok(());
}
state.tproxy_removed_done = true;
let err = std::io::Error::other("No original gateway found");
let original_gateway = state.gateway.take().ok_or(err)?.to_string();
let err = std::io::Error::other("No original gateway scope found");
let original_gw_scope = state.gw_scope.take().ok_or(err)?;
if let Some(service_id) = &state.default_service_id {
let iface_friendly_name = state.orig_iface_name.as_deref().unwrap_or("");
if let Some(default_service_dns) = &state.default_service_dns {
if let Err(_err) = configure_dns_servers(iface_friendly_name, service_id, default_service_dns.as_slice()) {
log::debug!("restore original dns servers error: {_err}");
}
} else if let Err(e) = remove_dns_servers(service_id, iface_friendly_name) {
log::debug!("failed to remove DNS servers: {e}");
}
}
let err = std::io::Error::new(std::io::ErrorKind::InvalidData, "tproxy_args is None");
let tproxy_args = state.tproxy_args.as_ref().ok_or(err)?;
for bypass_ip in tproxy_args.bypass_ips.iter() {
let args = &["delete", &bypass_ip.to_string()];
run_command("route", args)?;
}
if tproxy_args.bypass_ips.is_empty() && !crate::is_private_ip(tproxy_args.proxy_addr.ip()) {
let bypass_ip = IpCidr::new_host(tproxy_args.proxy_addr.ip());
let args = &["delete", &bypass_ip.to_string()];
run_command("route", args)?;
}
if let Err(_err) = run_command("route", &["delete", "default"]) {
log::debug!("command \"route delete default\" error: {_err}");
}
if let Err(_err) = run_command("route", &["delete", "default", "-ifscope", &original_gw_scope]) {
log::debug!("command \"route delete default -ifscope {original_gw_scope}\" error: {_err}");
}
if let Err(_err) = run_command("route", &["add", "default", &original_gateway]) {
log::debug!("command \"route add default {original_gateway}\" error: {_err}");
}
if let Some(data) = &state.restore_resolvconf_content {
std::fs::write(ETC_RESOLV_CONF_FILE, data)?;
}
#[cfg(all(feature = "unsafe-state-file", any(target_os = "macos", target_os = "windows")))]
let _ = std::fs::remove_file(crate::get_state_file_path());
flush_dns_cache()?;
Ok(())
}
fn get_cf_dict_entry<T>(dict: &CFDictionary, key: CFString) -> Option<T>
where
T: CFPropertyListSubClass,
{
let result = dict.find(key.as_CFTypeRef())?;
if result.is_null() {
return None;
}
let property_list = unsafe { CFPropertyList::wrap_under_get_rule(*result) };
property_list.downcast::<T>()
}
pub(crate) fn get_default_iface_params() -> std::io::Result<(IpAddr, String, String, Option<String>)> {
let store = SCDynamicStoreBuilder::new("tproxy-config get iface params")
.build()
.ok_or(std::io::Error::other("Create SC DynamicStore failed"))?;
let Some(property_list) = store.get("State:/Network/Global/IPv4") else {
return Err(std::io::Error::other("Failed to get network state"));
};
let Some(dict) = property_list.downcast::<CFDictionary>() else {
return Err(std::io::Error::other("Dictionary conversion failed"));
};
let Some(gateway) = get_cf_dict_entry::<CFString>(&dict, "Router".into()) else {
return Err(std::io::Error::other("Failed to get default gateway"));
};
let Some(interface) = get_cf_dict_entry::<CFString>(&dict, "PrimaryInterface".into()) else {
return Err(std::io::Error::other("Failed to get default interface"));
};
let Some(service_id) = get_cf_dict_entry::<CFString>(&dict, "PrimaryService".into()) else {
return Err(std::io::Error::other("Failed to get default service"));
};
use std::io::{Error, ErrorKind::InvalidData};
let gateway_ip = IpAddr::from_str(gateway.to_string().as_str()).map_err(|err| Error::new(InvalidData, err))?;
let mut iface_name = None;
let svc_path: CFString = format!("Setup:/Network/Service/{service_id}").as_str().into();
if let Some(service_dict) = unsafe { SCDynamicStoreCopyValue(store.as_concrete_TypeRef(), svc_path.as_concrete_TypeRef()).as_ref() }
.and_then(|plist| unsafe { CFPropertyList::wrap_under_create_rule(plist) }.downcast::<CFDictionary>())
{
let key: CFString = "UserDefinedName".into();
if let Some(name) = unsafe { CFDictionaryGetValue(service_dict.as_concrete_TypeRef(), key.as_CFTypeRef()).as_ref() }
.map(|cfstr| unsafe { CFString::wrap_under_get_rule(cfstr as *const _ as _) })
{
iface_name = Some(name.to_string());
}
}
Ok((gateway_ip, interface.to_string(), service_id.to_string(), iface_name))
}
fn configure_dns_servers(iface_friendly_name: &str, service_id: &str, dns_servers: &[IpAddr]) -> std::io::Result<()> {
if dns_servers.is_empty() {
return Ok(());
}
let store = SCDynamicStoreBuilder::new("tproxy-config configure dns")
.build()
.ok_or(std::io::Error::other("Create SC DynamicStore failed"))?;
let mut dns_server_vec = Vec::<CFString>::new();
dns_servers.iter().for_each(|x| dns_server_vec.push(x.to_string().as_str().into()));
let dns_server_array = CFArray::from_CFTypes(dns_server_vec.as_slice());
let dns_dict = CFDictionary::from_CFType_pairs(&[(CFString::from("ServerAddresses"), dns_server_array)]);
let key = format!("State:/Network/Service/{service_id}/DNS").as_str().into();
if !store.set::<CFString, CFDictionary>(key, dns_dict.to_untyped()) {
log::error!("Failed to set DNS servers");
}
if !iface_friendly_name.is_empty() {
let addrs = dns_servers.iter().map(|x| x.to_string()).collect::<Vec<String>>();
let mut args = vec!["-setdnsservers", iface_friendly_name];
args.extend(addrs.iter().map(|s| s.as_str()));
run_command("networksetup", &args)?;
}
Ok(())
}
fn get_dns_servers(service_id: &String) -> std::io::Result<Option<Vec<IpAddr>>> {
let store = SCDynamicStoreBuilder::new("tproxy-config get dns")
.build()
.ok_or(std::io::Error::other("Create SC DynamicStore failed"))?;
let key: CFString = format!("State:/Network/Service/{service_id}/DNS").as_str().into();
let Some(result) = store.get(key) else {
return Ok(None);
};
let Some(cf_dict) = result.downcast::<CFDictionary>() else {
return Err(std::io::Error::other("Network service DNS server conversion failed"));
};
let Some(server_addresses) = cf_dict.find(CFString::from("ServerAddresses").as_CFTypeRef()) else {
return Ok(None);
};
if server_addresses.is_null() {
return Err(std::io::Error::other("Server addresses are null"));
}
let server_addr_prop = unsafe { CFPropertyList::wrap_under_get_rule(*server_addresses) };
let Some(cf_array) = server_addr_prop.downcast::<CFArray>() else {
return Err(std::io::Error::other("Server address conversion failed"));
};
let mut vec: Vec<IpAddr> = Vec::new();
for item in cf_array.iter() {
if item.is_null() {
continue;
}
let property_list = unsafe { CFPropertyList::wrap_under_get_rule(*item) };
let Some(addr_str) = property_list.downcast::<CFString>() else {
continue;
};
use std::io::{Error, ErrorKind::InvalidData};
let ip = IpAddr::from_str(addr_str.to_string().as_str()).map_err(|err| Error::new(InvalidData, err))?;
vec.push(ip);
}
Ok(Some(vec))
}
fn remove_dns_servers(_service_id: &str, _friendly_name: &str) -> std::io::Result<()> {
if !_friendly_name.is_empty() {
run_command("networksetup", &["-setdnsservers", _friendly_name, "Empty"])?;
}
Ok(())
}
pub(crate) fn flush_dns_cache() -> std::io::Result<()> {
let ver = run_command("sw_vers", &["-productVersion"])?;
let ver = String::from_utf8_lossy(&ver).into_owned();
if crate::compare_version(&ver, "10.12") >= 0 {
if let Err(e) = run_command("killall", &["-HUP", "mDNSResponder"]) {
log::debug!("Failed to flush DNS cache: {e}");
}
} else {
}
Ok(())
}