feat(resolver): filter_aaaa for IPv4-only networks (#112)

When enabled, AAAA queries short-circuit to NODATA (NOERROR + empty
answer) so Happy Eyeballs clients don't stall waiting on a v6 address
they can't use. Also strips `ipv6hint` SvcParam from HTTPS/SVCB
answers (RFC 9460) so Chrome ≥103, Firefox, and Safari don't bypass
the AAAA filter via the HTTPS record path.

Local data is preserved: overrides, zones, the .numa proxy, and the
blocklist sinkhole keep whatever v6 addresses they configure — the
filter only kicks in on the cache/forward/recursive path. NODATA is
correct per RFC 2308 here; NXDOMAIN would incorrectly imply the name
doesn't exist for A queries either.

Off by default. Opt in via `filter_aaaa = true` under `[server]`.
This commit is contained in:
Razvan Dimescu
2026-04-18 19:52:06 +03:00
parent 34e2182ae4
commit be98a02e49
7 changed files with 363 additions and 0 deletions

View File

@@ -93,6 +93,12 @@ pub struct ServerConfig {
/// Defaults to `crate::data_dir()` (platform-specific system path) if unset.
#[serde(default)]
pub data_dir: Option<PathBuf>,
/// Synthesize NODATA (NOERROR + empty answer) for AAAA queries, and
/// strip `ipv6hint` from HTTPS/SVCB responses (RFC 9460). For IPv4-only
/// networks where Happy Eyeballs fallback adds latency. Local zones,
/// overrides, and the service proxy are not affected. Default false.
#[serde(default)]
pub filter_aaaa: bool,
}
impl Default for ServerConfig {
@@ -102,6 +108,7 @@ impl Default for ServerConfig {
api_port: default_api_port(),
api_bind_addr: default_api_bind_addr(),
data_dir: None,
filter_aaaa: false,
}
}
}
@@ -580,6 +587,17 @@ mod tests {
assert!(config.lan.enabled);
}
#[test]
fn filter_aaaa_defaults_false() {
assert!(!ServerConfig::default().filter_aaaa);
}
#[test]
fn filter_aaaa_parses_from_server_section() {
let config: Config = toml::from_str("[server]\nfilter_aaaa = true").unwrap();
assert!(config.server.filter_aaaa);
}
#[test]
fn custom_bind_addrs_parse() {
let toml = r#"