Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions crates/api-db/src/dns/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,56 @@ pub mod domain;
pub mod domain_metadata;
pub mod resource_record;

use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

pub fn normalize_domain(name: &str) -> String {
let normalize_domain = name.trim_end_matches('.').to_lowercase();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use ASCII case-folding in domain normalization for DNS keys.

normalize_domain uses to_lowercase(), while PTR parsing uses to_ascii_lowercase(). For DNS/domain-key normalization, this mismatch can produce inconsistent lookup keys for non-ASCII input paths.

Suggested patch
-    let normalize_domain = name.trim_end_matches('.').to_lowercase();
+    let normalize_domain = name.trim_end_matches('.').to_ascii_lowercase();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/api-db/src/dns/mod.rs` at line 25, The normalize_domain variable uses
to_lowercase() instead of to_ascii_lowercase(), which creates an inconsistency
with how PTR parsing normalizes domains and can produce inconsistent DNS lookup
keys for non-ASCII input. Replace the to_lowercase() method call with
to_ascii_lowercase() in the normalize_domain assignment to ensure consistent
ASCII case-folding for DNS domain-key normalization.

tracing::debug!("Normalized domain name: {} to: {}", name, normalize_domain);
normalize_domain
}

/// Parse a reverse-DNS (PTR) query name into the address it points at -- the
/// inverse of the `in-addr.arpa` (IPv4) / `ip6.arpa` (IPv6) form. Returns `None`
/// for anything that is not a well-formed arpa name, so the caller answers
/// NotFound rather than guessing.
pub fn arpa_qname_to_ip(qname: &str) -> Option<IpAddr> {
let name = qname.trim_end_matches('.').to_ascii_lowercase();

if let Some(reversed) = name.strip_suffix(".in-addr.arpa") {
// Four decimal octets, least-significant label first.
let octets: Vec<&str> = reversed.split('.').collect();
if octets.len() != 4 {
return None;
}
let mut addr = [0u8; 4];
for (byte, octet) in addr.iter_mut().zip(octets.iter().rev()) {
*byte = octet.parse().ok()?;
}
Some(IpAddr::V4(Ipv4Addr::from(addr)))
} else if let Some(reversed) = name.strip_suffix(".ip6.arpa") {
// Thirty-two hex nibbles, least-significant label first.
let nibbles: Vec<&str> = reversed.split('.').collect();
if nibbles.len() != 32 {
return None;
}
let mut addr = [0u8; 16];
for (i, nibble) in nibbles.iter().rev().enumerate() {
if nibble.len() != 1 {
return None;
}
let value = u8::from_str_radix(nibble, 16).ok()?;
if i % 2 == 0 {
addr[i / 2] = value << 4;
} else {
addr[i / 2] |= value;
}
}
Some(IpAddr::V6(Ipv6Addr::from(addr)))
} else {
None
}
}

#[cfg(test)]
mod tests {
use carbide_test_support::Outcome::*;
Expand All @@ -39,6 +83,38 @@ mod tests {
assert_eq!(normalized, expected);
}

#[test]
fn parses_arpa_qname_to_ip() {
use std::net::{IpAddr, Ipv4Addr};

use carbide_test_support::value_scenarios;

value_scenarios!(
run = |qname: &str| super::arpa_qname_to_ip(qname);
"ipv4 in-addr.arpa" {
"1.0.168.192.in-addr.arpa." => Some(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1))),
"3.2.1.10.in-addr.arpa." => Some(IpAddr::V4(Ipv4Addr::new(10, 1, 2, 3))),
}
"ipv6 ip6.arpa" {
"1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa."
=> Some("2001:db8::1".parse::<IpAddr>().unwrap()),
"1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa."
=> Some("::1".parse::<IpAddr>().unwrap()),
}
"rejects non-arpa and malformed" {
"host.example.com." => None,
"1.2.3.in-addr.arpa." => None,
"300.0.0.0.in-addr.arpa." => None,
"1.0.168.192.in-addr.arpa.extra." => None,
}
"normalizes case" {
"1.0.168.192.IN-ADDR.ARPA." => Some(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1))),
"1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.IP6.ARPA."
=> Some("2001:db8::1".parse::<IpAddr>().unwrap()),
}
);
}

#[crate::sqlx_test]
async fn test_dns_hostname_from_ipv6_expands_to_rust_format(pool: sqlx::PgPool) {
check_cases_async(
Expand Down
Loading