add ad blocking, live dashboard, system DNS auto-discovery

- DNS-level ad blocking: 385K+ domains via Hagezi Pro blocklist, subdomain
  matching, one-click allowlist, pause/toggle, background refresh every 24h
- Live dashboard at :5380 with real-time stats, query log, override
  management (create/edit/delete), blocking controls
- System DNS auto-discovery: parses scutil --dns on macOS to find
  conditional forwarding rules (Tailscale, VPN split-DNS)
- REST API expanded to 18 endpoints (blocking, overrides, diagnostics)
- Startup banner with colored system info
- Performance benchmarks (bench/dns-bench.sh)
- Landing page updated with new positioning and comparison table
- CI, Dockerfile, LICENSE, development plan docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Razvan Dimescu
2026-03-20 10:54:23 +02:00
parent e31188fb88
commit 4dc5b94c7a
23 changed files with 5494 additions and 226 deletions

77
src/query_log.rs Normal file
View File

@@ -0,0 +1,77 @@
use std::collections::VecDeque;
use std::net::SocketAddr;
use std::time::SystemTime;
use crate::header::ResultCode;
use crate::question::QueryType;
use crate::stats::QueryPath;
pub struct QueryLogEntry {
pub timestamp: SystemTime,
pub src_addr: SocketAddr,
pub domain: String,
pub query_type: QueryType,
pub path: QueryPath,
pub rescode: ResultCode,
pub latency_us: u64,
}
pub struct QueryLog {
entries: VecDeque<QueryLogEntry>,
capacity: usize,
}
impl QueryLog {
pub fn new(capacity: usize) -> Self {
QueryLog {
entries: VecDeque::with_capacity(capacity),
capacity,
}
}
pub fn push(&mut self, entry: QueryLogEntry) {
if self.entries.len() >= self.capacity {
self.entries.pop_front();
}
self.entries.push_back(entry);
}
pub fn query(&self, filter: &QueryLogFilter) -> Vec<&QueryLogEntry> {
self.entries
.iter()
.rev()
.filter(|e| {
if let Some(ref domain) = filter.domain {
if !e.domain.contains(domain.as_str()) {
return false;
}
}
if let Some(qtype) = filter.query_type {
if e.query_type != qtype {
return false;
}
}
if let Some(path) = filter.path {
if e.path != path {
return false;
}
}
if let Some(since) = filter.since {
if e.timestamp < since {
return false;
}
}
true
})
.take(filter.limit.unwrap_or(50))
.collect()
}
}
pub struct QueryLogFilter {
pub domain: Option<String>,
pub query_type: Option<QueryType>,
pub path: Option<QueryPath>,
pub since: Option<SystemTime>,
pub limit: Option<usize>,
}