fix: bracketed IPv6, localhost SAN, split host-check helpers
- is_doh_host split into strip_port + is_loopback_host + is_tld_match - strip_port handles bracketed IPv6 ([::1]:443) and rejects bare IPv6 - Add [::1] to accepted loopback hosts, add localhost DNS SAN to cert - Remove dead sans.is_empty() guard (loopback IPs always present)
This commit is contained in:
37
src/doh.rs
37
src/doh.rs
@@ -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"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user