Skip to content

Commit b7198e7

Browse files
committed
feat(dns): parse reverse-DNS (PTR) query names into IP addresses
Reverse DNS resolves a PTR query name -- an address in `in-addr.arpa` / `ip6.arpa` form -- back to a hostname. The lookup direction, arpa name to address, is a clean inverse, so this does it in Rust rather than a large PL/pgSQL function: `arpa_qname_to_ip` reverses the four octets (IPv4) or the 32 nibbles (IPv6) and hands back an `IpAddr`. The PTR handler will use that to look the interface up by address directly -- an indexed equality, not a per-row arpa string computed across a view. `Ipv6Addr` does the address assembly, so the IPv6 case is a few lines instead of the manual hextet expansion the equivalent SQL needs. This is the conversion foundation for reverse DNS; the indexed address lookup (`find_ptr_record`) and the handler arm build on it. Tests cover the IPv4 and IPv6 forms plus rejection of non-arpa and malformed names. This supports #2637. Signed-off-by: Chet Nichols III <chetn@nvidia.com>
1 parent 6d36a43 commit b7198e7

1 file changed

Lines changed: 71 additions & 0 deletions

File tree

crates/api-db/src/dns/mod.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,56 @@ pub mod domain;
1919
pub mod domain_metadata;
2020
pub mod resource_record;
2121

22+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
23+
2224
pub fn normalize_domain(name: &str) -> String {
2325
let normalize_domain = name.trim_end_matches('.').to_lowercase();
2426
tracing::debug!("Normalized domain name: {} to: {}", name, normalize_domain);
2527
normalize_domain
2628
}
2729

30+
/// Parse a reverse-DNS (PTR) query name into the address it points at -- the
31+
/// inverse of the `in-addr.arpa` (IPv4) / `ip6.arpa` (IPv6) form. Returns `None`
32+
/// for anything that is not a well-formed arpa name, so the caller answers
33+
/// NotFound rather than guessing.
34+
pub fn arpa_qname_to_ip(qname: &str) -> Option<IpAddr> {
35+
let name = qname.trim_end_matches('.').to_ascii_lowercase();
36+
37+
if let Some(reversed) = name.strip_suffix(".in-addr.arpa") {
38+
// Four decimal octets, least-significant label first.
39+
let octets: Vec<&str> = reversed.split('.').collect();
40+
if octets.len() != 4 {
41+
return None;
42+
}
43+
let mut addr = [0u8; 4];
44+
for (byte, octet) in addr.iter_mut().zip(octets.iter().rev()) {
45+
*byte = octet.parse().ok()?;
46+
}
47+
Some(IpAddr::V4(Ipv4Addr::from(addr)))
48+
} else if let Some(reversed) = name.strip_suffix(".ip6.arpa") {
49+
// Thirty-two hex nibbles, least-significant label first.
50+
let nibbles: Vec<&str> = reversed.split('.').collect();
51+
if nibbles.len() != 32 {
52+
return None;
53+
}
54+
let mut addr = [0u8; 16];
55+
for (i, nibble) in nibbles.iter().rev().enumerate() {
56+
if nibble.len() != 1 {
57+
return None;
58+
}
59+
let value = u8::from_str_radix(nibble, 16).ok()?;
60+
if i % 2 == 0 {
61+
addr[i / 2] = value << 4;
62+
} else {
63+
addr[i / 2] |= value;
64+
}
65+
}
66+
Some(IpAddr::V6(Ipv6Addr::from(addr)))
67+
} else {
68+
None
69+
}
70+
}
71+
2872
#[cfg(test)]
2973
mod tests {
3074
use carbide_test_support::Outcome::*;
@@ -39,6 +83,33 @@ mod tests {
3983
assert_eq!(normalized, expected);
4084
}
4185

86+
#[test]
87+
fn parses_arpa_qname_to_ip() {
88+
use std::net::{IpAddr, Ipv4Addr};
89+
90+
use carbide_test_support::value_scenarios;
91+
92+
value_scenarios!(
93+
run = |qname: &str| super::arpa_qname_to_ip(qname);
94+
"ipv4 in-addr.arpa" {
95+
"1.0.168.192.in-addr.arpa." => Some(IpAddr::V4(Ipv4Addr::new(192, 168, 0, 1))),
96+
"3.2.1.10.in-addr.arpa." => Some(IpAddr::V4(Ipv4Addr::new(10, 1, 2, 3))),
97+
}
98+
"ipv6 ip6.arpa" {
99+
"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."
100+
=> Some("2001:db8::1".parse::<IpAddr>().unwrap()),
101+
"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."
102+
=> Some("::1".parse::<IpAddr>().unwrap()),
103+
}
104+
"rejects non-arpa and malformed" {
105+
"host.example.com." => None,
106+
"1.2.3.in-addr.arpa." => None,
107+
"300.0.0.0.in-addr.arpa." => None,
108+
"1.0.168.192.in-addr.arpa.extra." => None,
109+
}
110+
);
111+
}
112+
42113
#[crate::sqlx_test]
43114
async fn test_dns_hostname_from_ipv6_expands_to_rust_format(pool: sqlx::PgPool) {
44115
check_cases_async(

0 commit comments

Comments
 (0)