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

View File

@@ -18,6 +18,8 @@ pub struct Config {
#[serde(default)]
pub cache: CacheConfig,
#[serde(default)]
pub blocking: BlockingConfig,
#[serde(default)]
pub zones: Vec<ZoneRecord>,
}
@@ -25,12 +27,15 @@ pub struct Config {
pub struct ServerConfig {
#[serde(default = "default_bind_addr")]
pub bind_addr: String,
#[serde(default = "default_api_port")]
pub api_port: u16,
}
impl Default for ServerConfig {
fn default() -> Self {
ServerConfig {
bind_addr: default_bind_addr(),
api_port: default_api_port(),
}
}
}
@@ -39,6 +44,10 @@ fn default_bind_addr() -> String {
"0.0.0.0:53".to_string()
}
fn default_api_port() -> u16 {
5380
}
#[derive(Deserialize)]
pub struct UpstreamConfig {
#[serde(default = "default_upstream_addr")]
@@ -108,6 +117,41 @@ pub struct ZoneRecord {
pub ttl: u32,
}
#[derive(Deserialize)]
pub struct BlockingConfig {
#[serde(default = "default_blocking_enabled")]
pub enabled: bool,
#[serde(default = "default_blocklists")]
pub lists: Vec<String>,
#[serde(default = "default_refresh_hours")]
pub refresh_hours: u64,
#[serde(default)]
pub allowlist: Vec<String>,
}
impl Default for BlockingConfig {
fn default() -> Self {
BlockingConfig {
enabled: default_blocking_enabled(),
lists: default_blocklists(),
refresh_hours: default_refresh_hours(),
allowlist: Vec::new(),
}
}
}
fn default_blocking_enabled() -> bool {
true
}
fn default_blocklists() -> Vec<String> {
vec!["https://cdn.jsdelivr.net/gh/hagezi/dns-blocklists@latest/hosts/pro.txt".to_string()]
}
fn default_refresh_hours() -> u64 {
24
}
fn default_zone_ttl() -> u32 {
300
}
@@ -118,6 +162,7 @@ pub fn load_config(path: &str) -> Result<Config> {
server: ServerConfig::default(),
upstream: UpstreamConfig::default(),
cache: CacheConfig::default(),
blocking: BlockingConfig::default(),
zones: Vec::new(),
});
}
@@ -126,10 +171,10 @@ pub fn load_config(path: &str) -> Result<Config> {
Ok(config)
}
pub fn build_zone_map(
zones: &[ZoneRecord],
) -> Result<HashMap<(String, QueryType), Vec<DnsRecord>>> {
let mut map: HashMap<(String, QueryType), Vec<DnsRecord>> = HashMap::new();
pub type ZoneMap = HashMap<String, HashMap<QueryType, Vec<DnsRecord>>>;
pub fn build_zone_map(zones: &[ZoneRecord]) -> Result<ZoneMap> {
let mut map: ZoneMap = HashMap::new();
for zone in zones {
let domain = zone.domain.to_lowercase();
@@ -203,7 +248,11 @@ pub fn build_zone_map(
}
};
map.entry((domain, qtype)).or_default().push(record);
map.entry(domain)
.or_default()
.entry(qtype)
.or_default()
.push(record);
}
Ok(map)