fix: DoH endpoint accepts loopback, TLS cert includes IP SANs #88

Merged
razvandimescu merged 4 commits from fix/doh-loopback-san into main 2026-04-13 05:03:31 +08:00
2 changed files with 34 additions and 16 deletions
Showing only changes of commit 115a55b199 - Show all commits

View File

@@ -53,21 +53,39 @@ fn is_doh_host(host: Option<&str>, tld: &str) -> bool {
Some(h) => h, Some(h) => h,
None => return false, None => return false,
}; };
is_doh_name(h, tld) let base = strip_port(h).unwrap_or(h);
|| h.rsplit_once(':').is_some_and(|(base, port)| { is_loopback_host(base) || is_tld_match(base, tld)
port.bytes().all(|b| b.is_ascii_digit()) && is_doh_name(base, tld)
})
} }
fn is_doh_name(h: &str, tld: &str) -> bool { fn strip_port(h: &str) -> Option<&str> {
if h.starts_with('[') {
// [::1]:443 → [::1]
let (base, port) = h.rsplit_once("]:")?;
port.bytes()
.all(|b| b.is_ascii_digit())
.then(|| &h[..base.len() + 1])
} else {
let (base, port) = h.rsplit_once(':')?;
// Bare IPv6 like "::1" has multiple colons — not a port suffix
if base.contains(':') {
return None;
}
port.bytes()
.all(|b| b.is_ascii_digit())
.then_some(base)
}
}
fn is_loopback_host(h: &str) -> bool {
matches!(h, "127.0.0.1" | "::1" | "[::1]" | "localhost")
}
fn is_tld_match(h: &str, tld: &str) -> bool {
h == tld h == tld
|| (h.len() == 2 * tld.len() + 1 || (h.len() == 2 * tld.len() + 1
&& h.starts_with(tld) && h.starts_with(tld)
&& h.as_bytes().get(tld.len()) == Some(&b'.') && h.as_bytes().get(tld.len()) == Some(&b'.')
&& h.ends_with(tld)) && h.ends_with(tld))
|| h == "127.0.0.1"
|| h == "::1"
|| h == "localhost"
} }
async fn resolve_doh( async fn resolve_doh(
@@ -160,7 +178,10 @@ mod tests {
assert!(is_doh_host(Some("127.0.0.1"), "numa")); assert!(is_doh_host(Some("127.0.0.1"), "numa"));
assert!(is_doh_host(Some("127.0.0.1:443"), "numa")); assert!(is_doh_host(Some("127.0.0.1:443"), "numa"));
assert!(is_doh_host(Some("::1"), "numa")); assert!(is_doh_host(Some("::1"), "numa"));
assert!(is_doh_host(Some("[::1]"), "numa"));
assert!(is_doh_host(Some("[::1]:443"), "numa"));
assert!(is_doh_host(Some("localhost"), "numa")); assert!(is_doh_host(Some("localhost"), "numa"));
assert!(is_doh_host(Some("localhost:443"), "numa"));
assert!(!is_doh_host(Some("foo.numa"), "numa")); assert!(!is_doh_host(Some("foo.numa"), "numa"));
assert!(!is_doh_host(None, "numa")); assert!(!is_doh_host(None, "numa"));
} }

View File

@@ -194,14 +194,11 @@ fn generate_service_cert(
std::net::Ipv6Addr::LOCALHOST, std::net::Ipv6Addr::LOCALHOST,
))); )));
// Bare TLD (e.g. "numa") for DoH via https://numa/dns-query for name in ["localhost", tld] {
match tld.to_string().try_into() { match name.to_string().try_into() {
Ok(ia5) => sans.push(SanType::DnsName(ia5)), Ok(ia5) => sans.push(SanType::DnsName(ia5)),
Err(e) => warn!("invalid SAN {}: {}", tld, e), Err(e) => warn!("invalid SAN {}: {}", name, e),
} }
if sans.is_empty() {
return Err("no valid service names for TLS cert".into());
} }
params.subject_alt_names = sans; params.subject_alt_names = sans;