1use anyhow::{bail, Context, Result};
12use hickory_proto::op::{Header, Message, MessageType, OpCode, Query, ResponseCode};
13use hickory_proto::rr::rdata::SOA;
14use hickory_proto::rr::{Name, RData, Record, RecordType};
15use hickory_proto::serialize::binary::BinEncodable;
16use serde_json::Value;
17
18use super::records::{parse_name, record_from_json, records_from_json};
19
20pub const MAX_ZONE_RECORDS: usize = 10_000;
23
24pub const TRANSFER_MESSAGE_BYTES: usize = 16 * 1024;
28
29pub struct Zone {
31 pub origin: Name,
32 pub soa: Record,
33 pub records: Vec<Record>,
34}
35
36fn origin_of(action: &Value) -> Result<Name> {
37 let zone = action
38 .get("zone")
39 .and_then(|z| z.as_str())
40 .context("a zone transfer answer needs 'zone', the zone's name, e.g. \"example.com.\"")?;
41 parse_name(zone, None)
42}
43
44fn soa_of(action: &Value, origin: &Name) -> Result<Record> {
45 let mut soa_json = action
46 .get("soa")
47 .cloned()
48 .context("a zone transfer answer needs 'soa': the zone's SOA record")?;
49 if let Some(obj) = soa_json.as_object_mut() {
50 obj.entry("type").or_insert(Value::from("SOA"));
51 obj.entry("name").or_insert(Value::from("@"));
52 }
53 let soa = record_from_json(&soa_json, Some(origin), Some(origin)).context("soa")?;
54 if soa.record_type() != RecordType::SOA {
55 bail!("'soa' must be an SOA record");
56 }
57 if soa.name() != origin {
58 bail!(
59 "the SOA belongs at the zone apex {origin}, not at {}",
60 soa.name()
61 );
62 }
63 Ok(soa)
64}
65
66pub fn serial_of(soa: &Record) -> Option<u32> {
68 match soa.data() {
69 Some(RData::SOA(s)) => Some(s.serial()),
70 _ => None,
71 }
72}
73
74fn with_serial(soa: &Record, serial: u32) -> Record {
75 let mut r = soa.clone();
76 if let Some(RData::SOA(s)) = soa.data() {
77 r.set_data(Some(RData::SOA(SOA::new(
78 s.mname().clone(),
79 s.rname().clone(),
80 serial,
81 s.refresh(),
82 s.retry(),
83 s.expire(),
84 s.minimum(),
85 ))));
86 }
87 r
88}
89
90fn in_zone(records: &[Record], origin: &Name, what: &str) -> Result<()> {
91 for r in records {
92 if !origin.zone_of(r.name()) {
93 bail!(
94 "{what}: {} {} is outside the zone {origin}",
95 r.name(),
96 r.record_type()
97 );
98 }
99 if r.record_type() == RecordType::SOA {
100 bail!("{what}: the zone's SOA goes in 'soa', not among the records");
101 }
102 }
103 Ok(())
104}
105
106pub fn zone_from_action(action: &Value) -> Result<Zone> {
108 let origin = origin_of(action)?;
109 let soa = soa_of(action, &origin)?;
110 let records = records_from_json(
111 action.get("records"),
112 Some(&origin),
113 None,
114 "records",
115 MAX_ZONE_RECORDS - 2,
116 )?;
117 in_zone(&records, &origin, "records")?;
118 Ok(Zone {
119 origin,
120 soa,
121 records,
122 })
123}
124
125pub fn axfr_sequence(zone: &Zone) -> Vec<Record> {
127 let mut out = Vec::with_capacity(zone.records.len() + 2);
128 out.push(zone.soa.clone());
129 out.extend(zone.records.iter().cloned());
130 out.push(zone.soa.clone());
131 out
132}
133
134pub fn ixfr_from_action(action: &Value, client_serial: u32) -> Result<(Name, Vec<Record>)> {
140 let origin = origin_of(action)?;
141 let soa = soa_of(action, &origin)?;
142 let new_serial = serial_of(&soa).unwrap_or_default();
143 let changes = match action.get("changes") {
144 None | Some(Value::Null) => Vec::new(),
145 Some(Value::Array(c)) => c.clone(),
146 Some(other) => bail!("'changes' must be an array, got {other}"),
147 };
148 let mut out = vec![soa.clone()];
149 if changes.is_empty() {
150 return Ok((origin, out));
151 }
152 let mut from = client_serial;
153 let mut total = 2;
154 for (i, change) in changes.iter().enumerate() {
155 let to = change
156 .get("serial")
157 .and_then(|s| s.as_u64())
158 .and_then(|s| u32::try_from(s).ok())
159 .with_context(|| format!("changes[{i}] needs 'serial', the serial it ends at"))?;
160 let deleted = records_from_json(
161 change.get("deleted"),
162 Some(&origin),
163 None,
164 &format!("changes[{i}].deleted"),
165 MAX_ZONE_RECORDS,
166 )?;
167 let added = records_from_json(
168 change.get("added"),
169 Some(&origin),
170 None,
171 &format!("changes[{i}].added"),
172 MAX_ZONE_RECORDS,
173 )?;
174 in_zone(&deleted, &origin, "deleted")?;
175 in_zone(&added, &origin, "added")?;
176 total += deleted.len() + added.len() + 2;
177 if total > MAX_ZONE_RECORDS {
178 bail!("the IXFR answer passes {MAX_ZONE_RECORDS} records");
179 }
180 out.push(with_serial(&soa, from));
181 out.extend(deleted);
182 out.push(with_serial(&soa, to));
183 out.extend(added);
184 from = to;
185 }
186 if from != new_serial {
187 bail!("the last change ends at serial {from} but 'soa' says {new_serial}; they must agree");
188 }
189 out.push(soa);
190 Ok((origin, out))
191}
192
193pub fn response_shell(query: &Message, rcode: ResponseCode) -> Message {
195 let mut message = Message::new();
196 let mut header = Header::new();
197 header.set_id(query.id());
198 header.set_message_type(MessageType::Response);
199 header.set_op_code(OpCode::Query);
200 header.set_authoritative(true);
201 header.set_recursion_desired(query.recursion_desired());
202 header.set_response_code(rcode);
203 message.set_header(header);
204 message
205}
206
207pub fn split_into_messages(
211 query: &Message,
212 question: Option<&Query>,
213 records: Vec<Record>,
214 target: usize,
215 extra: impl Fn(&mut Message),
216) -> Result<Vec<Vec<u8>>> {
217 let mut messages = Vec::new();
218 let mut current = response_shell(query, ResponseCode::NoError);
219 if let Some(q) = question {
220 current.add_query(q.clone());
221 }
222 let mut size = 12usize;
223 let mut count = 0usize;
224 let finish = |m: &mut Message, last: bool| -> Result<Vec<u8>> {
225 if last {
226 extra(m);
227 }
228 m.to_vec().context("cannot encode a zone transfer message")
229 };
230 let total = records.len();
231 for (i, record) in records.into_iter().enumerate() {
232 let len = record
233 .to_bytes()
234 .context("cannot encode a zone transfer record")?
235 .len();
236 if count > 0 && size + len > target {
237 messages.push(finish(&mut current, false)?);
238 current = response_shell(query, ResponseCode::NoError);
239 size = 12;
240 count = 0;
241 }
242 size += len;
243 count += 1;
244 current.add_answer(record);
245 if i + 1 == total {
246 messages.push(finish(&mut current, true)?);
247 return Ok(messages);
248 }
249 }
250 messages.push(finish(&mut current, true)?);
251 Ok(messages)
252}