1 Commits

Author SHA1 Message Date
Razvan Dimescu
25ebdb311f refactor(bootstrap): BTreeMap for overrides + simplify review
- Switch overrides from HashMap to BTreeMap — deterministic iteration by
  type, drops the manual sort when logging.
- Rename the flat_map closure's inner `ips` to `addrs` to stop shadowing
  the outer Vec<String>.
- Trim the Suite 8 TEST-NET-1 comment to keep the "why" and drop
  mechanism narration.
- Drop a redundant sleep 1 after wait — wait already blocks on exit.
2026-04-21 18:06:22 +03:00
4 changed files with 16 additions and 19 deletions

View File

@@ -13,7 +13,7 @@
//! servers, with TCP fallback on UDP timeout (for networks that block //! servers, with TCP fallback on UDP timeout (for networks that block
//! outbound UDP:53 — see memory: `project_network_udp_hostile.md`). //! outbound UDP:53 — see memory: `project_network_udp_hostile.md`).
use std::collections::HashMap; use std::collections::BTreeMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration; use std::time::Duration;
@@ -34,7 +34,7 @@ const DEFAULT_BOOTSTRAP: &[SocketAddr] = &[
pub struct NumaResolver { pub struct NumaResolver {
bootstrap: Vec<SocketAddr>, bootstrap: Vec<SocketAddr>,
overrides: HashMap<String, Vec<IpAddr>>, overrides: BTreeMap<String, Vec<IpAddr>>,
} }
impl NumaResolver { impl NumaResolver {
@@ -44,7 +44,7 @@ impl NumaResolver {
/// `fallback` entries are filtered to IP literals only — hostnames would /// `fallback` entries are filtered to IP literals only — hostnames would
/// re-introduce the self-loop inside the resolver itself. Empty or /// re-introduce the self-loop inside the resolver itself. Empty or
/// unusable fallback yields the hardcoded default (Quad9 + Cloudflare). /// unusable fallback yields the hardcoded default (Quad9 + Cloudflare).
pub fn new(fallback: &[String], overrides: HashMap<String, Vec<IpAddr>>) -> Self { pub fn new(fallback: &[String], overrides: BTreeMap<String, Vec<IpAddr>>) -> Self {
let mut bootstrap: Vec<SocketAddr> = Vec::with_capacity(fallback.len()); let mut bootstrap: Vec<SocketAddr> = Vec::with_capacity(fallback.len());
for entry in fallback { for entry in fallback {
match crate::forward::parse_upstream_addr(entry, 53) { match crate::forward::parse_upstream_addr(entry, 53) {
@@ -71,11 +71,10 @@ impl NumaResolver {
source source
); );
if !overrides.is_empty() { if !overrides.is_empty() {
let mut pairs: Vec<String> = overrides let pairs: Vec<String> = overrides
.iter() .iter()
.flat_map(|(host, ips)| ips.iter().map(move |ip| format!("{}={}", host, ip))) .flat_map(|(host, addrs)| addrs.iter().map(move |ip| format!("{}={}", host, ip)))
.collect(); .collect();
pairs.sort();
info!( info!(
"bootstrap resolver: host overrides (skip DNS, connect direct): {}", "bootstrap resolver: host overrides (skip DNS, connect direct): {}",
pairs.join(", ") pairs.join(", ")
@@ -185,7 +184,7 @@ mod tests {
#[test] #[test]
fn empty_fallback_uses_defaults() { fn empty_fallback_uses_defaults() {
let r = NumaResolver::new(&[], HashMap::new()); let r = NumaResolver::new(&[], BTreeMap::new());
let got: Vec<String> = r.bootstrap().iter().map(|s| s.to_string()).collect(); let got: Vec<String> = r.bootstrap().iter().map(|s| s.to_string()).collect();
assert_eq!(got, vec!["9.9.9.9:53", "1.1.1.1:53"]); assert_eq!(got, vec!["9.9.9.9:53", "1.1.1.1:53"]);
} }
@@ -197,14 +196,14 @@ mod tests {
"dns.quad9.net".to_string(), "dns.quad9.net".to_string(),
"1.1.1.1:5353".to_string(), "1.1.1.1:5353".to_string(),
]; ];
let r = NumaResolver::new(&fallback, HashMap::new()); let r = NumaResolver::new(&fallback, BTreeMap::new());
let got: Vec<String> = r.bootstrap().iter().map(|s| s.to_string()).collect(); let got: Vec<String> = r.bootstrap().iter().map(|s| s.to_string()).collect();
assert_eq!(got, vec!["9.9.9.9:53", "1.1.1.1:5353"]); assert_eq!(got, vec!["9.9.9.9:53", "1.1.1.1:5353"]);
} }
#[test] #[test]
fn override_returns_configured_ips_without_dns() { fn override_returns_configured_ips_without_dns() {
let mut overrides = HashMap::new(); let mut overrides = BTreeMap::new();
overrides.insert( overrides.insert(
"odoh-relay.example".to_string(), "odoh-relay.example".to_string(),
vec![IpAddr::V4(Ipv4Addr::new(178, 104, 229, 30))], vec![IpAddr::V4(Ipv4Addr::new(178, 104, 229, 30))],
@@ -220,7 +219,7 @@ mod tests {
#[test] #[test]
fn override_supports_multiple_ips_including_ipv6() { fn override_supports_multiple_ips_including_ipv6() {
let mut overrides = HashMap::new(); let mut overrides = BTreeMap::new();
overrides.insert( overrides.insert(
"dual.example".to_string(), "dual.example".to_string(),
vec![ vec![

View File

@@ -245,8 +245,8 @@ impl OdohUpstream {
/// Per-host IP overrides for the bootstrap resolver, lifted from /// Per-host IP overrides for the bootstrap resolver, lifted from
/// `relay_ip`/`target_ip`. Keeps the "zero plain-DNS leak for ODoH /// `relay_ip`/`target_ip`. Keeps the "zero plain-DNS leak for ODoH
/// endpoints" property when numa is its own system resolver. /// endpoints" property when numa is its own system resolver.
pub fn host_ip_overrides(&self) -> std::collections::HashMap<String, Vec<std::net::IpAddr>> { pub fn host_ip_overrides(&self) -> std::collections::BTreeMap<String, Vec<std::net::IpAddr>> {
let mut out = std::collections::HashMap::new(); let mut out = std::collections::BTreeMap::new();
if let Some(addr) = self.relay_bootstrap { if let Some(addr) = self.relay_bootstrap {
out.entry(self.relay_host.clone()) out.entry(self.relay_host.clone())
.or_insert_with(Vec::new) .or_insert_with(Vec::new)

View File

@@ -59,7 +59,7 @@ pub async fn run(config_path: String) -> crate::Result<()> {
.odoh_upstream() .odoh_upstream()
.map(|o| o.host_ip_overrides()) .map(|o| o.host_ip_overrides())
.unwrap_or_default(), .unwrap_or_default(),
_ => std::collections::HashMap::new(), _ => std::collections::BTreeMap::new(),
}; };
let bootstrap_resolver: Arc<NumaResolver> = Arc::new(NumaResolver::new( let bootstrap_resolver: Arc<NumaResolver> = Arc::new(NumaResolver::new(
&config.upstream.fallback, &config.upstream.fallback,

View File

@@ -975,11 +975,10 @@ check "Same-host relay+target rejected at startup" \
"same host" \ "same host" \
"$STARTUP_OUT" "$STARTUP_OUT"
# relay_ip / target_ip must land in the bootstrap resolver's override map, # Guards ODoH's zero-plain-DNS-leak property: relay_ip / target_ip must
# so reqwest connects direct to the configured IPs instead of resolving the # land in the bootstrap resolver's override map so reqwest connects direct
# hostnames via plain DNS (ODoH's zero-plain-DNS-leak property). Using # to the configured IPs instead of resolving the hostnames via plain DNS.
# RFC 5737 TEST-NET-1 IPs — never routable, so the OdohConfigCache won't # RFC 5737 TEST-NET-1 IPs (unroutable).
# actually connect, but the override-map wiring is visible in the startup log.
cat > "$CONFIG" << 'CONF' cat > "$CONFIG" << 'CONF'
[server] [server]
bind_addr = "127.0.0.1:5354" bind_addr = "127.0.0.1:5354"
@@ -1019,7 +1018,6 @@ check "target_ip wired into bootstrap override map" \
kill "$NUMA_PID" 2>/dev/null || true kill "$NUMA_PID" 2>/dev/null || true
wait "$NUMA_PID" 2>/dev/null || true wait "$NUMA_PID" 2>/dev/null || true
sleep 1
fi # end Suite 8 fi # end Suite 8