AI FULL KILLSWITCH AND EXCEPTIONS

This commit is contained in:
2026-04-30 14:28:15 +07:00
parent f0b6bf423e
commit 9e29104828
6 changed files with 196 additions and 30 deletions
+8 -1
View File
@@ -75,6 +75,9 @@ impl SessionManager {
remote_address: String,
tun_fd: Option<i32>,
cache_dir: String,
killswitch_enabled: bool,
excluded_apps: Vec<String>,
excluded_domains: Vec<String>,
) -> Arc<Session> {
netrunner_logger::Logger::init(None);
netrunner_logger::Logger::global().set_level("info");
@@ -86,7 +89,11 @@ impl SessionManager {
let addr: std::net::SocketAddr = remote_address.parse().expect("Invalid address format");
let remote_proxy_ip = addr.ip().to_string();
let mut config = EngineConfig::new(&remote_address).with_cache_path(&cache_dir);
let mut config = EngineConfig::new(&remote_address)
.with_cache_path(&cache_dir)
.with_killswitch(killswitch_enabled)
.with_excluded_apps(excluded_apps)
.with_excluded_domains(excluded_domains);
#[cfg(any(target_os = "android", target_os = "ios"))]
{
+10 -1
View File
@@ -63,10 +63,11 @@ pub struct DnsHandler {
block_list: HashSet<String>,
forbidden_suffixes: Vec<String>,
cache_path: String,
excluded_domains: HashSet<String>, // Добавлено
}
impl DnsHandler {
pub fn new(cache_dir: &str) -> Self {
pub fn new(cache_dir: &str, excluded: Vec<String>) -> Self {
Self {
block_list: HashSet::new(),
forbidden_suffixes: vec![".lan", ".local", ".home", ".arpa"]
@@ -74,6 +75,7 @@ impl DnsHandler {
.map(String::from)
.collect(),
cache_path: format!("{}/hosts_cache.txt", cache_dir),
excluded_domains: excluded.into_iter().map(|d| d.to_lowercase()).collect(),
}
}
@@ -162,6 +164,13 @@ impl DnsHandler {
.set_recursion_available(true)
.add_query(query.clone());
if self.excluded_domains.iter().any(|ext| name.ends_with(ext)) {
netrunner_logger::info!("Bypassing DNS for excluded domain: {}", name);
// Отвечаем ServFail (или Refused). Это заставит ОС сделать фолбэк на реальный DNS провайдера.
res.set_response_code(ResponseCode::ServFail);
return res.to_vec().ok();
}
if self.forbidden_suffixes.iter().any(|s| name.ends_with(s))
|| self.block_list.contains(&name)
{
+83 -7
View File
@@ -236,6 +236,10 @@ pub struct EngineConfig {
pub any_ip: bool,
pub transparent_mode: bool,
pub default_gateway: Ipv4Addr,
// Новые поля
pub killswitch_enabled: bool,
pub excluded_apps: Vec<String>,
pub excluded_domains: Vec<String>,
}
impl EngineConfig {
@@ -248,6 +252,9 @@ impl EngineConfig {
any_ip: true,
transparent_mode: true,
default_gateway: Ipv4Addr::new(10, 0, 0, 2),
killswitch_enabled: true,
excluded_apps: Vec::new(),
excluded_domains: Vec::new(),
}
}
@@ -265,6 +272,21 @@ impl EngineConfig {
self.setup_routing = false;
self
}
pub fn with_killswitch(mut self, enabled: bool) -> Self {
self.killswitch_enabled = enabled;
self
}
pub fn with_excluded_apps(mut self, apps: Vec<String>) -> Self {
self.excluded_apps = apps;
self
}
pub fn with_excluded_domains(mut self, domains: Vec<String>) -> Self {
self.excluded_domains = domains;
self
}
}
pub struct EngineBuilder {
@@ -287,20 +309,35 @@ impl EngineBuilder {
self
}
// client/src/net/engine.rs
pub async fn build(self) -> Result<(Engine, Tun), String> {
let tun = self.tun_device.ok_or("TUN device is required")?;
info!("Initializing Engine with config: {:?}", self.config);
let mut dns_handler = DnsHandler::new(&self.config.cache_path);
// 1. Обновляем DnsHandler: теперь он знает про список исключенных доменов
let mut dns_handler = DnsHandler::new(
&self.config.cache_path,
self.config.excluded_domains.clone(),
);
if let Err(e) = dns_handler.init().await {
error!("Failed to initialize DNS blocklist: {}", e);
}
// 2. Обновляем настройку роутинга: передаем флаг Killswitch и список приложений
if self.config.setup_routing {
info!("Applying platform routing rules...");
setup_platform_routing(&self.config.remote_address)
.map_err(|e| format!("Routing setup failed: {}", e))?;
info!(
"Applying platform routing rules (Killswitch: {})...",
self.config.killswitch_enabled
);
setup_platform_routing(
&self.config.remote_address,
self.config.killswitch_enabled, // Новый параметр
&self.config.excluded_apps, // Новый параметр
)
.map_err(|e| format!("Routing setup failed: {}", e))?;
}
let smol_config = Config::new(smoltcp::wire::HardwareAddress::Ip);
@@ -308,7 +345,6 @@ impl EngineBuilder {
caps.max_transmission_unit = self.config.mtu;
caps.medium = smoltcp::phy::Medium::Ip;
// 🔥 Используем Bounded каналы для Tunnel <-> Engine
let cap = NetworkConfig::global().channel_capacity;
let (tx_to_tunnel, rx_for_client_handler) = mpsc::channel::<RawCastFrame>(cap);
let (tx_for_client_handler, rx_from_tunnel) = mpsc::channel::<RawCastFrame>(cap);
@@ -328,6 +364,46 @@ impl EngineBuilder {
Arc::new(SmolSocketFactory::new(config))
});
// 3. Динамический резолвинг исключенных доменов.
// Чтобы Split-Tunneling работал, ОС должна знать, что к реальным IP этих доменов
// нужно идти через физический шлюз, а не через TUN.
let excluded_domains = self.config.excluded_domains.clone();
if !excluded_domains.is_empty() {
tokio::spawn(async move {
// Определяем физический шлюз (например, 192.168.1.1)
#[cfg(target_os = "linux")]
let phys_gw = crate::tun::routing::get_default_gateway_linux()
.unwrap_or_else(|| "192.168.1.1".into());
#[cfg(not(target_os = "linux"))]
let phys_gw = "192.168.1.1";
for domain in excluded_domains {
// Резолвим домен средствами ОС (через системный DNS, не наш)
if let Ok(addrs) = tokio::net::lookup_host(format!("{}:443", domain)).await {
for addr in addrs {
if let std::net::IpAddr::V4(ipv4) = addr.ip() {
debug!(
"Adding exception route for domain {} -> IP {}",
domain, ipv4
);
// Прописываем маршрут в ОС мимо туннеля
#[cfg(target_os = "linux")]
let _ = crate::tun::routing::run_cmd_ext(
&format!("ip route add {} via {}", ipv4, phys_gw),
true,
);
#[cfg(target_os = "windows")]
let _ = crate::tun::routing::run_cmd_ext(
&format!("route add {} mask 255.255.255.255 {}", ipv4, phys_gw),
true,
);
}
}
}
}
});
}
let mut engine = Engine::new(
smol_config,
caps,
@@ -346,8 +422,8 @@ impl EngineBuilder {
engine.activate();
info!(
"Engine successfully built. Stack IP: {}",
self.config.default_gateway
"Engine successfully built. Stack IP: {} | Killswitch: {}",
self.config.default_gateway, self.config.killswitch_enabled
);
Ok((engine, tun))
+9 -2
View File
@@ -9,8 +9,15 @@ dictionary VpnTrafficStats {
interface SessionManager {
constructor();
Session spawn_session(string remote_address, i32? tun_fd, string cache_path);
// Добавлены параметры для Killswitch и исключений
Session spawn_session(
string remote_address,
i32? tun_fd,
string cache_path,
bool killswitch_enabled,
sequence<string> excluded_apps,
sequence<string> excluded_domains
);
VpnTrafficStats get_traffic_stats();
};
+84 -18
View File
@@ -54,7 +54,21 @@ fn get_default_gateway() -> Option<String> {
.map(|s| s.to_string())
}
pub fn setup_platform_routing(remote_address: &str) -> io::Result<()> {
#[cfg(target_os = "linux")]
fn get_default_gateway_linux() -> Option<String> {
let output = Command::new("ip")
.args(["route", "show", "default"])
.output()
.ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.split_whitespace().nth(2).map(|s| s.to_string())
}
pub fn setup_platform_routing(
remote_address: &str,
killswitch: bool,
excluded_apps: &[String],
) -> io::Result<()> {
let proxy_ip = remote_address.split(':').next().unwrap_or(remote_address);
#[cfg(target_os = "linux")]
@@ -79,6 +93,52 @@ pub fn setup_platform_routing(remote_address: &str) -> io::Result<()> {
false,
)?;
// 1. Исключения для приложений (Split-Tunneling)
// Ожидается, что для Linux в excluded_apps передаются UID пользователей
for uid_str in excluded_apps {
if let Ok(uid) = uid_str.parse::<u32>() {
netrunner_logger::info!("Bypassing TUN for UID: {}", uid);
// Пропускаем трафик этого UID мимо нашего роутинга
run_cmd_ext(
&format!("nft add rule ip netrunner output meta skuid {} accept", uid),
false,
)?;
}
}
// 2. Базовая маркировка трафика для отправки в TUN
let mark_rule = format!(
"nft add rule ip netrunner output ip daddr != {} oifname != \"netr0\" mark set 0x1",
proxy_ip
);
run_cmd_ext(&mark_rule, false)?;
// 3. KILLSWITCH
if killswitch {
netrunner_logger::info!("🔒 Killswitch ENABLED (Linux)");
// Исключения для локальной сети (крайне важно для сохранения доступа к роутеру)
let lan_bypass = "nft add rule ip netrunner output ip daddr { 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } accept";
run_cmd_ext(lan_bypass, false)?;
// Разрешаем трафик до самого прокси-сервера
run_cmd_ext(
&format!(
"nft add rule ip netrunner output ip daddr {} accept",
proxy_ip
),
false,
)?;
// Разрешаем трафик, который УЖЕ внутри туннеля
run_cmd_ext(
"nft add rule ip netrunner output oifname \"netr0\" accept",
false,
)?;
// Блокируем всё остальное (Утечки)
run_cmd_ext("nft add rule ip netrunner output drop", false)?;
}
let mark_rule = format!(
"nft add rule ip netrunner output ip daddr != {} oifname != \"netr0\" mark set 0x1",
proxy_ip
@@ -100,23 +160,19 @@ pub fn setup_platform_routing(remote_address: &str) -> io::Result<()> {
#[cfg(target_os = "windows")]
{
let gateway = get_default_gateway().unwrap_or_else(|| "192.168.1.1".to_string());
let _ = run_cmd_ext(
"netsh interface ipv4 set address name=\"netr0\" static 10.0.0.1 255.255.255.0 none",
true,
);
let tun_idx = get_adapter_index("netr0")
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Interface netr0 not found"))?;
let _ = run_cmd_ext(
// Сохраняем доступ к прокси через физический шлюз
run_cmd_ext(
&format!(
"route add {} mask 255.255.255.255 {} metric 1",
proxy_ip, gateway
),
true,
);
)?;
// Направляем весь трафик в TUN
run_cmd_ext(
&format!(
"route add 0.0.0.0 mask 128.0.0.0 10.0.0.2 if {} metric 5",
@@ -132,15 +188,16 @@ pub fn setup_platform_routing(remote_address: &str) -> io::Result<()> {
true,
)?;
let _ = run_cmd_ext(
"netsh interface ipv4 set dnsservers name=\"netr0\" static 10.0.0.2 primary",
true,
);
info!(
"Windows: Routing configured on idx {} via 10.0.0.2",
tun_idx
);
// KILLSWITCH: Удаляем дефолтный физический маршрут
if killswitch {
netrunner_logger::info!(
"🔒 Killswitch ENABLED (Windows). Deleting default physical route."
);
run_cmd_ext(
&format!("route delete 0.0.0.0 mask 0.0.0.0 {}", gateway),
true,
)?;
}
}
#[cfg(any(target_os = "android", target_os = "ios"))]
@@ -174,6 +231,15 @@ pub fn reset_platform_routing(_proxy_ip: Option<&str>) -> io::Result<()> {
true,
);
if was_killswitch {
netrunner_logger::info!(
"Restoring physical default route (DHCP renew needed or manual restore)"
);
// Windows не всегда сама возвращает default route после `route delete`.
// Оптимальный хак: дернуть DHCP.
let _ = run_cmd_ext("ipconfig /renew", true);
}
info!("Windows routing reset complete.");
}
#[cfg(any(target_os = "android", target_os = "ios"))]