use anyhow::{bail, Context, Result};
use hickory_proto::op::{Header, Message, MessageType, OpCode, Query, ResponseCode};
use hickory_proto::rr::rdata::SOA;
use hickory_proto::rr::{Name, RData, Record, RecordType};
use hickory_proto::serialize::binary::BinEncodable;
use serde_json::Value;
use super::records::{parse_name, record_from_json, records_from_json};
pub const MAX_ZONE_RECORDS: usize = 10_000;
pub const TRANSFER_MESSAGE_BYTES: usize = 16 * 1024;
pub struct Zone {
pub origin: Name,
pub soa: Record,
pub records: Vec<Record>,
}
fn origin_of(action: &Value) -> Result<Name> {
let zone = action
.get("zone")
.and_then(|z| z.as_str())
.context("a zone transfer answer needs 'zone', the zone's name, e.g. \"example.com.\"")?;
parse_name(zone, None)
}
fn soa_of(action: &Value, origin: &Name) -> Result<Record> {
let mut soa_json = action
.get("soa")
.cloned()
.context("a zone transfer answer needs 'soa': the zone's SOA record")?;
if let Some(obj) = soa_json.as_object_mut() {
obj.entry("type").or_insert(Value::from("SOA"));
obj.entry("name").or_insert(Value::from("@"));
}
let soa = record_from_json(&soa_json, Some(origin), Some(origin)).context("soa")?;
if soa.record_type() != RecordType::SOA {
bail!("'soa' must be an SOA record");
}
if soa.name() != origin {
bail!(
"the SOA belongs at the zone apex {origin}, not at {}",
soa.name()
);
}
Ok(soa)
}
pub fn serial_of(soa: &Record) -> Option<u32> {
match soa.data() {
Some(RData::SOA(s)) => Some(s.serial()),
_ => None,
}
}
fn with_serial(soa: &Record, serial: u32) -> Record {
let mut r = soa.clone();
if let Some(RData::SOA(s)) = soa.data() {
r.set_data(Some(RData::SOA(SOA::new(
s.mname().clone(),
s.rname().clone(),
serial,
s.refresh(),
s.retry(),
s.expire(),
s.minimum(),
))));
}
r
}
fn in_zone(records: &[Record], origin: &Name, what: &str) -> Result<()> {
for r in records {
if !origin.zone_of(r.name()) {
bail!(
"{what}: {} {} is outside the zone {origin}",
r.name(),
r.record_type()
);
}
if r.record_type() == RecordType::SOA {
bail!("{what}: the zone's SOA goes in 'soa', not among the records");
}
}
Ok(())
}
pub fn zone_from_action(action: &Value) -> Result<Zone> {
let origin = origin_of(action)?;
let soa = soa_of(action, &origin)?;
let records = records_from_json(
action.get("records"),
Some(&origin),
None,
"records",
MAX_ZONE_RECORDS - 2,
)?;
in_zone(&records, &origin, "records")?;
Ok(Zone {
origin,
soa,
records,
})
}
pub fn axfr_sequence(zone: &Zone) -> Vec<Record> {
let mut out = Vec::with_capacity(zone.records.len() + 2);
out.push(zone.soa.clone());
out.extend(zone.records.iter().cloned());
out.push(zone.soa.clone());
out
}
pub fn ixfr_from_action(action: &Value, client_serial: u32) -> Result<(Name, Vec<Record>)> {
let origin = origin_of(action)?;
let soa = soa_of(action, &origin)?;
let new_serial = serial_of(&soa).unwrap_or_default();
let changes = match action.get("changes") {
None | Some(Value::Null) => Vec::new(),
Some(Value::Array(c)) => c.clone(),
Some(other) => bail!("'changes' must be an array, got {other}"),
};
let mut out = vec![soa.clone()];
if changes.is_empty() {
return Ok((origin, out));
}
let mut from = client_serial;
let mut total = 2;
for (i, change) in changes.iter().enumerate() {
let to = change
.get("serial")
.and_then(|s| s.as_u64())
.and_then(|s| u32::try_from(s).ok())
.with_context(|| format!("changes[{i}] needs 'serial', the serial it ends at"))?;
let deleted = records_from_json(
change.get("deleted"),
Some(&origin),
None,
&format!("changes[{i}].deleted"),
MAX_ZONE_RECORDS,
)?;
let added = records_from_json(
change.get("added"),
Some(&origin),
None,
&format!("changes[{i}].added"),
MAX_ZONE_RECORDS,
)?;
in_zone(&deleted, &origin, "deleted")?;
in_zone(&added, &origin, "added")?;
total += deleted.len() + added.len() + 2;
if total > MAX_ZONE_RECORDS {
bail!("the IXFR answer passes {MAX_ZONE_RECORDS} records");
}
out.push(with_serial(&soa, from));
out.extend(deleted);
out.push(with_serial(&soa, to));
out.extend(added);
from = to;
}
if from != new_serial {
bail!("the last change ends at serial {from} but 'soa' says {new_serial}; they must agree");
}
out.push(soa);
Ok((origin, out))
}
pub fn response_shell(query: &Message, rcode: ResponseCode) -> Message {
let mut message = Message::new();
let mut header = Header::new();
header.set_id(query.id());
header.set_message_type(MessageType::Response);
header.set_op_code(OpCode::Query);
header.set_authoritative(true);
header.set_recursion_desired(query.recursion_desired());
header.set_response_code(rcode);
message.set_header(header);
message
}
pub fn split_into_messages(
query: &Message,
question: Option<&Query>,
records: Vec<Record>,
target: usize,
extra: impl Fn(&mut Message),
) -> Result<Vec<Vec<u8>>> {
let mut messages = Vec::new();
let mut current = response_shell(query, ResponseCode::NoError);
if let Some(q) = question {
current.add_query(q.clone());
}
let mut size = 12usize;
let mut count = 0usize;
let finish = |m: &mut Message, last: bool| -> Result<Vec<u8>> {
if last {
extra(m);
}
m.to_vec().context("cannot encode a zone transfer message")
};
let total = records.len();
for (i, record) in records.into_iter().enumerate() {
let len = record
.to_bytes()
.context("cannot encode a zone transfer record")?
.len();
if count > 0 && size + len > target {
messages.push(finish(&mut current, false)?);
current = response_shell(query, ResponseCode::NoError);
size = 12;
count = 0;
}
size += len;
count += 1;
current.add_answer(record);
if i + 1 == total {
messages.push(finish(&mut current, true)?);
return Ok(messages);
}
}
messages.push(finish(&mut current, true)?);
Ok(messages)
}