use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::Arc; use tokio::sync::{RwLock, Mutex}; use simple_dns::{Packet, rdata::RData, ResourceRecord, CLASS, TYPE, QTYPE}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DnsConfig { /// Включить полный DNS: кастомные домены + AdBlock списки + DoH форвардинг pub enabled: bool, /// Перехватывать весь UDP-трафик к порту :53 и резолвить через DoH, /// даже если `enabled = false`. Это предотвращает DNS-утечки через сервер. #[serde(default)] pub intercept_all_port53: bool, /// Порт на котором встроенный DNS-сервер слушает UDP-запросы (по умолчанию 50053). /// Клиенты могут указать :50053 в качестве DNS-сервера. #[serde(default = "default_dns_local_port")] pub local_port: u16, pub doh_upstream: String, pub adblock_urls: Vec, pub custom_domains: HashMap, } fn default_dns_local_port() -> u16 { 50053 } impl Default for DnsConfig { fn default() -> Self { Self { enabled: false, intercept_all_port53: false, local_port: 50053, doh_upstream: "https://cloudflare-dns.com/dns-query".to_string(), adblock_urls: vec![], custom_domains: HashMap::new(), } } } #[derive(Debug, Clone, Serialize)] pub struct DnsQueryLog { pub timestamp: u64, pub domain: String, pub client_ip: String, pub blocked: bool, } pub struct DnsServer { pub config: RwLock, adblock_trie: RwLock>, query_log: Mutex>, reqwest_client: RwLock, } impl DnsServer { pub fn new(config: DnsConfig) -> Arc { let server = Arc::new(Self { config: RwLock::new(config.clone()), adblock_trie: RwLock::new(HashSet::new()), query_log: Mutex::new(VecDeque::with_capacity(1000)), reqwest_client: RwLock::new(reqwest::Client::builder().build().unwrap_or_default()), }); // Загружаем блок-листы при старте если DNS включён if config.enabled && !config.adblock_urls.is_empty() { let server_clone = server.clone(); tokio::spawn(async move { server_clone.update_blocklists().await; }); } server } pub async fn update_proxy(&self, outbound: Option<&crate::outbound::OutboundConfig>) { let mut builder = reqwest::Client::builder(); if let Some(outbound) = outbound { if outbound.enabled { // Determine if DoH upstream domain matches any proxy rules // We simplify by just setting the proxy for the client if outbound is globally enabled // But we should check if the DoH URL domain matches Proxy. // Since DoH usually goes to 1.1.1.1 or cloudflare-dns.com, if proxy is enabled, we route it. // Better: just route if proxy is enabled and protocol is socks5/http. let proxy_url = match outbound.protocol.as_str() { "socks5" => Some(format!("socks5h://{}:{}", outbound.address, outbound.port)), "http" => Some(format!("http://{}:{}", outbound.address, outbound.port)), _ => None, }; if let Some(url) = proxy_url { if let Ok(proxy) = reqwest::Proxy::all(&url) { builder = builder.proxy(proxy); } } } } if let Ok(client) = builder.build() { *self.reqwest_client.write().await = client; } } /// Скачать и обновить все AdBlock-листы. pub async fn update_blocklists(&self) { let urls = { let cfg = self.config.read().await; cfg.adblock_urls.clone() }; let mut new_blocked = HashSet::new(); for url in &urls { tracing::info!("DNS: downloading AdBlock list from {url}"); let client = self.reqwest_client.read().await.clone(); match client.get(url).send().await { Ok(resp) => { match resp.text().await { Ok(text) => { for line in text.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') || line.starts_with('!') { continue; } // Формат hosts: "0.0.0.0 ads.google.com" или просто "ads.google.com" // Формат adblock: "||ads.google.com^" или "ads.google.com" let domain = if line.starts_with("||") && line.ends_with('^') { line.trim_start_matches("||").trim_end_matches('^') } else { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 2 && (parts[0] == "0.0.0.0" || parts[0] == "127.0.0.1") { parts[1] } else if parts.len() == 1 { parts[0] } else { continue; } }; // Пропускаем localhost и wildcard-мусор if domain == "localhost" || domain.contains('*') || domain.contains(' ') { continue; } new_blocked.insert(domain.to_lowercase()); } } Err(e) => tracing::warn!("DNS: failed to read AdBlock list {url}: {e}"), } } Err(e) => tracing::warn!("DNS: failed to fetch AdBlock list {url}: {e}"), } } tracing::info!("DNS: loaded {} domains into AdBlock engine from {} lists", new_blocked.len(), urls.len()); *self.adblock_trie.write().await = new_blocked; } /// Резолвить DNS-запрос. /// /// Поведение зависит от конфигурации: /// - `enabled=true`: кастомные домены → AdBlock → DoH /// - `intercept_all_port53=true`: минуя AdBlock/custom, всегда форвардит через DoH /// - оба `false`: возвращает `None` (трафик идёт напрямую к целевому DNS-серверу) pub async fn resolve(&self, payload: &[u8], client_ip: std::net::IpAddr) -> Option> { let cfg = self.config.read().await; // Если оба флага выключены — не вмешиваемся if !cfg.enabled && !cfg.intercept_all_port53 { return None; } let enabled = cfg.enabled; let intercept = cfg.intercept_all_port53; let doh_url = cfg.doh_upstream.clone(); drop(cfg); // Освобождаем блокировку до IO // Парсим DNS-пакет let packet = match Packet::parse(payload) { Ok(p) => p, Err(_) => return None, }; if packet.questions.is_empty() { return None; } let question = &packet.questions[0]; let qname = question.qname.to_string().to_lowercase(); // ── Полный DNS-режим (enabled=true) ─────────────────────────────────── if enabled { // 1. Кастомные домены (прямой ответ из конфига) { let cfg = self.config.read().await; if let Some(ip_str) = cfg.custom_domains.get(&qname) { if let Ok(ip) = ip_str.parse::() { if question.qtype == QTYPE::TYPE(TYPE::A) { let mut response = Packet::new_reply(packet.id()); response.questions.push(question.clone()); response.answers.push(ResourceRecord::new( question.qname.clone(), CLASS::IN, 60, RData::A(ip.into()), )); return response.build_bytes_vec().ok(); } } } } // 2. AdBlock (suffix matching) let blocked = { let blocked_domains = self.adblock_trie.read().await; let mut parts: Vec<&str> = qname.split('.').collect(); let mut is_blocked = false; while !parts.is_empty() { let suffix = parts.join("."); if blocked_domains.contains(&suffix) { is_blocked = true; break; } parts.remove(0); } is_blocked }; if blocked { // Возвращаем пустой NXDOMAIN-ответ let mut response = Packet::new_reply(packet.id()); response.questions.push(question.clone()); tracing::debug!("DNS AdBlock: blocked {qname} for {client_ip}"); return response.build_bytes_vec().ok(); } } // ── Форвардинг через DoH ────────────────────────────────────────────── // Работает и при enabled=true и при intercept_all_port53=true tracing::debug!("DNS: resolving {qname} via DoH for {client_ip}"); let client = self.reqwest_client.read().await.clone(); match client .post(&doh_url) .header("Content-Type", "application/dns-message") .header("Accept", "application/dns-message") .body(payload.to_vec()) .timeout(std::time::Duration::from_secs(5)) .send() .await { Ok(resp) if resp.status().is_success() => { if let Ok(bytes) = resp.bytes().await { return Some(bytes.to_vec()); } } Ok(resp) => { tracing::warn!("DNS DoH upstream returned {}: {qname}", resp.status()); } Err(e) => { tracing::warn!("DNS DoH upstream error for {qname}: {e}"); } } // Если DoH упал и мы в режиме перехвата — возвращаем SERVFAIL // чтобы не пустить запрос напрямую к 8.8.8.8 с IP сервера if intercept && !enabled { let mut response = Packet::new_reply(packet.id()); response.questions.push(question.clone()); // Устанавливаем RCODE=2 (SERVFAIL) вручную в raw байтах if let Ok(mut bytes) = response.build_bytes_vec() { if bytes.len() >= 4 { bytes[3] = (bytes[3] & 0xF0) | 0x02; // RCODE=SERVFAIL } return Some(bytes); } } None } /// Запустить встроенный UDP DNS-сервер на порту `config.local_port`. /// /// Клиент может явно указать `:` как DNS-сервер /// в настройках — тогда все DNS-запросы туннелируются и резолвятся здесь. pub async fn run_local_udp_listener(self: Arc) { let port = self.config.read().await.local_port; let bind_addr = format!("0.0.0.0:{port}"); let socket = match tokio::net::UdpSocket::bind(&bind_addr).await { Ok(s) => Arc::new(s), Err(e) => { tracing::error!("Built-in DNS server failed to bind on {bind_addr}: {e}"); return; } }; tracing::info!("Built-in DNS server listening on UDP {bind_addr}"); let mut buf = vec![0u8; 4096]; loop { match socket.recv_from(&mut buf).await { Ok((n, peer)) => { let query = buf[..n].to_vec(); let srv = self.clone(); let sock = socket.clone(); let client_ip = peer.ip(); tokio::spawn(async move { if let Some(response) = srv.resolve(&query, client_ip).await { let _ = sock.send_to(&response, peer).await; } }); } Err(e) => { tracing::warn!("Built-in DNS listener recv error: {e}"); } } } } #[allow(dead_code)] async fn log_query(&self, domain: String, client_ip: String, blocked: bool) { let mut log = self.query_log.lock().await; if log.len() >= 1000 { log.pop_front(); } log.push_back(DnsQueryLog { timestamp: std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(), domain, client_ip, blocked, }); } pub async fn get_queries(&self) -> Vec { let log = self.query_log.lock().await; log.iter().cloned().collect() } }