diff --git a/Cargo.lock b/Cargo.lock index 5f0e6a1..9d114b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ dependencies = [ "thread-priority", "time", "tinyfiledialogs", - "toml 0.9.12+spec-1.1.0", + "toml 1.0.7+spec-1.1.0", "tray-icon", "ureq", "uuid", @@ -3166,17 +3166,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" dependencies = [ "indexmap", "serde_core", "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.15", + "winnow 1.0.0", ] [[package]] @@ -3188,15 +3188,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.0.1+spec-1.1.0" @@ -4066,12 +4057,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - [[package]] name = "winnow" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 88159f6..643c57d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ exclude = ["blockchain.db", "alfis.toml"] getopts = "0.2.24" log = "0.4.28" simplelog = "0.12.2" -toml = "0.9.12+spec-1.1.0" +toml = "1.0.7" sha2 = "0.10.9" ed25519-dalek = "2.2.0" x25519-dalek = { version = "2.0.1", features = ["reusable_secrets"] } diff --git a/src/blockchain/filter.rs b/src/blockchain/filter.rs index 398b876..f423709 100644 --- a/src/blockchain/filter.rs +++ b/src/blockchain/filter.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; use std::net::{IpAddr, SocketAddr}; use std::sync::{Arc, Mutex}; +use std::time::Instant; #[allow(unused_imports)] use log::{debug, error, info, trace, warn}; +use rand::seq::SliceRandom; use crate::blockchain::transaction::DomainData; use crate::dns::filter::DnsFilter; @@ -13,13 +16,32 @@ use crate::dns::client::{DnsClient, DnsNetworkClient}; const NAME_SERVER: &str = "ns.alfis.name"; const SERVER_ADMIN: &str = "admin.alfis.name"; +/// Unbound-style RTT band width in milliseconds. +/// Servers within min_rtt + BAND are considered equally good. +const RTT_BAND_MS: f64 = 100.0; +/// EWMA smoothing factor: 87.5% history, 12.5% new measurement. +const EWMA_WEIGHT: f64 = 7.0 / 8.0; +/// Penalty RTT assigned on timeout/failure (ms). +const TIMEOUT_PENALTY_MS: f64 = 5000.0; +/// Stats older than this are expired so the server gets re-probed. +const STATS_EXPIRE_SECS: u64 = 900; + +struct NsStats { + rtt: f64, + last_update: Instant, +} + pub struct BlockchainFilter { - context: Arc> + context: Arc>, + ns_stats: Arc>>, } impl BlockchainFilter { pub fn new(context: Arc>) -> Self { - BlockchainFilter { context } + BlockchainFilter { + context, + ns_stats: Arc::new(Mutex::new(HashMap::new())), + } } fn add_soa_record(zone: String, serial: u32, packet: &mut DnsPacket) { @@ -44,22 +66,95 @@ impl BlockchainFilter { have_zone } - fn lookup_from_ns(qname: &str, qtype: QueryType, servers: &Vec) -> Option { + fn lookup_from_ns(qname: &str, qtype: QueryType, servers: &[IpAddr], ns_stats: &Arc>>) -> Option { let mut dns_client = DnsNetworkClient::new(); dns_client.run().unwrap(); - let timeout = std::time::Duration::from_secs(5); + let timeout = std::time::Duration::from_secs(2); - for server in servers { - let addr = SocketAddr::new(server.to_owned(), 53); - if let Ok(res) = dns_client.send_udp_query(qname, qtype, addr, false, timeout) { - dns_client.stop(); - return Some(res); + // Build ordered server list using RTT banding + let ordered = Self::select_servers(servers, ns_stats); + + for server in &ordered { + let addr = SocketAddr::new(*server, 53); + let start = Instant::now(); + match dns_client.send_udp_query(qname, qtype, addr, false, timeout) { + Ok(res) => { + let elapsed = start.elapsed().as_secs_f64() * 1000.0; + Self::update_ns_stats(ns_stats, *server, elapsed); + dns_client.stop(); + return Some(res); + } + Err(_) => { + Self::update_ns_stats(ns_stats, *server, TIMEOUT_PENALTY_MS); + } } } dns_client.stop(); None } + /// Select servers using Unbound-style RTT banding. + /// Servers with no stats or expired stats are treated as preferred (to be probed). + /// Among known servers, those within min_rtt + RTT_BAND_MS are preferred. + /// Each group is shuffled, then preferred servers come first. + fn select_servers(servers: &[IpAddr], ns_stats: &Arc>>) -> Vec { + let now = Instant::now(); + let stats = ns_stats.lock().unwrap(); + + // Separate into known (with valid stats) and unknown + let mut known: Vec<(IpAddr, f64)> = Vec::new(); + let mut unknown: Vec = Vec::new(); + for &ip in servers { + match stats.get(&ip) { + Some(s) if now.duration_since(s.last_update).as_secs() < STATS_EXPIRE_SECS => { + known.push((ip, s.rtt)); + } + _ => { + unknown.push(ip); + } + } + } + drop(stats); + + let mut rng = rand::thread_rng(); + + if known.is_empty() { + // No stats yet — shuffle all and probe + unknown.shuffle(&mut rng); + return unknown; + } + + let min_rtt = known.iter().map(|(_, rtt)| *rtt).fold(f64::INFINITY, f64::min); + let band_threshold = min_rtt + RTT_BAND_MS; + + let mut preferred: Vec = Vec::new(); + let mut fallback: Vec = Vec::new(); + for (ip, rtt) in &known { + if *rtt <= band_threshold { + preferred.push(*ip); + } else { + fallback.push(*ip); + } + } + + // Unknown servers join the preferred group to get probed + preferred.extend(unknown); + preferred.shuffle(&mut rng); + fallback.shuffle(&mut rng); + preferred.extend(fallback); + preferred + } + + fn update_ns_stats(ns_stats: &Arc>>, ip: IpAddr, rtt_ms: f64) { + let mut stats = ns_stats.lock().unwrap(); + let entry = stats.entry(ip).or_insert(NsStats { + rtt: rtt_ms, + last_update: Instant::now(), + }); + entry.rtt = entry.rtt * EWMA_WEIGHT + rtt_ms * (1.0 - EWMA_WEIGHT); + entry.last_update = Instant::now(); + } + fn create_packet(&self, qname: &str, qtype: QueryType, zone: String, answers: Vec, ns_records: Vec, glue_records: Vec) -> Option { if !answers.is_empty() { // Create DnsPacket with answers @@ -92,7 +187,7 @@ impl BlockchainFilter { } } - fn resolve_by_ns(qname: &str, qtype: QueryType, top_domain: &String, data: &DomainData, recursive: bool) -> (bool, Option) { + fn resolve_by_ns(qname: &str, qtype: QueryType, top_domain: &String, data: &DomainData, recursive: bool, ns_stats: &Arc>>) -> (bool, Option) { // First we search for NS records, collecting nameserver domains let mut hosts = Vec::new(); for record in data.records.iter() { @@ -156,7 +251,7 @@ impl BlockchainFilter { if !servers.is_empty() { trace!("Found NS servers for domain {}: {:?}", &qname, &servers); - let answer = BlockchainFilter::lookup_from_ns(qname, qtype, &servers); + let answer = BlockchainFilter::lookup_from_ns(qname, qtype, &servers, ns_stats); if let Some(packet) = &answer { trace!("Resolved {:?} from NS: {:?}", (qname, qtype), &packet.answers); } @@ -284,7 +379,7 @@ impl DnsFilter for BlockchainFilter { // Check if this domain has NS records and needs to resolve all records through them // But skip this if we're querying for NS records themselves - return them directly if qtype != QueryType::NS { - let (has_ns, result) = Self::resolve_by_ns(qname, qtype, &top_domain, &data, recursive); + let (has_ns, result) = Self::resolve_by_ns(qname, qtype, &top_domain, &data, recursive, &self.ns_stats); if has_ns { return result; } diff --git a/src/commons/constants.rs b/src/commons/constants.rs index 9dfd773..16a3df1 100644 --- a/src/commons/constants.rs +++ b/src/commons/constants.rs @@ -45,7 +45,7 @@ pub const UI_REFRESH_DELAY_MS: u128 = 500; pub const LOG_REFRESH_DELAY_SEC: u64 = 60; pub const POLL_TIMEOUT: Option = Some(Duration::from_millis(200)); -pub const WAIT_FOR_INTERNET: Duration = Duration::from_secs(10); +pub const WAIT_FOR_INTERNET: Duration = Duration::from_secs(5); /// We start syncing blocks only when we got 4 and more connected nodes pub const MIN_CONNECTED_NODES_START_SYNC: usize = 4; pub const MAX_READ_BLOCK_TIME: u128 = 100;