use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
use tracing::trace;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SynthAnswer {
Addrs(Vec<IpAddr>),
Names(Vec<String>),
NxDomain,
NoData,
}
#[derive(Clone, Debug, Default)]
pub struct HostsTable {
by_name: HashMap<String, Vec<IpAddr>>,
by_addr: HashMap<IpAddr, Vec<String>>,
}
impl HostsTable {
pub fn new() -> Self {
Self::default()
}
pub fn parse_hosts_file(text: &str) -> Self {
let mut t = Self::new();
for line in text.lines() {
let line = line.split('#').next().unwrap_or("").trim();
if line.is_empty() {
continue;
}
let mut parts = line.split_whitespace();
let Some(ip_s) = parts.next() else { continue };
let Ok(ip) = IpAddr::from_str(ip_s) else {
continue;
};
let names: Vec<String> = parts
.map(|n| n.trim_end_matches('.').to_ascii_lowercase())
.collect();
if names.is_empty() {
continue;
}
for n in &names {
t.by_name.entry(n.clone()).or_default().push(ip);
}
t.by_addr.entry(ip).or_default().extend(names);
}
for v in t.by_name.values_mut() {
v.sort_unstable();
v.dedup();
}
for v in t.by_addr.values_mut() {
v.sort_unstable();
v.dedup();
}
t
}
pub fn lookup_name(&self, name: &str, qtype: u16) -> Option<SynthAnswer> {
let n = name.trim_end_matches('.').to_ascii_lowercase();
let addrs = self.by_name.get(&n)?;
let filtered: Vec<IpAddr> = addrs
.iter()
.copied()
.filter(|a| match (qtype, a) {
(1, IpAddr::V4(_)) | (28, IpAddr::V6(_)) | (255, _) | (0, _) => true,
_ => false,
})
.collect();
if filtered.is_empty() {
Some(SynthAnswer::NoData)
} else {
Some(SynthAnswer::Addrs(filtered))
}
}
pub fn lookup_addr(&self, ip: IpAddr) -> Option<SynthAnswer> {
let names = self.by_addr.get(&ip)?;
Some(SynthAnswer::Names(names.clone()))
}
}
#[derive(Clone, Debug)]
pub struct SynthContext<'a> {
pub hostname: &'a str,
pub pretty_hostname: Option<&'a str>,
pub gateway_v4: Option<Ipv4Addr>,
pub gateway_v6: Option<Ipv6Addr>,
pub outbound_addrs: &'a [IpAddr],
pub local_addrs: &'a [IpAddr],
pub hosts: Option<&'a HostsTable>,
}
fn norm(s: &str) -> String {
s.trim().trim_end_matches('.').to_ascii_lowercase()
}
fn filter_addrs(addrs: impl Iterator<Item = IpAddr>, qtype: u16) -> Vec<IpAddr> {
addrs
.filter(|a| match (qtype, a) {
(1, IpAddr::V4(_)) => true,
(28, IpAddr::V6(_)) => true,
(255, _) | (0, _) => true,
_ => false,
})
.collect()
}
pub fn lookup_synthetic(ctx: &SynthContext<'_>, qname: &str, qtype: u16) -> Option<SynthAnswer> {
let n = norm(qname);
trace!(%n, qtype, "synthetic lookup");
if let Some(h) = ctx.hosts {
if let Some(a) = h.lookup_name(&n, qtype) {
return Some(a);
}
}
if n == "localhost" || n == "localhost.localdomain" || n.ends_with(".localhost") {
return match qtype {
1 => Some(SynthAnswer::Addrs(vec![IpAddr::V4(Ipv4Addr::LOCALHOST)])),
28 => Some(SynthAnswer::Addrs(vec![IpAddr::V6(Ipv6Addr::LOCALHOST)])),
255 | 0 => Some(SynthAnswer::Addrs(vec![
IpAddr::V4(Ipv4Addr::LOCALHOST),
IpAddr::V6(Ipv6Addr::LOCALHOST),
])),
_ => Some(SynthAnswer::NoData),
};
}
if n == "_gateway" || n == "gateway" {
let mut v = Vec::new();
if matches!(qtype, 1 | 255 | 0) {
if let Some(a) = ctx.gateway_v4 {
v.push(IpAddr::V4(a));
}
}
if matches!(qtype, 28 | 255 | 0) {
if let Some(a) = ctx.gateway_v6 {
v.push(IpAddr::V6(a));
}
}
return if v.is_empty() {
Some(SynthAnswer::NxDomain)
} else {
Some(SynthAnswer::Addrs(v))
};
}
if n == "_outbound" {
let v = filter_addrs(ctx.outbound_addrs.iter().copied(), qtype);
return if v.is_empty() {
Some(SynthAnswer::NxDomain)
} else {
Some(SynthAnswer::Addrs(v))
};
}
let host = norm(ctx.hostname);
let pretty = ctx.pretty_hostname.map(norm);
if n == host || pretty.as_ref() == Some(&n) {
let v = filter_addrs(ctx.local_addrs.iter().copied(), qtype);
if !v.is_empty() {
return Some(SynthAnswer::Addrs(v));
}
return Some(SynthAnswer::NoData);
}
None
}
pub fn lookup_synthetic_ptr(ctx: &SynthContext<'_>, addr: IpAddr) -> Option<SynthAnswer> {
if let Some(h) = ctx.hosts {
if let Some(a) = h.lookup_addr(addr) {
return Some(a);
}
}
match addr {
IpAddr::V4(v) if v.octets()[0] == 127 => Some(SynthAnswer::Names(vec!["localhost".into()])),
IpAddr::V6(v) if v.is_loopback() => Some(SynthAnswer::Names(vec!["localhost".into()])),
_ => {
if ctx.local_addrs.iter().any(|a| *a == addr) {
return Some(SynthAnswer::Names(vec![norm(ctx.hostname)]));
}
None
}
}
}
pub fn parse_in_addr_arpa(name: &str) -> Option<IpAddr> {
let n = norm(name);
if let Some(rest) = n.strip_suffix(".in-addr.arpa") {
let mut octs: Vec<u8> = Vec::new();
for p in rest.split('.') {
octs.push(p.parse().ok()?);
}
if octs.len() != 4 {
return None;
}
octs.reverse();
return Some(IpAddr::V4(Ipv4Addr::new(
octs[0], octs[1], octs[2], octs[3],
)));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn localhost_a() {
let ctx = SynthContext {
hostname: "box",
pretty_hostname: None,
gateway_v4: None,
gateway_v6: None,
outbound_addrs: &[],
local_addrs: &[],
hosts: None,
};
match lookup_synthetic(&ctx, "localhost", 1) {
Some(SynthAnswer::Addrs(a)) => assert_eq!(a[0], IpAddr::V4(Ipv4Addr::LOCALHOST)),
_ => panic!(),
}
}
#[test]
fn hosts_file() {
let t = HostsTable::parse_hosts_file("10.0.0.1 foo.example foo\n");
match t.lookup_name("foo", 1) {
Some(SynthAnswer::Addrs(a)) => assert_eq!(a[0].to_string(), "10.0.0.1"),
_ => panic!(),
}
}
}