Skip to main content

netget/server/dns/
transfer.rs

1//! Zone transfers: AXFR (RFC 5936) and IXFR (RFC 1995), built from the model's structured
2//! answer and split into as many DNS messages as the zone needs.
3//!
4//! The model answers a `dns_zone_transfer` event with `send_dns_zone` (the whole zone) or
5//! `send_dns_ixfr` (the changes since the client's serial). Neither executor builds wire
6//! bytes, because a zone does not fit one message — hickory refuses to encode past 65535 bytes,
7//! the most a TCP DNS message can carry. The executors validate the action and hand it back;
8//! this module turns it into the record sequence the RFCs define, and `mod.rs` signs it (for a
9//! `dnssec_zone`) and cuts it into messages with [`split_into_messages`].
10
11use 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
20/// Most records one transfer may carry, SOAs and changes included. Far past any zone a model
21/// would invent, and the bound on what one answer can make the server hold and sign.
22pub const MAX_ZONE_RECORDS: usize = 10_000;
23
24/// A transfer message is filled to about this many bytes before the next one starts. Well
25/// under the 65535 a TCP message can carry, as BIND does, so one oversize record cannot push a
26/// message past the limit.
27pub const TRANSFER_MESSAGE_BYTES: usize = 16 * 1024;
28
29/// The zone, its SOA and its other records, from a `send_dns_zone` action.
30pub 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
66/// The SOA's serial.
67pub 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
106/// Read and check a `send_dns_zone` action.
107pub 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
125/// The AXFR answer sequence: SOA, every record, SOA (RFC 5936 §2.2).
126pub 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
134/// Read a `send_dns_ixfr` action into the IXFR answer sequence for a client at
135/// `client_serial` (RFC 1995 §4): the new SOA; then, per change, the SOA it starts from, the
136/// records it deletes, the SOA it ends at and the records it adds; then the new SOA again.
137/// With no changes the answer is the new SOA alone, which tells a client at that serial it is
138/// current.
139pub 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
193/// An empty response to `query`, AA set: the shell every transfer message is built in.
194pub 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
207/// Cut `records` into transfer messages of about `target` bytes, each answering `query`. The
208/// question goes in the first message only (RFC 5936 §2.2.1 permits it in the rest but nothing
209/// requires it). `extra` is added to the last message — the OPT record, when the query had one.
210pub 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}