AI FULL KILLSWITCH AND EXCEPTIONS
This commit is contained in:
+8
-1
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,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
@@ -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"))]
|
||||
|
||||
Reference in New Issue
Block a user