use std::borrow::Borrow;
use std::collections::HashMap;
use std::str::FromStr;
use hickory_proto::rr::{
rdata::{self, CNAME},
LowerName, Name, RData, Record, RecordType,
};
use crate::config::parser::{LocalData, LocalZone};
#[derive(Debug, Clone, PartialEq)]
pub enum ZoneAction {
Refuse,
NxDomain,
Static,
#[allow(dead_code)]
Redirect,
}
impl From<&str> for ZoneAction {
fn from(s: &str) -> Self {
match s {
"refuse" | "inform_deny" => ZoneAction::Refuse,
"always_nxdomain" | "nxdomain" => ZoneAction::NxDomain,
"static" | "redirect" => ZoneAction::Static,
_ => ZoneAction::Refuse,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct LocalZoneSet {
pub zones: HashMap<Name, ZoneAction>,
pub records: HashMap<Name, Vec<Record>>,
}
impl LocalZoneSet {
pub fn from_config(zones: &[LocalZone], data: &[LocalData]) -> Self {
let mut map = HashMap::with_capacity(zones.len());
for z in zones {
let name_str = if z.name.ends_with('.') {
z.name.clone()
} else {
format!("{}.", z.name)
};
if let Ok(n) = Name::from_str(&name_str) {
map.insert(n, ZoneAction::from(z.zone_type.as_str()));
}
}
let mut record_map: HashMap<Name, Vec<Record>> = HashMap::new();
for d in data {
if let Some(rec) = parse_local_data(&d.rr) {
let name = rec.name().clone();
map.entry(name.clone()).or_insert(ZoneAction::Static);
record_map.entry(name).or_default().push(rec);
}
}
Self { zones: map, records: record_map }
}
#[inline]
pub fn override_zone(&mut self, name: &str, action: ZoneAction) {
let name_str = if name.ends_with('.') {
name.to_string()
} else {
format!("{}.", name)
};
if let Ok(n) = Name::from_str(&name_str) {
self.zones.insert(n, action);
}
}
#[inline]
pub fn remove_zone(&mut self, name: &str) {
let name_str = if name.ends_with('.') {
name.to_string()
} else {
format!("{}.", name)
};
if let Ok(n) = Name::from_str(&name_str) {
self.zones.remove(&n);
}
}
#[inline]
pub fn find(&self, query: &LowerName) -> Option<ZoneAction> {
if let Some(action) = self.zones.get(query.borrow() as &Name) {
return Some(action.clone());
}
if query.is_root() {
return None;
}
let mut name = query.base_name();
loop {
if let Some(action) = self.zones.get(name.borrow() as &Name) {
return Some(action.clone());
}
if name.is_root() {
break;
}
name = name.base_name();
}
None
}
#[inline(always)]
pub fn local_records(&self, query_name: &LowerName, rtype: RecordType) -> Vec<&Record> {
self.records.get(query_name.borrow() as &Name)
.map(|recs| recs.iter().filter(|r| r.record_type() == rtype).collect())
.unwrap_or_default()
}
#[inline(always)]
pub fn name_has_records(&self, name: &LowerName) -> bool {
self.records.contains_key(name.borrow() as &Name)
}
}
pub fn parse_local_data(rr: &str) -> Option<Record> {
let parts: Vec<&str> = rr.split_whitespace().collect();
if parts.len() < 3 {
return None;
}
let mut idx = 1usize;
let mut ttl = 300u32;
if parts[idx].parse::<u32>().is_ok() {
ttl = parts[idx].parse().ok()?;
idx += 1;
}
if idx < parts.len() {
let up = parts[idx].to_uppercase();
if up == "IN" || up == "CH" || up == "HS" || up == "ANY" {
idx += 1;
}
}
if idx >= parts.len() { return None; }
let name_str = parts[0];
let type_str = parts[idx];
let rest = &parts[idx + 1..];
if rest.is_empty() { return None; }
let name_fqdn = if name_str.ends_with('.') {
name_str.to_string()
} else {
format!("{}.", name_str)
};
let name = Name::from_str(&name_fqdn).ok()?;
let joined = rest.join(" ");
let rdata: RData = match type_str.to_uppercase().as_str() {
"A" => RData::A(rest[0].parse().ok()?),
"AAAA" => RData::AAAA(rest[0].parse().ok()?),
"CNAME" => RData::CNAME(CNAME(Name::from_str(rest[0]).ok()?)),
"PTR" => RData::PTR(rdata::PTR(Name::from_str(rest[0]).ok()?)),
"NS" => RData::NS(rdata::NS(Name::from_str(rest[0]).ok()?)),
"TXT" => {
let txt = joined.trim_matches('"').to_string();
RData::TXT(rdata::TXT::new(vec![txt]))
}
"MX" => {
let pref: u16 = rest[0].parse().ok()?;
let exch = Name::from_str(rest[1]).ok()?;
RData::MX(rdata::MX::new(pref, exch))
}
"SRV" => {
let priority: u16 = rest[0].parse().ok()?;
let weight: u16 = rest[1].parse().ok()?;
let port: u16 = rest[2].parse().ok()?;
let target = Name::from_str(rest[3]).ok()?;
RData::SRV(rdata::SRV::new(priority, weight, port, target))
}
"CAA" => {
let flags: u8 = rest[0].parse().ok()?;
let tag_str = rest[1];
let val = rest[2..].join(" ").trim_matches('"').to_string();
let issuer_crit = flags & 0x80 != 0;
match tag_str {
"issue" => RData::CAA(rdata::CAA::new_issue(issuer_crit,
Name::from_str(&val).ok(), vec![])),
"issuewild" => RData::CAA(rdata::CAA::new_issuewild(issuer_crit,
Name::from_str(&val).ok(), vec![])),
_ => return None,
}
}
"SSHFP" => {
use hickory_proto::rr::rdata::sshfp::{Algorithm, FingerprintType, SSHFP};
let algo: u8 = rest[0].parse().ok()?;
let fpt: u8 = rest[1].parse().ok()?;
let fp_hex = rest[2];
let fp_bytes = hex::decode(fp_hex).ok()?;
let algorithm = Algorithm::from(algo);
let fp_type = FingerprintType::from(fpt);
RData::SSHFP(SSHFP::new(algorithm, fp_type, fp_bytes))
}
"TLSA" => {
use hickory_proto::rr::rdata::tlsa::{CertUsage, Selector, Matching, TLSA};
let cu: u8 = rest[0].parse().ok()?;
let sel: u8 = rest[1].parse().ok()?;
let mt: u8 = rest[2].parse().ok()?;
let data = hex::decode(rest[3]).ok()?;
RData::TLSA(TLSA::new(
CertUsage::from(cu), Selector::from(sel), Matching::from(mt), data))
}
"NAPTR" => {
use hickory_proto::rr::rdata::NAPTR;
let order: u16 = rest[0].parse().ok()?;
let preference: u16 = rest[1].parse().ok()?;
let flags_raw = rest[2].trim_matches('"');
let services_raw = rest[3].trim_matches('"');
let regexp_raw = rest[4].trim_matches('"');
let replacement = Name::from_str(rest[5]).ok()?;
RData::NAPTR(NAPTR::new(
order,
preference,
flags_raw.as_bytes().to_vec().into_boxed_slice(),
services_raw.as_bytes().to_vec().into_boxed_slice(),
regexp_raw.as_bytes().to_vec().into_boxed_slice(),
replacement,
))
}
_ => return None,
};
let mut record = Record::new();
record
.set_name(name)
.set_rr_type(rdata.record_type())
.set_data(Some(rdata))
.set_ttl(ttl);
Some(record)
}