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 37 additions and 10 deletions
Showing only changes of commit 3665deb56b - Show all commits

View File

@@ -49,16 +49,25 @@ pub async fn doh_post(State(state): State<super::proxy::DohState>, req: Request)
} }
fn is_doh_host(host: Option<&str>, tld: &str) -> bool { fn is_doh_host(host: Option<&str>, tld: &str) -> bool {
match host { let h = match host {
Some(h) if h == tld => true, Some(h) => h,
Some(h) => { None => return false,
h.len() == 2 * tld.len() + 1 };
&& h.starts_with(tld) is_doh_name(h, tld)
&& h.as_bytes().get(tld.len()) == Some(&b'.') || h.rsplit_once(':').is_some_and(|(base, port)| {
&& h.ends_with(tld) port.bytes().all(|b| b.is_ascii_digit()) && is_doh_name(base, tld)
} })
None => false, }
}
fn is_doh_name(h: &str, tld: &str) -> bool {
h == tld
|| (h.len() == 2 * tld.len() + 1
&& h.starts_with(tld)
&& h.as_bytes().get(tld.len()) == Some(&b'.')
&& h.ends_with(tld))
|| h == "127.0.0.1"
|| h == "::1"
|| h == "localhost"
} }
async fn resolve_doh( async fn resolve_doh(
@@ -148,6 +157,10 @@ mod tests {
fn is_doh_host_matches_tld() { fn is_doh_host_matches_tld() {
assert!(is_doh_host(Some("numa"), "numa")); assert!(is_doh_host(Some("numa"), "numa"));
assert!(is_doh_host(Some("numa.numa"), "numa")); assert!(is_doh_host(Some("numa.numa"), "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("::1"), "numa"));
assert!(is_doh_host(Some("localhost"), "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

@@ -186,6 +186,20 @@ fn generate_service_cert(
} }
} }
// Loopback IP SANs so browsers can reach DoH at https://127.0.0.1/dns-query
sans.push(SanType::IpAddress(std::net::IpAddr::V4(
std::net::Ipv4Addr::LOCALHOST,
)));
sans.push(SanType::IpAddress(std::net::IpAddr::V6(
std::net::Ipv6Addr::LOCALHOST,
)));
// Bare TLD (e.g. "numa") for DoH via https://numa/dns-query
match tld.to_string().try_into() {
Ok(ia5) => sans.push(SanType::DnsName(ia5)),
Err(e) => warn!("invalid SAN {}: {}", tld, e),
}
if sans.is_empty() { if sans.is_empty() {
return Err("no valid service names for TLS cert".into()); return Err("no valid service names for TLS cert".into());
} }