Add XFR middleware. (#384)

Add XFR middleware and:
- Fixes a bug where the incorrect owner name was passed to the zone walker callback.
- New `ixfr-client.rs` example.
- Updated `serve-zone.rs` example demonstrating TSIG authenticated XFR and NOTIFY middlewares.
- Additional From impls for CallResult.
- Renames ZoneDiff to InMemoryZoneDiff.
- Renames ZoneDiffBuilder to InMemoryZoneDiffBuilder.
- Introduces new ZoneDiff and related traits.
- Adds AnswerContent::first().
This commit is contained in:
Ximon Eighteen
2024-10-02 21:28:01 +02:00
committed by GitHub
parent c87c6d50a8
commit 01fccdf942
33 changed files with 4670 additions and 192 deletions
+4
View File
@@ -127,6 +127,10 @@ required-features = ["zonefile", "unstable-zonetree"]
name = "serve-zone"
required-features = ["zonefile", "net", "unstable-server-transport", "unstable-zonetree"]
[[example]]
name = "ixfr-client"
required-features = ["zonefile", "net", "unstable-client-transport", "unstable-zonetree"]
# This example is commented out because it is difficult, if not impossible,
# when including the sqlx dependency, to make the dependency tree compatible
# with both `cargo +nightly update -Z minimal versions` and the crate minimum
+7 -3
View File
@@ -2,7 +2,9 @@ use bytes::Bytes;
use domain::base::{Message, MessageBuilder, Name, ParsedName, Rtype};
use domain::rdata::ZoneRecordData;
use domain::zonetree::Answer;
use octseq::Octets;
#[allow(dead_code)]
pub fn generate_wire_query(
qname: &Name<Bytes>,
qtype: Rtype,
@@ -13,6 +15,7 @@ pub fn generate_wire_query(
query.into()
}
#[allow(dead_code)]
pub fn generate_wire_response(
wire_query: &Message<Vec<u8>>,
zone_answer: Answer,
@@ -22,9 +25,10 @@ pub fn generate_wire_response(
response.into()
}
pub fn print_dig_style_response(
query: &Message<Vec<u8>>,
response: &Message<Vec<u8>>,
#[allow(dead_code)]
pub fn print_dig_style_response<Octs1: Octets, Octs2: Octets>(
query: &Message<Octs1>,
response: &Message<Octs2>,
short: bool,
) {
if !short {
+69
View File
@@ -0,0 +1,69 @@
/// Using the `domain::net::client` module for sending a query.
use core::str::FromStr;
use std::vec::Vec;
use tokio::net::TcpStream;
use domain::base::Name;
use domain::base::Rtype;
use domain::base::{MessageBuilder, Serial, Ttl};
use domain::net::client::request::SendRequestMulti;
use domain::net::client::request::{RequestMessage, RequestMessageMulti};
use domain::net::client::stream;
use domain::rdata::Soa;
#[path = "common/serve-utils.rs"]
mod common;
#[tokio::main]
async fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() != 4 {
eprintln!(
"Usage: {} <ip addr:port> <zone name> <SOA serial>",
args[0]
);
eprintln!("E.g.: {} 127.0.0.1:8053 example.com 2020080302", args[0]);
std::process::exit(1);
}
let server_addr = &args[1];
let qname = Name::<Vec<u8>>::from_str(&args[2]).unwrap();
let soa_serial: u32 = args[3].parse().unwrap();
eprintln!("Requesting IXFR from {server_addr} for zone {qname} from serial {soa_serial}");
let tcp_conn = TcpStream::connect(server_addr).await.unwrap();
let (tcp, transport) = stream::Connection::<
RequestMessage<Vec<u8>>,
RequestMessageMulti<Vec<u8>>,
>::new(tcp_conn);
tokio::spawn(async move {
transport.run().await;
println!("single TSIG TCP run terminated");
});
let mname = Name::<Vec<u8>>::from_str("mname").unwrap();
let rname = Name::<Vec<u8>>::from_str("rname").unwrap();
let ttl = Ttl::from_secs(3600);
let soa = Soa::new(mname, rname, Serial(soa_serial), ttl, ttl, ttl, ttl);
let mut msg = MessageBuilder::new_vec();
msg.header_mut().set_rd(true);
msg.header_mut().set_ad(true);
let mut msg = msg.question();
msg.push((&qname, Rtype::IXFR)).unwrap();
let mut msg = msg.authority();
msg.push((&qname, 3600, soa)).unwrap();
let req = RequestMessageMulti::new(msg.clone()).unwrap();
let mut request = SendRequestMulti::send_request(&tcp, req);
// Get the reply
let mock_req = msg.into_message();
while let Some(reply) = request.get_response().await.unwrap() {
common::print_dig_style_response(&mock_req, &reply, false);
}
}
+205 -9
View File
@@ -15,14 +15,27 @@
//!
//! dig @127.0.0.1 -p 8053 AXFR example.com
use core::future::{ready, Future};
use core::pin::Pin;
use core::str::FromStr;
use std::collections::HashMap;
use std::future::pending;
use std::io::BufReader;
use std::process::exit;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use domain::base::iana::Rcode;
use domain::base::ToName;
use octseq::Octets;
use rand::distributions::Alphanumeric;
use rand::Rng;
use tokio::net::{TcpListener, UdpSocket};
use tracing_subscriber::EnvFilter;
use domain::base::iana::{Class, Rcode};
use domain::base::name::OwnedLabel;
use domain::base::net::IpAddr;
use domain::base::{Name, Rtype, Serial, ToName, Ttl};
use domain::net::server::buf::VecBufSource;
use domain::net::server::dgram::DgramServer;
use domain::net::server::message::Request;
@@ -30,14 +43,22 @@ use domain::net::server::message::Request;
use domain::net::server::middleware::cookies::CookiesMiddlewareSvc;
use domain::net::server::middleware::edns::EdnsMiddlewareSvc;
use domain::net::server::middleware::mandatory::MandatoryMiddlewareSvc;
use domain::net::server::middleware::notify::{
Notifiable, NotifyError, NotifyMiddlewareSvc,
};
use domain::net::server::middleware::tsig::TsigMiddlewareSvc;
use domain::net::server::middleware::xfr::{
XfrData, XfrDataProvider, XfrDataProviderError, XfrMiddlewareSvc,
};
use domain::net::server::service::{CallResult, ServiceResult};
use domain::net::server::stream::StreamServer;
use domain::net::server::util::{mk_builder_for_target, service_fn};
use domain::tsig::{Algorithm, Key, KeyName};
use domain::zonefile::inplace;
use domain::zonetree::Answer;
use domain::zonetree::{
Answer, InMemoryZoneDiff, Rrset, SharedRrset, StoredName,
};
use domain::zonetree::{Zone, ZoneTree};
use tokio::net::{TcpListener, UdpSocket};
use tracing_subscriber::EnvFilter;
#[tokio::main()]
async fn main() {
@@ -50,6 +71,18 @@ async fn main() {
.try_init()
.ok();
// Create a TSIG key store with a demo key.
let mut key_store = HashMap::<(KeyName, Algorithm), Key>::new();
let key_name = KeyName::from_str("demo-key").unwrap();
let secret = domain::utils::base64::decode::<Vec<u8>>(
"zlCZbVJPIhobIs1gJNQfrsS3xCxxsR9pMUrGwG8OgG8=",
)
.unwrap();
let key =
Key::new(Algorithm::Sha256, &secret, key_name.clone(), None, None)
.unwrap();
key_store.insert((key_name, Algorithm::Sha256), key);
// Populate a zone tree with test data
let zone_bytes = include_bytes!("../test-data/zonefiles/nsd-example.txt");
let mut zone_bytes = BufReader::new(&zone_bytes[..]);
@@ -76,16 +109,28 @@ async fn main() {
zones.insert_zone(zone).unwrap();
let zones = Arc::new(zones);
// Create an XFR data provider that can serve diffs for our zone.
let zones_and_diffs = ZoneTreeWithDiffs::new(zones.clone());
// Create a server with middleware layers and an application service
// listening on localhost port 8053.
let addr = "127.0.0.1:8053";
let svc = service_fn(my_service, zones);
let svc = service_fn(my_service, zones.clone());
#[cfg(feature = "siphasher")]
let svc = CookiesMiddlewareSvc::<Vec<u8>, _, _>::with_random_secret(svc);
let svc = EdnsMiddlewareSvc::<Vec<u8>, _, _>::new(svc);
let svc = XfrMiddlewareSvc::<Vec<u8>, _, _, _>::new(
svc,
zones_and_diffs.clone(),
1,
);
let svc = NotifyMiddlewareSvc::new(svc, DemoNotifyTarget);
let svc = MandatoryMiddlewareSvc::<Vec<u8>, _, _>::new(svc);
let svc = TsigMiddlewareSvc::new(svc, key_store);
let svc = Arc::new(svc);
let sock = UdpSocket::bind(addr).await.unwrap();
let sock = UdpSocket::bind(&addr).await.unwrap();
let sock = Arc::new(sock);
let mut udp_metrics = vec![];
let num_cores = std::thread::available_parallelism().unwrap().get();
@@ -103,8 +148,17 @@ async fn main() {
tokio::spawn(async move { tcp_srv.run().await });
eprintln!("Ready");
eprintln!("Listening on {addr}");
eprintln!("Try:");
eprintln!(" dig @127.0.0.1 -p 8053 example.com");
eprintln!(" dig @127.0.0.1 -p 8053 example.com AXFR");
eprintln!(" dig @127.0.0.1 -p 8053 -y hmac-sha256:demo-key:zlCZbVJPIhobIs1gJNQfrsS3xCxxsR9pMUrGwG8OgG8= example.com AXFR");
eprintln!(" dig @127.0.0.1 -p 8053 +opcode=notify example.com SOA");
eprintln!(" cargo run --example ixfr-client --all-features -- 127.0.0.1:8053 example.com 2020080302");
eprintln!();
eprintln!("Tip: set env var RUST_LOG=info (or debug or trace) for more log output.");
// Print some status information every 5 seconds
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_millis(5000)).await;
@@ -138,6 +192,50 @@ async fn main() {
}
});
// Mutate our own zone every 10 seconds.
tokio::spawn(async move {
let zone_name = Name::<Vec<u8>>::from_str("example.com").unwrap();
let mut label: Option<OwnedLabel> = None;
loop {
tokio::time::sleep(Duration::from_millis(10000)).await;
let zone = zones.get_zone(&zone_name, Class::IN).unwrap();
let mut writer = zone.write().await;
{
let node = writer.open(true).await.unwrap();
if let Some(old_label) = label {
let node = node.update_child(&old_label).await.unwrap();
node.remove_rrset(Rtype::A).await.unwrap();
}
let random_string: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(7)
.map(char::from)
.collect();
let new_label = OwnedLabel::from_str(&random_string).unwrap();
let node = node.update_child(&new_label).await.unwrap();
let mut rrset = Rrset::new(Rtype::A, Ttl::from_secs(60));
let rec = domain::rdata::A::new("127.0.0.1".parse().unwrap());
rrset.push_data(rec.into());
node.update_rrset(SharedRrset::new(rrset)).await.unwrap();
label = Some(new_label);
}
let diff = writer.commit(true).await.unwrap();
if let Some(diff) = diff {
zones_and_diffs.add_diff(diff);
}
eprintln!(
"Added {} A record to zone example.com",
label.unwrap()
);
}
});
pending::<()>().await;
}
@@ -163,3 +261,101 @@ fn my_service(
let additional = answer.to_message(request.message(), builder);
Ok(CallResult::new(additional))
}
#[derive(Copy, Clone, Default, Debug)]
struct DemoNotifyTarget;
impl Notifiable for DemoNotifyTarget {
fn notify_zone_changed(
&self,
class: Class,
apex_name: &StoredName,
source: IpAddr,
) -> Pin<
Box<dyn Future<Output = Result<(), NotifyError>> + Sync + Send + '_>,
> {
eprintln!("Notify received from {source} of change to zone {apex_name} in class {class}");
let res = match apex_name.to_string().to_lowercase().as_str() {
"example.com" => Ok(()),
"othererror.com" => Err(NotifyError::Other),
_ => Err(NotifyError::NotAuthForZone),
};
Box::pin(ready(res))
}
}
#[derive(Clone)]
struct ZoneTreeWithDiffs {
zones: Arc<ZoneTree>,
diffs: Arc<Mutex<Vec<InMemoryZoneDiff>>>,
}
impl ZoneTreeWithDiffs {
fn new(zones: Arc<ZoneTree>) -> Self {
Self {
zones,
diffs: Default::default(),
}
}
fn add_diff(&self, diff: InMemoryZoneDiff) {
self.diffs.lock().unwrap().push(diff);
}
fn get_diffs(&self, diff_from: Option<Serial>) -> Vec<InMemoryZoneDiff> {
let diffs = self.diffs.lock().unwrap();
if let Some(idx) = diffs
.iter()
.position(|diff| Some(diff.start_serial) == diff_from)
{
diffs[idx..].to_vec()
} else {
vec![]
}
}
}
impl<RequestMeta> XfrDataProvider<RequestMeta> for ZoneTreeWithDiffs {
type Diff = InMemoryZoneDiff;
fn request<Octs>(
&self,
req: &Request<Octs, RequestMeta>,
diff_from: Option<Serial>,
) -> Pin<
Box<
dyn Future<
Output = Result<
XfrData<Self::Diff>,
XfrDataProviderError,
>,
> + Sync
+ Send,
>,
>
where
Octs: Octets + Send + Sync,
{
let res = req
.message()
.sole_question()
.map_err(XfrDataProviderError::ParseError)
.and_then(|q| {
if let Some(zone) =
self.zones.find_zone(q.qname(), q.qclass())
{
Ok(XfrData::new(
zone.clone(),
self.get_diffs(diff_from),
false,
))
} else {
Err(XfrDataProviderError::UnknownZone)
}
});
Box::pin(ready(res))
}
}
+2
View File
@@ -47,3 +47,5 @@ pub mod notify;
pub mod stream;
#[cfg(feature = "tsig")]
pub mod tsig;
#[cfg(feature = "unstable-xfr")]
pub mod xfr;
+94
View File
@@ -0,0 +1,94 @@
use std::boxed::Box;
use std::sync::Arc;
use bytes::Bytes;
use tokio::sync::mpsc::Sender;
use tokio::sync::Semaphore;
use tracing::error;
use crate::base::iana::OptRcode;
use crate::base::{Name, Rtype};
use crate::zonetree::{ReadableZone, SharedRrset, StoredName};
//------------ ZoneFunneler ---------------------------------------------------
pub struct ZoneFunneler {
read: Box<dyn ReadableZone>,
qname: StoredName,
zone_soa_rrset: SharedRrset,
batcher_tx: Sender<(Name<Bytes>, SharedRrset)>,
zone_walk_semaphore: Arc<Semaphore>,
}
impl ZoneFunneler {
pub fn new(
read: Box<dyn ReadableZone>,
qname: StoredName,
zone_soa_rrset: SharedRrset,
batcher_tx: Sender<(Name<Bytes>, SharedRrset)>,
zone_walk_semaphore: Arc<Semaphore>,
) -> Self {
Self {
read,
qname,
zone_soa_rrset,
batcher_tx,
zone_walk_semaphore,
}
}
pub async fn run(self) -> Result<(), OptRcode> {
// Limit the number of concurrently running XFR related zone walking
// operations.
if self.zone_walk_semaphore.acquire().await.is_err() {
error!("Internal error: Failed to acquire XFR zone walking semaphore");
return Err(OptRcode::SERVFAIL);
}
let cloned_batcher_tx = self.batcher_tx.clone();
let op = Box::new(move |owner: StoredName, rrset: &SharedRrset| {
if rrset.rtype() != Rtype::SOA {
let _ = cloned_batcher_tx
.blocking_send((owner.clone(), rrset.clone()));
// If the blocking send fails it means that the
// batcher is no longer available. This can happen if
// it was no longer able to pass messages back to the
// underlying transport, which can happen if the
// client closed the connection. We don't log this
// because we can't stop the tree walk and so will
// keep hitting this error until the tree walk is
// complete, causing a lot of noise if we were to log
// this.
}
});
// Walk the zone tree, invoking our operation for each leaf.
match self.read.is_async() {
true => {
self.read.walk_async(op).await;
if let Err(err) = self
.batcher_tx
.send((self.qname, self.zone_soa_rrset))
.await
{
error!("Internal error: Failed to send final AXFR SOA to batcher: {err}");
return Err(OptRcode::SERVFAIL);
}
}
false => {
tokio::task::spawn_blocking(move || {
self.read.walk(op);
if let Err(err) = self
.batcher_tx
.blocking_send((self.qname, self.zone_soa_rrset))
{
error!("Internal error: Failed to send final AXFR SOA to batcher: {err}");
// Note: The lack of the final SOA will be detected by the batcher.
}
});
}
}
Ok(())
}
}
+224
View File
@@ -0,0 +1,224 @@
use core::marker::PhantomData;
use std::sync::Arc;
use octseq::Octets;
use tokio::sync::mpsc::UnboundedSender;
use tracing::trace;
use crate::base::iana::{Opcode, Rcode};
use crate::base::message_builder::{
AdditionalBuilder, AnswerBuilder, PushError,
};
use crate::base::wire::Composer;
use crate::base::{Message, StreamTarget};
use crate::net::server::batcher::{
CallbackBatcher, Callbacks, ResourceRecordBatcher,
};
use crate::net::server::service::{CallResult, ServiceResult};
use crate::net::server::util::mk_builder_for_target;
//------------ BatchReadyError ------------------------------------------------
#[derive(Clone, Copy, Debug)]
pub enum BatchReadyError {
PushError(PushError),
SendError,
MustFitInSingleMessage,
}
//--- Display
impl std::fmt::Display for BatchReadyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
BatchReadyError::MustFitInSingleMessage => {
f.write_str("MustFitInSingleMessage")
}
BatchReadyError::PushError(err) => {
f.write_fmt(format_args!("PushError: {err}"))
}
BatchReadyError::SendError => f.write_str("SendError"),
}
}
}
//--- From<PushError>
impl From<PushError> for BatchReadyError {
fn from(err: PushError) -> Self {
Self::PushError(err)
}
}
//------------ XfrRrBatcher ---------------------------------------------------
pub struct XfrRrBatcher<RequestOctets, Target> {
_phantom: PhantomData<(RequestOctets, Target)>,
}
impl<RequestOctets, Target> XfrRrBatcher<RequestOctets, Target>
where
RequestOctets: Octets + Sync + Send + 'static,
Target: Composer + Default + Send + 'static,
{
pub fn build(
req_msg: Arc<Message<RequestOctets>>,
sender: UnboundedSender<ServiceResult<Target>>,
soft_byte_limit: Option<usize>,
hard_rr_limit: Option<u16>,
must_fit_in_single_message: bool,
) -> impl ResourceRecordBatcher<RequestOctets, Target, Error = BatchReadyError>
{
let cb_state = CallbackState::new(
req_msg.clone(),
sender,
soft_byte_limit,
hard_rr_limit,
must_fit_in_single_message,
);
CallbackBatcher::<
RequestOctets,
Target,
Self,
CallbackState<RequestOctets, Target>,
>::new(req_msg, cb_state)
}
}
impl<RequestOctets, Target> XfrRrBatcher<RequestOctets, Target>
where
RequestOctets: Octets,
Target: Composer + Default,
{
fn set_axfr_header(
msg: &Message<RequestOctets>,
additional: &mut AdditionalBuilder<StreamTarget<Target>>,
) {
// https://datatracker.ietf.org/doc/html/rfc5936#section-2.2.1
// 2.2.1: Header Values
//
// "These are the DNS message header values for AXFR responses.
//
// ID MUST be copied from request -- see Note a)
//
// QR MUST be 1 (Response)
//
// OPCODE MUST be 0 (Standard Query)
//
// Flags:
// AA normally 1 -- see Note b)
// TC MUST be 0 (Not truncated)
// RD RECOMMENDED: copy request's value; MAY be set to 0
// RA SHOULD be 0 -- see Note c)
// Z "mbz" -- see Note d)
// AD "mbz" -- see Note d)
// CD "mbz" -- see Note d)"
let header = additional.header_mut();
// Note: MandatoryMiddlewareSvc will also "fix" ID and QR, so strictly
// speaking this isn't necessary, but as a caller might not use
// MandatoryMiddlewareSvc we do it anyway to try harder to conform to
// the RFC.
header.set_id(msg.header().id());
header.set_qr(true);
header.set_opcode(Opcode::QUERY);
header.set_aa(true);
header.set_tc(false);
header.set_rd(msg.header().rd());
header.set_ra(false);
header.set_z(false);
header.set_ad(false);
header.set_cd(false);
}
}
//--- Callbacks
impl<RequestOctets, Target>
Callbacks<RequestOctets, Target, CallbackState<RequestOctets, Target>>
for XfrRrBatcher<RequestOctets, Target>
where
RequestOctets: Octets,
Target: Composer + Default,
{
type Error = BatchReadyError;
fn batch_started(
cb_state: &CallbackState<RequestOctets, Target>,
msg: &Message<RequestOctets>,
) -> Result<AnswerBuilder<StreamTarget<Target>>, PushError> {
let mut builder = mk_builder_for_target();
if let Some(limit) = cb_state.soft_byte_limit {
builder.set_push_limit(limit);
}
let answer = builder.start_answer(msg, Rcode::NOERROR)?;
Ok(answer)
}
fn batch_ready(
cb_state: &CallbackState<RequestOctets, Target>,
builder: AnswerBuilder<StreamTarget<Target>>,
finished: bool,
) -> Result<(), Self::Error> {
if !finished && cb_state.must_fit_in_single_message {
return Err(BatchReadyError::MustFitInSingleMessage);
}
trace!("Sending RR batch");
let mut additional = builder.additional();
Self::set_axfr_header(&cb_state.req_msg, &mut additional);
let call_result = Ok(CallResult::new(additional));
cb_state
.sender
.send(call_result)
.map_err(|_unsent_msg| BatchReadyError::SendError)
}
fn record_pushed(
cb_state: &CallbackState<RequestOctets, Target>,
answer: &AnswerBuilder<StreamTarget<Target>>,
) -> bool {
if let Some(hard_rr_limit) = cb_state.hard_rr_limit {
let ancount = answer.counts().ancount();
let limit_reached = ancount == hard_rr_limit;
trace!(
"ancount={ancount}, hard_rr_limit={hard_rr_limit}, limit_reached={limit_reached}");
limit_reached
} else {
false
}
}
}
//------------ CallbackState --------------------------------------------------
struct CallbackState<RequestOctets, Target> {
req_msg: Arc<Message<RequestOctets>>,
sender: UnboundedSender<ServiceResult<Target>>,
soft_byte_limit: Option<usize>,
hard_rr_limit: Option<u16>,
must_fit_in_single_message: bool,
}
impl<RequestOctets, Target> CallbackState<RequestOctets, Target> {
fn new(
req_msg: Arc<Message<RequestOctets>>,
sender: UnboundedSender<ServiceResult<Target>>,
soft_byte_limit: Option<usize>,
hard_rr_limit: Option<u16>,
must_fit_in_single_message: bool,
) -> Self {
Self {
req_msg,
sender,
soft_byte_limit,
hard_rr_limit,
must_fit_in_single_message,
}
}
}
@@ -0,0 +1,243 @@
use core::future::{ready, Future};
use core::ops::Deref;
use core::pin::Pin;
use octseq::Octets;
use std::boxed::Box;
use std::vec::Vec;
use crate::base::wire::ParseError;
use crate::base::Serial;
use crate::net::server::message::Request;
use crate::zonetree::types::EmptyZoneDiff;
use crate::zonetree::{Zone, ZoneDiff, ZoneTree};
//------------ XfrDataProviderError -------------------------------------------
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum XfrDataProviderError {
ParseError(ParseError),
UnknownZone,
Refused,
TemporarilyUnavailable,
}
//--- From<ParseError>
impl From<ParseError> for XfrDataProviderError {
fn from(err: ParseError) -> Self {
Self::ParseError(err)
}
}
//------------ XfrData --------------------------------------------------------
/// The data supplied by an [`XfrDataProvider`].
pub struct XfrData<Diff> {
/// The zone to transfer.
zone: Zone,
/// The requested diffs.
///
/// Empty if the requested diff range could not be satisfied.
diffs: Vec<Diff>,
/// Should XFR be done in RFC 5936 backward compatible mode?
///
/// See: https://www.rfc-editor.org/rfc/rfc5936#section-7
compatibility_mode: bool,
}
impl<Diff> XfrData<Diff> {
pub fn new(
zone: Zone,
diffs: Vec<Diff>,
backward_compatible: bool,
) -> Self {
Self {
zone,
diffs,
compatibility_mode: backward_compatible,
}
}
pub fn zone(&self) -> &Zone {
&self.zone
}
pub fn diffs(&self) -> &[Diff] {
&self.diffs
}
pub fn into_diffs(self) -> Vec<Diff> {
self.diffs
}
pub fn compatibility_mode(&self) -> bool {
self.compatibility_mode
}
}
//------------ XfrDataProvider ------------------------------------------------
/// A provider of data needed for responding to XFR requests.
pub trait XfrDataProvider<RequestMeta = ()> {
type Diff: ZoneDiff + Send + Sync;
/// Request data needed to respond to an XFR request.
///
/// Returns Ok if the request is allowed and the requested data is
/// available.
///
/// Returns Err otherwise.
///
/// Pass `Some` zone SOA serial number in the `diff_from` parameter to
/// request `ZoneDiff`s from the specified serial to the current SOA
/// serial number of the zone, inclusive, if available.
#[allow(clippy::type_complexity)]
fn request<Octs>(
&self,
req: &Request<Octs, RequestMeta>,
diff_from: Option<Serial>,
) -> Pin<
Box<
dyn Future<
Output = Result<
XfrData<Self::Diff>,
XfrDataProviderError,
>,
> + Sync
+ Send
+ '_,
>,
>
where
Octs: Octets + Send + Sync;
}
//--- impl XfrDataProvider for Deref<XfrDataProvider>
impl<RequestMeta, T, U> XfrDataProvider<RequestMeta> for U
where
T: XfrDataProvider<RequestMeta> + 'static,
U: Deref<Target = T>,
{
type Diff = T::Diff;
fn request<Octs>(
&self,
req: &Request<Octs, RequestMeta>,
diff_from: Option<Serial>,
) -> Pin<
Box<
dyn Future<
Output = Result<
XfrData<Self::Diff>,
XfrDataProviderError,
>,
> + Sync
+ Send
+ '_,
>,
>
where
Octs: Octets + Send + Sync,
{
(**self).request(req, diff_from)
}
}
//--- impl XfrDataProvider for Zone
impl<RequestMeta> XfrDataProvider<RequestMeta> for Zone {
type Diff = EmptyZoneDiff;
/// Request data needed to respond to an XFR request.
///
/// Returns Ok(Self, vec![]) if the given apex name and class match this
/// zone, irrespective of the given request or diff range.
///
/// Returns Err if the requested zone is not this zone.
fn request<Octs>(
&self,
req: &Request<Octs, RequestMeta>,
_diff_from: Option<Serial>,
) -> Pin<
Box<
dyn Future<
Output = Result<
XfrData<Self::Diff>,
XfrDataProviderError,
>,
> + Sync
+ Send,
>,
>
where
Octs: Octets + Send + Sync,
{
let res = req
.message()
.sole_question()
.map_err(XfrDataProviderError::ParseError)
.and_then(|q| {
if q.qname() == self.apex_name() && q.qclass() == self.class()
{
Ok(XfrData::new(self.clone(), vec![], false))
} else {
Err(XfrDataProviderError::UnknownZone)
}
});
Box::pin(ready(res))
}
}
//--- impl XfrDataProvider for ZoneTree
impl<RequestMeta> XfrDataProvider<RequestMeta> for ZoneTree {
type Diff = EmptyZoneDiff;
/// Request data needed to respond to an XFR request.
///
/// Returns Ok(zone, vec![]) if the given apex name and class match a zone
/// in this zone tree, irrespective of the given request or diff range.
///
/// Returns Err if the requested zone is not this zone tree.
fn request<Octs>(
&self,
req: &Request<Octs, RequestMeta>,
_diff_from: Option<Serial>,
) -> Pin<
Box<
dyn Future<
Output = Result<
XfrData<Self::Diff>,
XfrDataProviderError,
>,
> + Sync
+ Send,
>,
>
where
Octs: Octets + Send + Sync,
{
let res = req
.message()
.sole_question()
.map_err(XfrDataProviderError::ParseError)
.and_then(|q| {
if let Some(zone) = self.find_zone(q.qname(), q.qclass()) {
Ok(XfrData::new(zone.clone(), vec![], false))
} else {
Err(XfrDataProviderError::UnknownZone)
}
});
Box::pin(ready(res))
}
}
+146
View File
@@ -0,0 +1,146 @@
use std::vec::Vec;
use bytes::Bytes;
use futures_util::{pin_mut, StreamExt};
use tokio::sync::mpsc::Sender;
use tracing::error;
use crate::base::iana::OptRcode;
use crate::base::{Name, Rtype};
use crate::zonetree::{SharedRrset, StoredName, ZoneDiff, ZoneDiffItem};
//------------ DiffFunneler ----------------------------------------------------
pub struct DiffFunneler<Diff> {
qname: StoredName,
zone_soa_rrset: SharedRrset,
diffs: Vec<Diff>,
batcher_tx: Sender<(Name<Bytes>, SharedRrset)>,
}
impl<Diff> DiffFunneler<Diff>
where
Diff: ZoneDiff,
{
pub fn new(
qname: StoredName,
zone_soa_rrset: SharedRrset,
diffs: Vec<Diff>,
batcher_tx: Sender<(Name<Bytes>, SharedRrset)>,
) -> Self {
Self {
qname,
zone_soa_rrset,
diffs,
batcher_tx,
}
}
pub async fn run(self) -> Result<(), OptRcode> {
// https://datatracker.ietf.org/doc/html/rfc1995#section-4
// 4. Response Format
// ...
// "If incremental zone transfer is available, one or more
// difference sequences is returned. The list of difference
// sequences is preceded and followed by a copy of the server's
// current version of the SOA.
//
// Each difference sequence represents one update to the zone
// (one SOA serial change) consisting of deleted RRs and added
// RRs. The first RR of the deleted RRs is the older SOA RR
// and the first RR of the added RRs is the newer SOA RR.
//
// Modification of an RR is performed first by removing the
// original RR and then adding the modified one.
//
// The sequences of differential information are ordered oldest
// first newest last. Thus, the differential sequences are the
// history of changes made since the version known by the IXFR
// client up to the server's current version.
//
// RRs in the incremental transfer messages may be partial. That
// is, if a single RR of multiple RRs of the same RR type changes,
// only the changed RR is transferred."
if let Err(err) = self
.batcher_tx
.send((self.qname.clone(), self.zone_soa_rrset.clone()))
.await
{
error!("Internal error: Failed to send initial IXFR SOA to batcher: {err}");
return Err(OptRcode::SERVFAIL);
}
let qname = self.qname.clone();
for diff in self.diffs {
// 4. Response Format
// "Each difference sequence represents one update to the
// zone (one SOA serial change) consisting of deleted RRs
// and added RRs. The first RR of the deleted RRs is the
// older SOA RR and the first RR of the added RRs is the
// newer SOA RR.
let removed_soa =
diff.get_removed(qname.clone(), Rtype::SOA).await.unwrap(); // The diff MUST have a SOA record
Self::send_diff_section(
&qname,
&self.batcher_tx,
removed_soa,
diff.removed(),
)
.await?;
let added_soa =
diff.get_added(qname.clone(), Rtype::SOA).await.unwrap(); // The diff MUST have a SOA record
Self::send_diff_section(
&qname,
&self.batcher_tx,
added_soa,
diff.added(),
)
.await?;
}
if let Err(err) = self
.batcher_tx
.send((qname.clone(), self.zone_soa_rrset))
.await
{
error!("Internal error: Failed to send final IXFR SOA to batcher: {err}");
return Err(OptRcode::SERVFAIL);
}
Ok(())
}
async fn send_diff_section(
qname: &StoredName,
batcher_tx: &Sender<(Name<Bytes>, SharedRrset)>,
soa: &SharedRrset,
diff_stream: <Diff as ZoneDiff>::Stream<'_>,
) -> Result<(), OptRcode> {
if let Err(err) = batcher_tx.send((qname.clone(), soa.clone())).await
{
error!("Internal error: Failed to send SOA to batcher: {err}");
return Err(OptRcode::SERVFAIL);
}
pin_mut!(diff_stream);
while let Some(item) = diff_stream.next().await {
let (owner, rtype) = item.key();
if *rtype != Rtype::SOA {
let rrset = item.value();
if let Err(err) =
batcher_tx.send((owner.clone(), rrset.clone())).await
{
error!("Internal error: Failed to send RRSET to batcher: {err}");
return Err(OptRcode::SERVFAIL);
}
}
}
Ok(())
}
}
+60
View File
@@ -0,0 +1,60 @@
//! RFC 5936 AXFR and RFC 1995 IXFR request handling middleware.
//!
//! This module provides the [`XfrMiddlewareSvc`] service which responds to
//! [RFC 5936] AXFR and [RFC 1995] IXFR requests to perform entire or
//! incremental difference based zone transfers.
//!
//! Determining which requests to honour and with what data is delegated to a
//! caller supplied implementation of the [`XfrDataProvider`] trait.
//! [`XfrDataProvider`] implementations for [`Zone`] and [`ZoneTree`] are
//! provided allowing those types to be used as-is as XFR data providers with
//! this middleware service.
//!
//! # Requiring TSIG authenticated XFR requests
//!
//! To require XFR requests to be TSIG authenticated, implement
//! `XfrDataProvider<Option<Key>>`, extract the key data using
//! [`Request::metadata()`] and verify that a TSIG key was used to sign the
//! request, and that the name and algorithm of the used key are acceptable to
//! you.
//!
//! You can then use your [`XfrDataProvider`] impl with [`XfrMiddlewareSvc`],
//! and add [`TsigMiddlewareSvc`] directly before [`XfrMiddlewareSvc`] in the
//! middleware layer stack so that the used `Key` is made available from the
//! TSIG middleware to the XFR middleware.
//!
//! # Limitations
//!
//! * RFC 1995 2 Brief Description of the Protocol states: _"To ensure
//! integrity, servers should use UDP checksums for all UDP responses."_.
//! This is not implemented.
//! * RFC 1995 5 Purging Strategy states: _"Information about older versions
//! should be purged if the total length of an IXFR response would be longer
//! than that of an AXFR response."_. This is not implemented.
//! * RFC 1995 6 Optional Condensation of Multiple Versions states: _"An IXFR
//! server may optionally condense multiple difference sequences into a
//! single difference sequence, thus, dropping information on intermediate
//! versions."_. This is not implemented.
//!
//! [RFC 5936]: https://www.rfc-editor.org/info/rfc5936
//! [RFC 1995]: https://www.rfc-editor.org/info/rfc1995
//! [`Request::metadata()`]: crate::net::server::message::Request::metadata
//! [`TsigMiddlewareSvc`]:
//! crate::net::server::middleware::tsig::TsigMiddlewareSvc
//! [`XfrDataProvider`]: super::data_provider::XfrDataProvider
//! [`Zone`]: crate::net::zonetree::Zone
//! [`ZoneTree`]: crate::net::zonetree::ZoneTree
mod axfr;
mod batcher;
mod ixfr;
mod responder;
mod util;
pub mod data_provider;
pub mod service;
pub use data_provider::{XfrData, XfrDataProvider, XfrDataProviderError};
pub use service::XfrMiddlewareSvc;
#[cfg(test)]
mod tests;
+161
View File
@@ -0,0 +1,161 @@
use std::sync::Arc;
use bytes::Bytes;
use octseq::Octets;
use tokio::sync::mpsc::{Receiver, UnboundedSender};
use tokio::sync::Semaphore;
use tracing::{debug, error};
use crate::base::iana::OptRcode;
use crate::base::rdata::RecordData;
use crate::base::wire::Composer;
use crate::base::{Message, Name, Rtype};
use crate::net::server::batcher::ResourceRecordBatcher;
use crate::net::server::middleware::xfr::util::add_to_stream;
use crate::net::server::service::ServiceResult;
use crate::net::server::util::mk_builder_for_target;
use crate::zonetree::{Answer, SharedRrset};
use super::batcher::{BatchReadyError, XfrRrBatcher};
//------------ BatchingRrResponder ---------------------------------------------
pub struct BatchingRrResponder<RequestOctets, Target> {
msg: Arc<Message<RequestOctets>>,
zone_soa_answer: Answer,
batcher_rx: Receiver<(Name<Bytes>, SharedRrset)>,
response_tx: UnboundedSender<ServiceResult<Target>>,
compatibility_mode: bool,
soft_byte_limit: usize,
must_fit_in_single_message: bool,
batcher_semaphore: Arc<Semaphore>,
}
impl<RequestOctets, Target> BatchingRrResponder<RequestOctets, Target>
where
RequestOctets: Octets + Send + Sync + 'static + Unpin,
Target: Composer + Default + Send + Sync + 'static,
{
#[allow(clippy::too_many_arguments)]
pub fn new(
msg: Arc<Message<RequestOctets>>,
zone_soa_answer: Answer,
batcher_rx: Receiver<(Name<Bytes>, SharedRrset)>,
response_tx: UnboundedSender<ServiceResult<Target>>,
compatibility_mode: bool,
soft_byte_limit: usize,
must_fit_in_single_message: bool,
batcher_semaphore: Arc<Semaphore>,
) -> Self {
Self {
msg,
zone_soa_answer,
batcher_rx,
response_tx,
compatibility_mode,
soft_byte_limit,
must_fit_in_single_message,
batcher_semaphore,
}
}
pub async fn run(mut self) -> Result<(), OptRcode> {
// Limit the number of concurrently running XFR batching
// operations.
if self.batcher_semaphore.acquire().await.is_err() {
error!("Internal error: Failed to acquire XFR batcher semaphore");
return Err(OptRcode::SERVFAIL);
}
// SAFETY: msg.sole_question() was already checked in
// get_relevant_question().
let qclass = self.msg.sole_question().unwrap().qclass();
// Note: NSD apparently uses name compresson on AXFR responses
// because AXFR responses they typically contain lots of
// alphabetically ordered duplicate names which compress well. NSD
// limits AXFR responses to 16,383 bytes because DNS name
// compression uses a 14-bit offset (2^14-1=16383) from the start
// of the message to the first occurence of a name instead of
// repeating the name, and name compression is less effective
// over 16383 bytes. (Credit: Wouter Wijngaards)
//
// TODO: Once we start supporting name compression in responses decide
// if we want to behave the same way.
let hard_rr_limit = match self.compatibility_mode {
true => Some(1),
false => None,
};
let mut batcher = XfrRrBatcher::build(
self.msg.clone(),
self.response_tx.clone(),
Some(self.soft_byte_limit),
hard_rr_limit,
self.must_fit_in_single_message,
);
let mut last_rr_rtype = None;
while let Some((owner, rrset)) = self.batcher_rx.recv().await {
for rr in rrset.data() {
last_rr_rtype = Some(rr.rtype());
if let Err(err) =
batcher.push((owner.clone(), qclass, rrset.ttl(), rr))
{
match err {
BatchReadyError::MustFitInSingleMessage => {
// https://datatracker.ietf.org/doc/html/rfc1995#section-2
// 2. Brief Description of the Protocol
// ..
// "If the UDP reply does not fit, the
// query is responded to with a single SOA
// record of the server's current version
// to inform the client that a TCP query
// should be initiated."
debug_assert!(self.must_fit_in_single_message);
debug!("Responding to IXFR with single SOA because response does not fit in a single UDP reply");
let builder = mk_builder_for_target();
let resp = self
.zone_soa_answer
.to_message(&self.msg, builder);
add_to_stream(resp, &self.response_tx);
return Ok(());
}
BatchReadyError::PushError(err) => {
error!("Internal error: Failed to send RR to batcher: {err}");
return Err(OptRcode::SERVFAIL);
}
BatchReadyError::SendError => {
debug!("Batcher was unable to send completed batch. Was the receiver dropped?");
return Err(OptRcode::SERVFAIL);
}
}
}
}
}
if let Err(err) = batcher.finish() {
debug!("Batcher was unable to finish: {err}");
return Err(OptRcode::SERVFAIL);
}
if last_rr_rtype != Some(Rtype::SOA) {
error!(
"Internal error: Last RR was {}, expected SOA",
last_rr_rtype.unwrap()
);
return Err(OptRcode::SERVFAIL);
}
Ok(())
}
}
+754
View File
@@ -0,0 +1,754 @@
use core::future::{ready, Future, Ready};
use core::marker::PhantomData;
use core::ops::ControlFlow;
use std::boxed::Box;
use std::fmt::Debug;
use std::pin::Pin;
use std::sync::Arc;
use std::vec::Vec;
use futures_util::stream::{once, Once, Stream};
use octseq::Octets;
use tokio::sync::mpsc::unbounded_channel;
use tokio::sync::Semaphore;
use tokio_stream::wrappers::UnboundedReceiverStream;
use tracing::{debug, error, info, trace, warn};
use crate::base::iana::{Opcode, OptRcode};
use crate::base::wire::Composer;
use crate::base::{Message, ParsedName, Question, Rtype, Serial, ToName};
use crate::net::server::message::{Request, TransportSpecificContext};
use crate::net::server::middleware::stream::MiddlewareStream;
use crate::net::server::middleware::xfr::axfr::ZoneFunneler;
use crate::net::server::middleware::xfr::data_provider::XfrDataProvider;
use crate::net::server::middleware::xfr::data_provider::XfrDataProviderError;
use crate::net::server::middleware::xfr::ixfr::DiffFunneler;
use crate::net::server::middleware::xfr::responder::BatchingRrResponder;
use crate::net::server::service::{CallResult, Service, ServiceFeedback};
use crate::net::server::util::{mk_builder_for_target, mk_error_response};
use crate::rdata::{Soa, ZoneRecordData};
use crate::zonetree::{
Answer, AnswerContent, ReadableZone, SharedRrset, StoredName,
};
use super::util::{add_to_stream, read_soa};
//------------ Constants -----------------------------------------------------
/// https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4
/// 2.3.4. Size limits
/// "UDP messages 512 octets or less"
const MAX_UDP_MSG_BYTE_LEN: u16 = 512;
/// https://datatracker.ietf.org/doc/html/rfc1035#section-4.2.2
/// 4.2.2. TCP usage
/// "The message is prefixed with a two byte length field which gives the
/// message length, excluding the two byte length field"
const MAX_TCP_MSG_BYTE_LEN: u16 = u16::MAX;
//------------ XfrMiddlewareSvc ----------------------------------------------
/// RFC 5936 AXFR and RFC 1995 IXFR request handling middleware.
///
/// See the [module documentation] for a high level introduction.
///
/// [module documentation]: crate::net::server::middleware::xfr
#[derive(Clone, Debug)]
pub struct XfrMiddlewareSvc<RequestOctets, NextSvc, RequestMeta, XDP> {
/// The upstream [`Service`] to pass requests to and receive responses
/// from.
next_svc: NextSvc,
/// A caller supplied implementation of [`XfrDataProvider`] for
/// determining which requests to answer and with which data.
xfr_data_provider: XDP,
/// A limit on the number of XFR related zone walking operations
/// that may run concurrently.
zone_walking_semaphore: Arc<Semaphore>,
/// A limit on the number of XFR related response batching operations that
/// may run concurrently.
batcher_semaphore: Arc<Semaphore>,
_phantom: PhantomData<(RequestOctets, RequestMeta)>,
}
impl<RequestOctets, NextSvc, RequestMeta, XDP>
XfrMiddlewareSvc<RequestOctets, NextSvc, RequestMeta, XDP>
where
XDP: XfrDataProvider<RequestMeta>,
{
/// Creates a new instance of this middleware.
///
/// Takes an implementation of [`XfrDataProvider`] as a parameter to
/// determine which requests to honour and with which data.
///
/// The `max_concurrency` parameter limits the number of simultaneous zone
/// transfer operations that may occur concurrently without blocking.
#[must_use]
pub fn new(
next_svc: NextSvc,
xfr_data_provider: XDP,
max_concurrency: usize,
) -> Self {
let zone_walking_semaphore =
Arc::new(Semaphore::new(max_concurrency));
let batcher_semaphore = Arc::new(Semaphore::new(max_concurrency));
Self {
next_svc,
xfr_data_provider,
zone_walking_semaphore,
batcher_semaphore,
_phantom: PhantomData,
}
}
}
impl<RequestOctets, NextSvc, RequestMeta, XDP>
XfrMiddlewareSvc<RequestOctets, NextSvc, RequestMeta, XDP>
where
RequestOctets: Octets + Send + Sync + 'static + Unpin,
for<'a> <RequestOctets as octseq::Octets>::Range<'a>: Send + Sync,
NextSvc: Service<RequestOctets, ()> + Clone + Send + Sync + 'static,
NextSvc::Future: Send + Sync + Unpin,
NextSvc::Target: Composer + Default + Send + Sync,
NextSvc::Stream: Send + Sync,
XDP: XfrDataProvider<RequestMeta>,
XDP::Diff: Debug + 'static,
{
/// Pre-process received DNS XFR queries.
///
/// Other types of query will be propagated unmodified to the next
/// middleware or application service in the layered stack of services.
///
/// Data to respond to the query will be requested from the given
/// [`XfrDataProvider`] which will act according to its policy concerning
/// the given [`Request`].
pub async fn preprocess(
zone_walking_semaphore: Arc<Semaphore>,
batcher_semaphore: Arc<Semaphore>,
req: &Request<RequestOctets, RequestMeta>,
xfr_data_provider: XDP,
) -> Result<
ControlFlow<
XfrMiddlewareStream<
NextSvc::Future,
NextSvc::Stream,
<NextSvc::Stream as Stream>::Item,
>,
>,
OptRcode,
> {
let msg = req.message();
// Do we support this type of request?
let Some(q) = Self::get_relevant_question(msg) else {
return Ok(ControlFlow::Continue(()));
};
// https://datatracker.ietf.org/doc/html/rfc1995#section-3
// 3. Query Format
// "The IXFR query packet format is the same as that of a normal DNS
// query, but with the query type being IXFR and the authority
// section containing the SOA record of client's version of the
// zone."
let ixfr_query_serial = if let Ok(Some(Ok(query_soa))) = msg
.authority()
.map(|section| section.limit_to::<Soa<ParsedName<_>>>().next())
{
Some(query_soa.data().serial())
} else {
None
};
if q.qtype() == Rtype::IXFR && ixfr_query_serial.is_none() {
warn!(
"{} for {} from {} refused: IXFR request lacks authority section SOA",
q.qtype(),
q.qname(),
req.client_addr()
);
return Err(OptRcode::FORMERR);
}
// Is transfer allowed for the requested zone for this requestor?
let xfr_data = xfr_data_provider
.request(req, ixfr_query_serial)
.await
.map_err(|err| match err {
XfrDataProviderError::ParseError(err) => {
debug!(
"{} for {} from {} refused: parse error: {err}",
q.qtype(),
q.qname(),
req.client_addr()
);
OptRcode::FORMERR
}
XfrDataProviderError::UnknownZone => {
// https://datatracker.ietf.org/doc/html/rfc5936#section-2.2.1
// 2.2.1 Header Values
// "If a server is not authoritative for the queried
// zone, the server SHOULD set the value to NotAuth(9)"
debug!(
"{} for {} from {} refused: unknown zone",
q.qtype(),
q.qname(),
req.client_addr()
);
OptRcode::NOTAUTH
}
XfrDataProviderError::TemporarilyUnavailable => {
// The zone is not yet loaded or has expired, both of
// which are presumably transient conditions and thus
// SERVFAIL is the appropriate response, not NOTAUTH, as
// we know we are supposed to be authoritative for the
// zone but we just don't have the data right now.
warn!(
"{} for {} from {} refused: zone not currently available",
q.qtype(),
q.qname(),
req.client_addr()
);
OptRcode::SERVFAIL
}
XfrDataProviderError::Refused => {
warn!(
"{} for {} from {} refused: access denied",
q.qtype(),
q.qname(),
req.client_addr()
);
OptRcode::REFUSED
}
})?;
// Read the zone SOA RR
let read = xfr_data.zone().read();
let Ok(zone_soa_answer) = read_soa(&read, q.qname().to_name()).await
else {
debug!(
"{} for {} from {} refused: name is outside the zone",
q.qtype(),
q.qname(),
req.client_addr()
);
return Err(OptRcode::SERVFAIL);
};
match q.qtype() {
Rtype::AXFR if req.transport_ctx().is_udp() => {
// https://datatracker.ietf.org/doc/html/rfc5936#section-4.2
// 4.2. UDP
// "With the addition of EDNS0 and applications that require
// many small zones, such as in web hosting and some ENUM
// scenarios, AXFR sessions on UDP would now seem
// desirable. However, there are still some aspects of
// AXFR sessions that are not easily translated to UDP.
//
// Therefore, this document does not update RFC 1035 in
// this respect: AXFR sessions over UDP transport are not
// defined."
warn!(
"{} for {} from {} refused: AXFR not supported over UDP",
q.qtype(),
q.qname(),
req.client_addr()
);
let response = mk_error_response(msg, OptRcode::NOTIMP);
let res = Ok(CallResult::new(response));
Ok(ControlFlow::Break(MiddlewareStream::Map(once(ready(
res,
)))))
}
Rtype::AXFR | Rtype::IXFR if xfr_data.diffs().is_empty() => {
if q.qtype() == Rtype::IXFR && xfr_data.diffs().is_empty() {
// https://datatracker.ietf.org/doc/html/rfc1995#section-4
// 4. Response Format
// "If incremental zone transfer is not available, the
// entire zone is returned. The first and the last RR of
// the response is the SOA record of the zone. I.e. the
// behavior is the same as an AXFR response except the
// query type is IXFR."
info!(
"IXFR for {} (serial {} from {}: diffs not available, falling back to AXFR",
q.qname(),
ixfr_query_serial.unwrap(), // SAFETY: Always Some() if IXFR
req.client_addr()
);
} else {
info!(
"AXFR for {} from {}",
q.qname(),
req.client_addr()
);
}
let stream = Self::respond_to_axfr_query(
zone_walking_semaphore,
batcher_semaphore,
req,
q.qname().to_name(),
&zone_soa_answer,
read,
xfr_data.compatibility_mode(),
)
.await?;
Ok(ControlFlow::Break(stream))
}
Rtype::IXFR => {
// SAFETY: Always Some() if IXFR
let ixfr_query_serial = ixfr_query_serial.unwrap();
info!(
"IXFR for {} (serial {ixfr_query_serial}) from {}",
q.qname(),
req.client_addr()
);
// https://datatracker.ietf.org/doc/html/rfc1995#section-2
// 2. Brief Description of the Protocol
// "Transport of a query may be by either UDP or TCP. If an
// IXFR query is via UDP, the IXFR server may attempt to
// reply using UDP if the entire response can be contained
// in a single DNS packet. If the UDP reply does not fit,
// the query is responded to with a single SOA record of
// the server's current version to inform the client that a
// TCP query should be initiated."
let stream = Self::respond_to_ixfr_query(
batcher_semaphore.clone(),
req,
ixfr_query_serial,
q.qname().to_name(),
&zone_soa_answer,
xfr_data.into_diffs(),
)
.await?;
Ok(ControlFlow::Break(stream))
}
_ => {
// Other QTYPEs should have been filtered out by get_relevant_question().
unreachable!();
}
}
}
/// Generate and send an AXFR response for a given request and zone.
#[allow(clippy::too_many_arguments)]
async fn respond_to_axfr_query<T>(
zone_walk_semaphore: Arc<Semaphore>,
batcher_semaphore: Arc<Semaphore>,
req: &Request<RequestOctets, T>,
qname: StoredName,
zone_soa_answer: &Answer,
read: Box<dyn ReadableZone>,
compatibility_mode: bool,
) -> Result<
XfrMiddlewareStream<
NextSvc::Future,
NextSvc::Stream,
<NextSvc::Stream as Stream>::Item,
>,
OptRcode,
> {
let AnswerContent::Data(zone_soa_rrset) =
zone_soa_answer.content().clone()
else {
error!(
"AXFR for {qname} from {} refused: zone lacks SOA RR",
req.client_addr()
);
return Err(OptRcode::SERVFAIL);
};
if compatibility_mode {
trace!(
"Compatibility mode enabled for client with IP address {}",
req.client_addr().ip()
);
}
// Return a stream of response messages containing:
// - SOA
// - RRSETs, one or more per response message
// - SOA
//
// Neither RFC 5936 nor RFC 1035 defined AXFR for UDP, only for TCP.
// However, RFC 1995 says that for IXFR if no diffs are available the
// full zone should be served just as with AXFR, and that UDP is
// supported as long as the entire XFR response fits in a single
// datagram. Thus we don't check for UDP or TCP here, except to abort
// if the response is too large to fit in a single UDP datagram,
// instead we let the caller that has the context decide whether AXFR
// is supported or not.
//
// References:
// - https://datatracker.ietf.org/doc/html/rfc1995#section-2
// - https://datatracker.ietf.org/doc/html/rfc1995#section-4
// - https://datatracker.ietf.org/doc/html/rfc5936#section-4.2
let soft_byte_limit = Self::calc_msg_bytes_available(req);
// Create a stream that will be immediately returned to the caller.
// Async tasks will then push DNS response messages into the stream as
// they become available.
let (response_tx, response_rx) = unbounded_channel();
let stream = UnboundedReceiverStream::new(response_rx);
// Create a bounded queue for passing RRsets found during zone walking
// to a task which will batch the RRs together before pushing them
// into the result stream.
let (batcher_tx, batcher_rx) =
tokio::sync::mpsc::channel::<(StoredName, SharedRrset)>(100);
let must_fit_in_single_message =
matches!(req.transport_ctx(), TransportSpecificContext::Udp(_));
if !must_fit_in_single_message {
// Notify the underlying transport to expect a stream of related
// responses. The transport should modify its behaviour to account
// for the potentially slow and long running nature of a
// transaction.
add_to_stream(ServiceFeedback::BeginTransaction, &response_tx);
}
// Enqueue the zone SOA RRset for the batcher to process.
if batcher_tx
.send((qname.clone(), zone_soa_rrset.clone()))
.await
.is_err()
{
return Err(OptRcode::SERVFAIL);
}
let msg = req.message().clone();
// Stream the remaining non-SOA zone RRsets in the background to the
// batcher.
let zone_funneler = ZoneFunneler::new(
read,
qname,
zone_soa_rrset,
batcher_tx,
zone_walk_semaphore,
);
let batching_responder = BatchingRrResponder::new(
req.message().clone(),
zone_soa_answer.clone(),
batcher_rx,
response_tx.clone(),
compatibility_mode,
soft_byte_limit,
must_fit_in_single_message,
batcher_semaphore,
);
let cloned_msg = msg.clone();
let cloned_response_tx = response_tx.clone();
// Start the funneler. It will walk the zone and send all of the RRs
// one at a time to the batching responder.
tokio::spawn(async move {
if let Err(rcode) = zone_funneler.run().await {
add_to_stream(
mk_error_response(&cloned_msg, rcode),
&cloned_response_tx,
);
}
});
// Start the batching responder. It will receive RRs from the funneler
// and push them in batches into the response stream.
tokio::spawn(async move {
match batching_responder.run().await {
Ok(()) => {
trace!("Ending transaction");
add_to_stream(
ServiceFeedback::EndTransaction,
&response_tx,
);
}
Err(rcode) => {
add_to_stream(
mk_error_response(&msg, rcode),
&response_tx,
);
}
}
});
// If either the funneler or batcher responder terminate then so will
// the other as they each own half of a send <-> receive channel and
// abort if the other side of the channel is gone.
Ok(MiddlewareStream::Result(stream))
}
// Generate and send an IXFR response for the given request and zone
// diffs.
#[allow(clippy::too_many_arguments)]
async fn respond_to_ixfr_query<T>(
batcher_semaphore: Arc<Semaphore>,
req: &Request<RequestOctets, T>,
query_serial: Serial,
qname: StoredName,
zone_soa_answer: &Answer,
diffs: Vec<XDP::Diff>,
) -> Result<
XfrMiddlewareStream<
NextSvc::Future,
NextSvc::Stream,
<NextSvc::Stream as Stream>::Item,
>,
OptRcode,
>
where
XDP::Diff: Send + 'static,
{
let msg = req.message();
let AnswerContent::Data(zone_soa_rrset) =
zone_soa_answer.content().clone()
else {
return Err(OptRcode::SERVFAIL);
};
let Some(first_rr) = zone_soa_rrset.first() else {
return Err(OptRcode::SERVFAIL);
};
let ZoneRecordData::Soa(soa) = first_rr.data() else {
return Err(OptRcode::SERVFAIL);
};
// Note: Unlike RFC 5936 for AXFR, neither RFC 1995 nor RFC 9103 say
// anything about whether an IXFR response can consist of more than
// one response message, but given the 2^16 byte maximum response size
// of a TCP DNS message and the 2^16 maximum number of ANSWER RRs
// allowed per DNS response, large zones may not fit in a single
// response message and will have to be split into multiple response
// messages.
// https://datatracker.ietf.org/doc/html/rfc1995#section-2
// 2. Brief Description of the Protocol
// "If an IXFR query with the same or newer version number than that
// of the server is received, it is replied to with a single SOA
// record of the server's current version, just as in AXFR."
// ^^^^^^^^^^^^^^^
// Errata https://www.rfc-editor.org/errata/eid3196 points out that
// this is NOT "just as in AXFR" as AXFR does not do that.
if query_serial >= soa.serial() {
trace!("Responding to IXFR with single SOA because query serial >= zone serial");
let builder = mk_builder_for_target();
let response = zone_soa_answer.to_message(msg, builder);
let res = Ok(CallResult::new(response));
return Ok(MiddlewareStream::Map(once(ready(res))));
}
// TODO: Add something like the Bind `max-ixfr-ratio` option that
// "sets the size threshold (expressed as a percentage of the size of
// the full zone) beyond which named chooses to use an AXFR response
// rather than IXFR when answering zone transfer requests"?
let soft_byte_limit = Self::calc_msg_bytes_available(req);
// Create a stream that will be immediately returned to the caller.
// Async tasks will then push DNS response messages into the stream as
// they become available.
let (response_tx, response_rx) = unbounded_channel();
let stream = UnboundedReceiverStream::new(response_rx);
// Create a bounded queue for passing RRsets found during diff walking
// to a task which will batch the RRs together before pushing them
// into the result stream.
let (batcher_tx, batcher_rx) =
tokio::sync::mpsc::channel::<(StoredName, SharedRrset)>(100);
let must_fit_in_single_message =
matches!(req.transport_ctx(), TransportSpecificContext::Udp(_));
if !must_fit_in_single_message {
// Notify the underlying transport to expect a stream of related
// responses. The transport should modify its behaviour to account
// for the potentially slow and long running nature of a
// transaction.
add_to_stream(ServiceFeedback::BeginTransaction, &response_tx);
}
// Stream the IXFR diffs in the background to the batcher.
let diff_funneler =
DiffFunneler::new(qname, zone_soa_rrset, diffs, batcher_tx);
let batching_responder = BatchingRrResponder::new(
req.message().clone(),
zone_soa_answer.clone(),
batcher_rx,
response_tx.clone(),
false,
soft_byte_limit,
must_fit_in_single_message,
batcher_semaphore,
);
let cloned_msg = msg.clone();
let cloned_response_tx = response_tx.clone();
// Start the funneler. It will walk the diffs and send all of the RRs
// one at a time to the batching responder.
tokio::spawn(async move {
if let Err(rcode) = diff_funneler.run().await {
add_to_stream(
mk_error_response(&cloned_msg, rcode),
&cloned_response_tx,
);
}
});
let cloned_msg = msg.clone();
// Start the batching responder. It will receive RRs from the funneler
// and push them in batches into the response stream.
tokio::spawn(async move {
match batching_responder.run().await {
Ok(()) => {
trace!("Ending transaction");
add_to_stream(
ServiceFeedback::EndTransaction,
&response_tx,
);
}
Err(rcode) => {
add_to_stream(
mk_error_response(&cloned_msg, rcode),
&response_tx,
);
}
}
});
// If either the funneler or batcher responder terminate then so will
// the other as they each own half of a send <-> receive channel and
// abort if the other side of the channel is gone.
Ok(MiddlewareStream::Result(stream))
}
/// Is this message for us?
///
/// Returns `Some(Question)` if the given query uses OPCODE QUERYY and has
/// a first question with a QTYPE of `AXFR` or `IXFR`, `None` otherwise.
fn get_relevant_question(
msg: &Message<RequestOctets>,
) -> Option<Question<ParsedName<RequestOctets::Range<'_>>>> {
if Opcode::QUERY == msg.header().opcode() && !msg.header().qr() {
if let Ok(q) = msg.sole_question() {
if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) {
return Some(q);
}
}
}
None
}
fn calc_msg_bytes_available<T>(req: &Request<RequestOctets, T>) -> usize {
let bytes_available = match req.transport_ctx() {
TransportSpecificContext::Udp(ctx) => {
let max_msg_size = ctx
.max_response_size_hint()
.unwrap_or(MAX_UDP_MSG_BYTE_LEN);
max_msg_size - req.num_reserved_bytes()
}
TransportSpecificContext::NonUdp(_) => {
MAX_TCP_MSG_BYTE_LEN - req.num_reserved_bytes()
}
};
bytes_available as usize
}
}
//--- impl Service
impl<RequestOctets, NextSvc, RequestMeta, XDP>
Service<RequestOctets, RequestMeta>
for XfrMiddlewareSvc<RequestOctets, NextSvc, RequestMeta, XDP>
where
RequestOctets: Octets + Send + Sync + Unpin + 'static,
for<'a> <RequestOctets as octseq::Octets>::Range<'a>: Send + Sync,
NextSvc: Service<RequestOctets, ()> + Clone + Send + Sync + 'static,
NextSvc::Future: Send + Sync + Unpin,
NextSvc::Target: Composer + Default + Send + Sync,
NextSvc::Stream: Send + Sync,
XDP: XfrDataProvider<RequestMeta> + Clone + Sync + Send + 'static,
XDP::Diff: Debug + Sync,
RequestMeta: Clone + Default + Sync + Send + 'static,
{
type Target = NextSvc::Target;
type Stream = XfrMiddlewareStream<
NextSvc::Future,
NextSvc::Stream,
<NextSvc::Stream as Stream>::Item,
>;
type Future = Pin<Box<dyn Future<Output = Self::Stream> + Send + Sync>>;
fn call(
&self,
request: Request<RequestOctets, RequestMeta>,
) -> Self::Future {
let request = request.clone();
let next_svc = self.next_svc.clone();
let xfr_data_provider = self.xfr_data_provider.clone();
let zone_walking_semaphore = self.zone_walking_semaphore.clone();
let batcher_semaphore = self.batcher_semaphore.clone();
Box::pin(async move {
match Self::preprocess(
zone_walking_semaphore,
batcher_semaphore,
&request,
xfr_data_provider,
)
.await
{
Ok(ControlFlow::Continue(())) => {
let request = request.with_new_metadata(());
let stream = next_svc.call(request).await;
MiddlewareStream::IdentityStream(stream)
}
Ok(ControlFlow::Break(stream)) => stream,
Err(rcode) => {
let response =
mk_error_response(request.message(), rcode);
let res = Ok(CallResult::new(response));
MiddlewareStream::Map(once(ready(res)))
}
}
})
}
}
//------------ XfrMapStream ---------------------------------------------------
pub type XfrResultStream<StreamItem> = UnboundedReceiverStream<StreamItem>;
//------------ XfrMiddlewareStream --------------------------------------------
pub type XfrMiddlewareStream<Future, Stream, StreamItem> = MiddlewareStream<
Future,
Stream,
Once<Ready<StreamItem>>,
XfrResultStream<StreamItem>,
StreamItem,
>;
+824
View File
@@ -0,0 +1,824 @@
use core::future::{ready, Future, Ready};
use core::ops::ControlFlow;
use core::pin::Pin;
use core::str::FromStr;
use core::sync::atomic::{AtomicBool, Ordering};
use std::borrow::ToOwned;
use std::boxed::Box;
use std::fmt::Debug;
use std::sync::Arc;
use std::vec::Vec;
use bytes::Bytes;
use futures_util::stream::Once;
use futures_util::{Stream, StreamExt};
use octseq::Octets;
use tokio::sync::Semaphore;
use tokio::time::Instant;
use crate::base::iana::{Class, OptRcode, Rcode};
use crate::base::{
Message, MessageBuilder, Name, ParsedName, Rtype, Serial, ToName, Ttl,
};
use crate::net::server::message::{
NonUdpTransportContext, Request, TransportSpecificContext,
UdpTransportContext,
};
use crate::net::server::middleware::xfr::data_provider::{
XfrData, XfrDataProvider, XfrDataProviderError,
};
use crate::net::server::service::{
CallResult, Service, ServiceError, ServiceFeedback, ServiceResult,
};
use crate::rdata::{
Aaaa, AllRecordData, Cname, Mx, Ns, Soa, Txt, ZoneRecordData, A,
};
use crate::tsig::{Algorithm, Key, KeyName};
use crate::zonefile::inplace::Zonefile;
use crate::zonetree::types::{EmptyZoneDiff, Rrset};
use crate::zonetree::{
AnswerContent, InMemoryZoneDiff, InMemoryZoneDiffBuilder, SharedRrset,
Zone,
};
use super::service::{XfrMiddlewareStream, XfrMiddlewareSvc};
use super::util::read_soa;
//------------ ExpectedRecords ------------------------------------------------
type ExpectedRecords = Vec<(Name<Bytes>, AllRecordData<Bytes, Name<Bytes>>)>;
//------------ Tests ----------------------------------------------------------
#[tokio::test]
async fn axfr_with_example_zone() {
let zone = load_zone(include_bytes!(
"../../../../../test-data/zonefiles/nsd-example.txt"
));
let req = mk_axfr_request(zone.apex_name(), ());
let res = do_preprocess(zone.clone(), &req).await.unwrap();
let ControlFlow::Break(mut stream) = res else {
panic!("AXFR failed");
};
let zone_soa = get_zone_soa(&zone).await;
let mut expected_records: ExpectedRecords = vec![
(n("example.com"), zone_soa.clone().into()),
(n("example.com"), Ns::new(n("example.com")).into()),
(n("example.com"), A::new(p("192.0.2.1")).into()),
(n("example.com"), Aaaa::new(p("2001:db8::3")).into()),
(n("www.example.com"), Cname::new(n("example.com")).into()),
(n("mail.example.com"), Mx::new(10, n("example.com")).into()),
(n("example.com"), zone_soa.into()),
];
let msg = stream.next().await.unwrap().unwrap();
assert!(matches!(
msg.feedback(),
Some(ServiceFeedback::BeginTransaction)
));
let stream =
assert_stream_eq(req.message(), &mut stream, &mut expected_records)
.await;
let msg = stream.next().await.unwrap().unwrap();
assert!(matches!(
msg.feedback(),
Some(ServiceFeedback::EndTransaction)
));
}
#[tokio::test]
async fn axfr_multi_response() {
let zone = load_zone(include_bytes!(
"../../../../../test-data/zonefiles/big.example.com.txt"
));
let req = mk_axfr_request(zone.apex_name(), ());
let res = do_preprocess(zone.clone(), &req).await.unwrap();
let ControlFlow::Break(mut stream) = res else {
panic!("AXFR failed");
};
let zone_soa = get_zone_soa(&zone).await;
let mut expected_records: ExpectedRecords = vec![
(n("example.com"), zone_soa.clone().into()),
(n("example.com"), Ns::new(n("ns1.example.com")).into()),
(n("example.com"), Ns::new(n("ns2.example.com")).into()),
(n("example.com"), Mx::new(10, n("mail.example.com")).into()),
(n("example.com"), A::new(p("192.0.2.1")).into()),
(n("example.com"), Aaaa::new(p("2001:db8:10::1")).into()),
(n("ns1.example.com"), A::new(p("192.0.2.2")).into()),
(n("ns1.example.com"), Aaaa::new(p("2001:db8:10::2")).into()),
(n("ns2.example.com"), A::new(p("192.0.2.3")).into()),
(n("ns2.example.com"), Aaaa::new(p("2001:db8:10::3")).into()),
(n("mail.example.com"), A::new(p("192.0.2.4")).into()),
(n("mail.example.com"), Aaaa::new(p("2001:db8:10::4")).into()),
];
for i in 1..=10000 {
expected_records.push((
n(&format!("host-{i}.example.com")),
Txt::build_from_slice(b"text").unwrap().into(),
));
}
expected_records.push((n("example.com"), zone_soa.into()));
let msg = stream.next().await.unwrap().unwrap();
assert!(matches!(
msg.feedback(),
Some(ServiceFeedback::BeginTransaction)
));
let stream =
assert_stream_eq(req.message(), &mut stream, &mut expected_records)
.await;
let msg = stream.next().await.unwrap().unwrap();
assert!(matches!(
msg.feedback(),
Some(ServiceFeedback::EndTransaction)
));
}
#[tokio::test]
async fn axfr_delegation_records() {
// https://datatracker.ietf.org/doc/html/rfc5936#section-3.2
}
#[tokio::test]
async fn axfr_glue_records() {
// https://datatracker.ietf.org/doc/html/rfc5936#section-3.3
}
#[tokio::test]
async fn axfr_name_compression_not_yet_supported() {
// https://datatracker.ietf.org/doc/html/rfc5936#section-3.4
}
#[tokio::test]
async fn axfr_occluded_names() {
// https://datatracker.ietf.org/doc/html/rfc5936#section-3.5
}
#[tokio::test]
async fn axfr_not_allowed_over_udp() {
// https://datatracker.ietf.org/doc/html/rfc5936#section-4.2
let zone = load_zone(include_bytes!(
"../../../../../test-data/zonefiles/nsd-example.txt"
));
let req = mk_udp_axfr_request(zone.apex_name(), ());
let res = do_preprocess(zone, &req).await.unwrap();
let ControlFlow::Break(mut stream) = res else {
panic!("AXFR failed");
};
let msg = stream.next().await.unwrap().unwrap();
let resp_builder = msg.into_inner().0.unwrap();
let resp = resp_builder.as_message();
assert_eq!(resp.header().rcode(), Rcode::NOTIMP);
}
#[tokio::test]
async fn ixfr_rfc1995_section7_full_zone_reply() {
// Based on https://datatracker.ietf.org/doc/html/rfc1995#section-7
// initial zone content:
// JAIN.AD.JP. IN SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. (
// 1 600 600 3600000 604800)
// IN NS NS.JAIN.AD.JP.
// NS.JAIN.AD.JP. IN A 133.69.136.1
// NEZU.JAIN.AD.JP. IN A 133.69.136.5
// Final zone content:
let rfc_1995_zone = r#"
JAIN.AD.JP. IN SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. (
3 600 600 3600000 604800)
IN NS NS.JAIN.AD.JP.
NS.JAIN.AD.JP. IN A 133.69.136.1
JAIN-BB.JAIN.AD.JP. IN A 133.69.136.3
JAIN-BB.JAIN.AD.JP. IN A 192.41.197.2
"#;
let zone = load_zone(rfc_1995_zone.as_bytes());
// Create an object that knows how to provide zone and diff data for
// our zone and no diffs.
let zone_with_diffs = ZoneWithDiffs::new(zone.clone(), vec![]);
// The following IXFR query
let req = mk_udp_ixfr_request(zone.apex_name(), Serial(1), ());
let res = do_preprocess(zone_with_diffs, &req).await.unwrap();
let ControlFlow::Break(mut stream) = res else {
panic!("IXFR failed");
};
// could be replied to with the following full zone transfer message:
let zone_soa = get_zone_soa(&zone).await;
let mut expected_records: ExpectedRecords = vec![
(n("JAIN.AD.JP."), zone_soa.clone().into()),
(n("JAIN.AD.JP."), Ns::new(n("NS.JAIN.AD.JP.")).into()),
(n("NS.JAIN.AD.JP."), A::new(p("133.69.136.1")).into()),
(n("JAIN-BB.JAIN.AD.JP."), A::new(p("133.69.136.3")).into()),
(n("JAIN-BB.JAIN.AD.JP."), A::new(p("192.41.197.2")).into()),
(n("JAIN.AD.JP."), zone_soa.into()),
];
assert_stream_eq(req.message(), &mut stream, &mut expected_records).await;
}
#[tokio::test]
async fn ixfr_rfc1995_section7_incremental_reply() {
// Based on https://datatracker.ietf.org/doc/html/rfc1995#section-7
let mut diffs = Vec::new();
// initial zone content:
// JAIN.AD.JP. IN SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. (
// 1 600 600 3600000 604800)
// IN NS NS.JAIN.AD.JP.
// NS.JAIN.AD.JP. IN A 133.69.136.1
// NEZU.JAIN.AD.JP. IN A 133.69.136.5
// Final zone content:
let rfc_1995_zone = r#"
JAIN.AD.JP. IN SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. (
3 600 600 3600000 604800)
IN NS NS.JAIN.AD.JP.
NS.JAIN.AD.JP. IN A 133.69.136.1
JAIN-BB.JAIN.AD.JP. IN A 133.69.136.3
JAIN-BB.JAIN.AD.JP. IN A 192.41.197.2
"#;
let zone = load_zone(rfc_1995_zone.as_bytes());
// Diff 1: NEZU.JAIN.AD.JP. is removed and JAIN-BB.JAIN.AD.JP. is added.
let mut diff = InMemoryZoneDiffBuilder::new();
// -- Remove the old SOA.
let mut rrset = Rrset::new(Rtype::SOA, Ttl::from_secs(0));
let soa = Soa::new(
n("NS.JAIN.AD.JP."),
n("mohta.jain.ad.jp."),
Serial(1),
Ttl::from_secs(600),
Ttl::from_secs(600),
Ttl::from_secs(3600000),
Ttl::from_secs(604800),
);
rrset.push_data(soa.into());
diff.remove(n("JAIN.AD.JP"), Rtype::SOA, SharedRrset::new(rrset));
// -- Remove the A record.
let mut rrset = Rrset::new(Rtype::A, Ttl::from_secs(0));
rrset.push_data(A::new(p("133.69.136.5")).into());
diff.remove(n("NEZU.JAIN.AD.JP"), Rtype::A, SharedRrset::new(rrset));
// -- Add the new SOA.
let mut rrset = Rrset::new(Rtype::SOA, Ttl::from_secs(0));
let soa = Soa::new(
n("NS.JAIN.AD.JP."),
n("mohta.jain.ad.jp."),
Serial(2),
Ttl::from_secs(600),
Ttl::from_secs(600),
Ttl::from_secs(3600000),
Ttl::from_secs(604800),
);
rrset.push_data(soa.into());
diff.add(n("JAIN.AD.JP"), Rtype::SOA, SharedRrset::new(rrset));
// -- Add the new A records.
let mut rrset = Rrset::new(Rtype::A, Ttl::from_secs(0));
rrset.push_data(A::new(p("133.69.136.4")).into());
rrset.push_data(A::new(p("192.41.197.2")).into());
diff.add(n("JAIN-BB.JAIN.AD.JP"), Rtype::A, SharedRrset::new(rrset));
diffs.push(diff.build().unwrap());
// Diff 2: One of the IP addresses of JAIN-BB.JAIN.AD.JP. is changed.
let mut diff = InMemoryZoneDiffBuilder::new();
// -- Remove the old SOA.
let mut rrset = Rrset::new(Rtype::SOA, Ttl::from_secs(0));
let soa = Soa::new(
n("NS.JAIN.AD.JP."),
n("mohta.jain.ad.jp."),
Serial(2),
Ttl::from_secs(600),
Ttl::from_secs(600),
Ttl::from_secs(3600000),
Ttl::from_secs(604800),
);
rrset.push_data(soa.into());
diff.remove(n("JAIN.AD.JP"), Rtype::SOA, SharedRrset::new(rrset));
// Remove the outdated IP address.
let mut rrset = Rrset::new(Rtype::A, Ttl::from_secs(0));
rrset.push_data(A::new(p("133.69.136.4")).into());
diff.remove(n("JAIN-BB.JAIN.AD.JP"), Rtype::A, SharedRrset::new(rrset));
// -- Add the new SOA.
let mut rrset = Rrset::new(Rtype::SOA, Ttl::from_secs(0));
let soa = Soa::new(
n("NS.JAIN.AD.JP."),
n("mohta.jain.ad.jp."),
Serial(3),
Ttl::from_secs(600),
Ttl::from_secs(600),
Ttl::from_secs(3600000),
Ttl::from_secs(604800),
);
rrset.push_data(soa.into());
diff.add(n("JAIN.AD.JP"), Rtype::SOA, SharedRrset::new(rrset));
// Add the updated IP address.
let mut rrset = Rrset::new(Rtype::A, Ttl::from_secs(0));
rrset.push_data(A::new(p("133.69.136.3")).into());
diff.add(n("JAIN-BB.JAIN.AD.JP"), Rtype::A, SharedRrset::new(rrset));
diffs.push(diff.build().unwrap());
// Create an object that knows how to provide zone and diff data for
// our zone and diffs.
let zone_with_diffs = ZoneWithDiffs::new(zone.clone(), diffs);
// The following IXFR query
let req = mk_ixfr_request(zone.apex_name(), Serial(1), ());
let res = do_preprocess(zone_with_diffs, &req).await.unwrap();
let ControlFlow::Break(mut stream) = res else {
panic!("IXFR failed");
};
let zone_soa = get_zone_soa(&zone).await;
// could be replied to with the following incremental message:
let mut expected_records: ExpectedRecords = vec![
(n("JAIN.AD.JP."), zone_soa.clone().into()),
(
n("JAIN.AD.JP."),
Soa::new(
n("NS.JAIN.AD.JP."),
n("mohta.jain.ad.jp."),
Serial(1),
Ttl::from_secs(600),
Ttl::from_secs(600),
Ttl::from_secs(3600000),
Ttl::from_secs(604800),
)
.into(),
),
(n("NEZU.JAIN.AD.JP."), A::new(p("133.69.136.5")).into()),
(
n("JAIN.AD.JP."),
Soa::new(
n("NS.JAIN.AD.JP."),
n("mohta.jain.ad.jp."),
Serial(2),
Ttl::from_secs(600),
Ttl::from_secs(600),
Ttl::from_secs(3600000),
Ttl::from_secs(604800),
)
.into(),
),
(n("JAIN-BB.JAIN.AD.JP."), A::new(p("133.69.136.4")).into()),
(n("JAIN-BB.JAIN.AD.JP."), A::new(p("192.41.197.2")).into()),
(
n("JAIN.AD.JP."),
Soa::new(
n("NS.JAIN.AD.JP."),
n("mohta.jain.ad.jp."),
Serial(2),
Ttl::from_secs(600),
Ttl::from_secs(600),
Ttl::from_secs(3600000),
Ttl::from_secs(604800),
)
.into(),
),
(n("JAIN-BB.JAIN.AD.JP."), A::new(p("133.69.136.4")).into()),
(
n("JAIN.AD.JP."),
Soa::new(
n("NS.JAIN.AD.JP."),
n("mohta.jain.ad.jp."),
Serial(3),
Ttl::from_secs(600),
Ttl::from_secs(600),
Ttl::from_secs(3600000),
Ttl::from_secs(604800),
)
.into(),
),
(n("JAIN-BB.JAIN.AD.JP."), A::new(p("133.69.136.3")).into()),
(n("JAIN.AD.JP."), zone_soa.into()),
];
let msg = stream.next().await.unwrap().unwrap();
assert!(matches!(
msg.feedback(),
Some(ServiceFeedback::BeginTransaction)
));
let stream =
assert_stream_eq(req.message(), &mut stream, &mut expected_records)
.await;
let msg = stream.next().await.unwrap().unwrap();
assert!(matches!(
msg.feedback(),
Some(ServiceFeedback::EndTransaction)
));
}
#[tokio::test]
async fn ixfr_rfc1995_section7_udp_packet_overflow() {
// Based on https://datatracker.ietf.org/doc/html/rfc1995#section-7
let zone = load_zone(include_bytes!(
"../../../../../test-data/zonefiles/big.example.com.txt"
));
let req = mk_udp_ixfr_request(zone.apex_name(), Serial(0), ());
let res = do_preprocess(zone.clone(), &req).await.unwrap();
let ControlFlow::Break(mut stream) = res else {
panic!("IXFR failed");
};
let zone_soa = get_zone_soa(&zone).await;
let mut expected_records: ExpectedRecords =
vec![(n("example.com"), zone_soa.into())];
assert_stream_eq(req.message(), &mut stream, &mut expected_records).await;
}
#[tokio::test]
async fn ixfr_multi_response_tcp() {}
#[tokio::test]
async fn axfr_with_tsig_key() {
// Define an XfrDataProvider that expects to receive a Request that is
// generic over a type that we specify: Authentication. This is the
// type over which the Request produced by TsigMiddlewareSvc is generic.
// When the XfrMiddlewareSvc receives a Request<Octs, Authentication> it
// passes it to the XfrDataProvider which in turn can inspect it.
struct KeyReceivingXfrDataProvider {
key: Arc<Key>,
checked: Arc<AtomicBool>,
}
impl XfrDataProvider<Option<Arc<Key>>> for KeyReceivingXfrDataProvider {
type Diff = EmptyZoneDiff;
#[allow(clippy::type_complexity)]
fn request<Octs>(
&self,
req: &Request<Octs, Option<Arc<Key>>>,
_diff_from: Option<Serial>,
) -> Pin<
Box<
dyn Future<
Output = Result<
XfrData<Self::Diff>,
XfrDataProviderError,
>,
> + Sync
+ Send,
>,
>
where
Octs: Octets + Send + Sync,
{
let key = req.metadata().as_ref().unwrap();
assert_eq!(key.name(), self.key.name());
self.checked.store(true, Ordering::SeqCst);
Box::pin(ready(Err(XfrDataProviderError::Refused)))
}
}
let key_name = KeyName::from_str("some_tsig_key_name").unwrap();
let secret = crate::utils::base64::decode::<Vec<u8>>(
"zlCZbVJPIhobIs1gJNQfrsS3xCxxsR9pMUrGwG8OgG8=",
)
.unwrap();
let key = Arc::new(
Key::new(Algorithm::Sha256, &secret, key_name, None, None).unwrap(),
);
let metadata = Some(key.clone());
let req = mk_axfr_request(n("example.com"), metadata);
let checked = Arc::new(AtomicBool::new(false));
let xdp = KeyReceivingXfrDataProvider {
key,
checked: checked.clone(),
};
// Invoke XfrMiddlewareSvc with our custom XfrDataProvidedr.
let _ = do_preprocess(xdp, &req).await;
// Veirfy that our XfrDataProvider was invoked and received the expected
// TSIG key name data.
assert!(checked.load(Ordering::SeqCst));
}
//------------ Helper functions -------------------------------------------
fn n(name: &str) -> Name<Bytes> {
Name::from_str(name).unwrap()
}
fn p<T: FromStr>(txt: &str) -> T
where
<T as FromStr>::Err: Debug,
{
txt.parse().unwrap()
}
fn load_zone(bytes: &[u8]) -> Zone {
let mut zone_bytes = std::io::BufReader::new(bytes);
let reader = Zonefile::load(&mut zone_bytes).unwrap();
Zone::try_from(reader).unwrap()
}
async fn get_zone_soa(zone: &Zone) -> Soa<Name<Bytes>> {
let read = zone.read();
let zone_soa_answer =
read_soa(&read, zone.apex_name().to_owned()).await.unwrap();
let AnswerContent::Data(zone_soa_rrset) =
zone_soa_answer.content().clone()
else {
unreachable!()
};
let first_rr = zone_soa_rrset.first().unwrap();
let ZoneRecordData::Soa(soa) = first_rr.data() else {
unreachable!()
};
soa.clone()
}
fn mk_axfr_request<T>(
qname: impl ToName,
metadata: T,
) -> Request<Vec<u8>, T> {
mk_axfr_request_for_transport(
qname,
metadata,
TransportSpecificContext::NonUdp(NonUdpTransportContext::new(None)),
)
}
fn mk_udp_axfr_request<T>(
qname: impl ToName,
metadata: T,
) -> Request<Vec<u8>, T> {
mk_axfr_request_for_transport(
qname,
metadata,
TransportSpecificContext::Udp(UdpTransportContext::new(None)),
)
}
fn mk_axfr_request_for_transport<T>(
qname: impl ToName,
metadata: T,
transport_specific: TransportSpecificContext,
) -> Request<Vec<u8>, T> {
let client_addr = "127.0.0.1:12345".parse().unwrap();
let received_at = Instant::now();
let msg = MessageBuilder::new_vec();
let mut msg = msg.question();
msg.push((qname, Rtype::AXFR)).unwrap();
let msg = msg.into_message();
Request::new(client_addr, received_at, msg, transport_specific, metadata)
}
fn mk_ixfr_request<T>(
qname: impl ToName + Clone,
serial: Serial,
metadata: T,
) -> Request<Vec<u8>, T> {
mk_ixfr_request_for_transport(
qname,
serial,
metadata,
TransportSpecificContext::NonUdp(NonUdpTransportContext::new(None)),
)
}
fn mk_udp_ixfr_request<T>(
qname: impl ToName + Clone,
serial: Serial,
metadata: T,
) -> Request<Vec<u8>, T> {
mk_ixfr_request_for_transport(
qname,
serial,
metadata,
TransportSpecificContext::Udp(UdpTransportContext::new(None)),
)
}
fn mk_ixfr_request_for_transport<T>(
qname: impl ToName + Clone,
serial: Serial,
metadata: T,
transport_specific: TransportSpecificContext,
) -> Request<Vec<u8>, T> {
let client_addr = "127.0.0.1:12345".parse().unwrap();
let received_at = Instant::now();
let msg = MessageBuilder::new_vec();
let mut msg = msg.question();
msg.push((qname.clone(), Rtype::IXFR)).unwrap();
let mut msg = msg.authority();
let ttl = Ttl::from_secs(0);
let soa = Soa::new(n("name"), n("rname"), serial, ttl, ttl, ttl, ttl);
msg.push((qname, Class::IN, Ttl::from_secs(0), soa))
.unwrap();
let msg = msg.into_message();
Request::new(client_addr, received_at, msg, transport_specific, metadata)
}
async fn do_preprocess<RequestMeta, XDP: XfrDataProvider<RequestMeta>>(
zone: XDP,
req: &Request<Vec<u8>, RequestMeta>,
) -> Result<
ControlFlow<
XfrMiddlewareStream<
<TestNextSvc as Service>::Future,
<TestNextSvc as Service>::Stream,
<<TestNextSvc as Service>::Stream as Stream>::Item,
>,
>,
OptRcode,
>
where
XDP::Diff: Debug + 'static,
{
XfrMiddlewareSvc::<Vec<u8>, TestNextSvc, RequestMeta, XDP>::preprocess(
Arc::new(Semaphore::new(1)),
Arc::new(Semaphore::new(1)),
req,
zone,
)
.await
}
async fn assert_stream_eq<
O: octseq::Octets,
S: Stream<Item = Result<CallResult<Vec<u8>>, ServiceError>> + Unpin,
>(
req: &Message<O>,
mut stream: S,
expected_records: &mut ExpectedRecords,
) -> S {
while !expected_records.is_empty() {
let msg = stream.next().await.unwrap().unwrap();
let resp_builder = msg.into_inner().0.unwrap();
let resp = resp_builder.as_message();
assert!(resp.is_answer(req));
let mut records = resp.answer().unwrap().peekable();
for (idx, rec) in records.by_ref().enumerate() {
let rec = rec.unwrap();
let rec = rec
.into_record::<AllRecordData<_, ParsedName<_>>>()
.unwrap()
.unwrap();
eprintln!(
"XFR record {idx} {} {} {} {}",
rec.owner(),
rec.class(),
rec.rtype(),
rec.data(),
);
let pos = expected_records
.iter()
.position(|(name, data)| {
name == rec.owner() && data == rec.data()
})
.unwrap_or_else(|| {
panic!(
"XFR record {idx} {} {} {} {} was not expected",
rec.owner(),
rec.class(),
rec.rtype(),
rec.data(),
)
});
let _ = expected_records.remove(pos);
eprintln!("Found {} {} {}", rec.owner(), rec.class(), rec.rtype())
}
assert!(records.next().is_none());
}
stream
}
#[derive(Clone)]
struct TestNextSvc;
impl Service<Vec<u8>, ()> for TestNextSvc {
type Target = Vec<u8>;
type Stream = Once<Ready<ServiceResult<Self::Target>>>;
type Future = Ready<Self::Stream>;
fn call(&self, _request: Request<Vec<u8>, ()>) -> Self::Future {
todo!()
}
}
struct ZoneWithDiffs {
zone: Zone,
diffs: Vec<Arc<InMemoryZoneDiff>>,
}
impl ZoneWithDiffs {
fn new(zone: Zone, diffs: Vec<InMemoryZoneDiff>) -> Self {
Self {
zone,
diffs: diffs.into_iter().map(Arc::new).collect(),
}
}
fn get_diffs(
&self,
diff_from: Option<Serial>,
) -> Vec<Arc<InMemoryZoneDiff>> {
if self.diffs.first().map(|diff| diff.start_serial) == diff_from {
self.diffs.clone()
} else {
vec![]
}
}
}
impl XfrDataProvider for ZoneWithDiffs {
type Diff = Arc<InMemoryZoneDiff>;
fn request<Octs>(
&self,
req: &Request<Octs, ()>,
diff_from: Option<Serial>,
) -> Pin<
Box<
dyn Future<
Output = Result<
XfrData<Self::Diff>,
XfrDataProviderError,
>,
> + Sync
+ Send,
>,
>
where
Octs: Octets + Send + Sync,
{
let res = req
.message()
.sole_question()
.map_err(XfrDataProviderError::ParseError)
.and_then(|q| {
if q.qname() == self.zone.apex_name()
&& q.qclass() == self.zone.class()
{
Ok(XfrData::new(
self.zone.clone(),
self.get_diffs(diff_from),
false,
))
} else {
Err(XfrDataProviderError::UnknownZone)
}
});
Box::pin(ready(res))
}
}
+39
View File
@@ -0,0 +1,39 @@
use std::boxed::Box;
use bytes::Bytes;
use tokio::sync::mpsc::UnboundedSender;
use tracing::error;
use crate::base::{Name, Rtype};
use crate::net::server::service::{CallResult, ServiceResult};
use crate::zonetree::error::OutOfZone;
use crate::zonetree::{Answer, ReadableZone};
//------------ read_soa() -----------------------------------------------------
#[allow(clippy::borrowed_box)]
pub async fn read_soa(
read: &Box<dyn ReadableZone>,
qname: Name<Bytes>,
) -> Result<Answer, OutOfZone> {
match read.is_async() {
true => read.query_async(qname, Rtype::SOA).await,
false => read.query(qname, Rtype::SOA),
}
}
//------------ add_to_stream() ------------------------------------------------
pub fn add_to_stream<Target, T: Into<CallResult<Target>>>(
call_result: T,
response_tx: &UnboundedSender<ServiceResult<Target>>,
) {
if response_tx.send(Ok(call_result.into())).is_err() {
// We failed to write the message into the response stream. This
// shouldn't happen. We can't now return an error to the client
// because that would require writing to the response stream as well.
// We don't want to panic and take down the entire application, so
// instead just log.
error!("Failed to send DNS message to the internal response stream");
}
}
+44
View File
@@ -105,6 +105,50 @@
//! [`Service`] trait yourself and passing an instance of that service to the
//! server or middleware service as input.
//!
//! ## Zone maintenance and zone transfers
//!
//! This crate provides everything you need to do zone maintenance, i.e.
//! serving entire zones to clients and keeping your own zones synchronized
//! with those of a primary server.
//!
//! If acting as a primary nameserver:
//! - Use [`XfrMiddlewareSvc`] to respond to AXFR and IXFR requests from
//! secondary nameservers.
//! - Implement [`XfrDataProvider`] to define your XFR access policy.
//! - Create [`ZoneDiff`]s when making changes to [`Zone`] content and make
//! those diffs available via your [`XfrDataProvider`] implementation.
//! - Use [`TsigMiddlewareSvc`] to authenticate transfer requests from
//! secondary nameservers.
//! - Use the UDP client support in `net::client` to send out NOTIFY messages
//! on zone change.
//!
//! If acting as a secondary nameserver:
//! - Use [`NotifyMiddlewareSvc`] to detect changes at the primary to zones
//! that you are mirroring.
//! - Use the TCP client support in [`net::client`] to make outbound XFR
//! requests on SOA timer expiration or NOTIFY to fetch changes to zone
//! content.
//! - Use [`net::client::tsig`] to authenticate your transfer requests to
//! primary nameservers.
//! - Use [`XfrResponseInterpreter`] and [`ZoneUpdater`] to parse transfer
//! responses and apply the changes to your zones.
//!
//! Additionally you may wish to use [`ZoneTree`] to simplify serving multiple
//! zones.
//!
//! [`net::client`]: crate::net::client
//! [`net::client::tsig`]: crate::net::client::tsig
//! [`NotifyMiddlewareSvc`]: middleware::notify::NotifyMiddlewareSvc
//! [`TsigMiddlewareSvc`]: middleware::tsig::TsigMiddlewareSvc
//! [`XfrMiddlewareSvc`]: middleware::xfr::XfrMiddlewareSvc
//! [`XfrDataProvider`]: middleware::xfr::XfrDataProvider
//! [`XfrResponseInterpreter`]:
//! crate::net::xfr::protocol::XfrResponseInterpreter
//! [`Zone`]: crate::zonetree::Zone
//! [`ZoneDiff`]: crate::zonetree::ZoneDiff
//! [`ZoneTree`]: crate::zonetree::ZoneTree
//! [`ZoneUpdater`]: crate::zonetree::update::ZoneUpdater
//!
//! # Advanced
//!
//! ## Memory allocation
+18
View File
@@ -374,3 +374,21 @@ impl<Target> CallResult<Target> {
(response, feedback)
}
}
//--- From<AdditionalBuilder>
impl<Target> From<AdditionalBuilder<StreamTarget<Target>>>
for CallResult<Target>
{
fn from(response: AdditionalBuilder<StreamTarget<Target>>) -> Self {
Self::new(response)
}
}
//--- From<ServiceFeedback>
impl<Target> From<ServiceFeedback> for CallResult<Target> {
fn from(feedback: ServiceFeedback) -> Self {
Self::feedback_only(feedback)
}
}
+90 -81
View File
@@ -1,3 +1,6 @@
use core::future::{ready, Future};
use core::ops::Deref;
use core::pin::Pin;
use core::str::FromStr;
use std::boxed::Box;
@@ -6,6 +9,7 @@ use std::fs::File;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::result::Result;
use std::string::{String, ToString};
use std::sync::Arc;
use std::time::Duration;
use std::vec::Vec;
@@ -16,9 +20,10 @@ use tracing::instrument;
use tracing::{trace, warn};
use crate::base::iana::{Class, Rcode};
use crate::base::name::{Name, ToName};
use crate::base::name::ToName;
use crate::base::net::IpAddr;
use crate::base::wire::Composer;
use crate::base::Name;
use crate::base::Rtype;
use crate::net::client::request::{RequestMessage, RequestMessageMulti};
use crate::net::client::{dgram, stream, tsig};
@@ -32,6 +37,8 @@ use crate::net::server::middleware::mandatory::MandatoryMiddlewareSvc;
use crate::net::server::middleware::notify::{
Notifiable, NotifyError, NotifyMiddlewareSvc,
};
use crate::net::server::middleware::tsig::TsigMiddlewareSvc;
use crate::net::server::middleware::xfr::XfrMiddlewareSvc;
use crate::net::server::service::{CallResult, Service, ServiceResult};
use crate::net::server::stream::StreamServer;
use crate::net::server::util::{mk_builder_for_target, service_fn};
@@ -44,11 +51,9 @@ use crate::stelline::parse_stelline::{self, parse_file, Config, Matches};
use crate::stelline::simple_dgram_client;
use crate::tsig::{Algorithm, Key, KeyName, KeyStore};
use crate::utils::base16;
use crate::zonefile::inplace::{Entry, ScannedRecord, Zonefile};
use crate::zonetree::StoredName;
use core::future::{ready, Future};
use core::pin::Pin;
use std::string::ToString;
use crate::zonefile::inplace::Zonefile;
use crate::zonetree::{Answer, Zone};
use crate::zonetree::{StoredName, ZoneBuilder, ZoneTree};
//----------- Tests ----------------------------------------------------------
@@ -57,8 +62,6 @@ use std::string::ToString;
///
/// Note: Adding or removing .rpl files on disk won't be detected until the
/// test is re-compiled.
// #[cfg(feature = "mock-time")] # Needed for the cookies test but that is
// currently disabled by renaming it to .rpl.not.
#[instrument(skip_all, fields(rpl = rpl_file.file_name().unwrap().to_str()))]
#[rstest]
#[tokio::test(start_paused = true)]
@@ -70,8 +73,6 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) {
// Initialize tracing based logging. Override with env var RUST_LOG, e.g.
// RUST_LOG=trace. DEBUG level will show the .rpl file name, Stelline step
// numbers and types as they are being executed.
use crate::net::server::middleware::tsig::TsigMiddlewareSvc;
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_thread_ids(true)
@@ -100,7 +101,34 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) {
let dgram_server_conn = ClientServerChannel::new_dgram();
let stream_server_conn = ClientServerChannel::new_stream();
let zonefile = server_config.zonefile.clone();
// Build the test defined zone, if any.
let mut zones = ZoneTree::new();
let zone = match &server_config.zone {
ServerZone {
zone_file: Some(zone_file),
..
} => {
// This is a primary zone with content already defined.
Some(Zone::try_from(zone_file.clone()).unwrap())
}
ServerZone {
zone_name: Some(zone_name),
zone_file: None,
} => {
// This is a secondary zone with content to be received via
// XFR.
let builder = ZoneBuilder::new(
Name::from_str(zone_name).unwrap(),
Class::IN,
);
Some(builder.build())
}
_ => None,
};
if let Some(zone) = zone {
zones.insert_zone(zone).unwrap();
}
let zones = Arc::new(zones);
let with_cookies = server_config.cookies.enabled
&& server_config.cookies.secret.is_some();
@@ -120,7 +148,7 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) {
// it and without it having to know or do anything about it.
// 1. Application logic service
let svc = service_fn(test_service, zonefile);
let svc = service_fn(test_service, zones.clone());
// 2. DNS COOKIES middleware service
let svc = CookiesMiddlewareSvc::new(svc, secret)
@@ -131,13 +159,18 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) {
let svc =
EdnsMiddlewareSvc::new(svc).enable(server_config.edns_tcp_keepalive);
// 4. Mandatory DNS behaviour (e.g. RFC 1034/35 rules).
let svc = MandatoryMiddlewareSvc::new(svc);
// 4. RFC 5936 AXFR and RFC 1995 IXFR middleware service.
let svc = XfrMiddlewareSvc::<Vec<u8>, _, Option<Arc<Key>>, _>::new(
svc, zones, 1,
);
// 5. RFC 1996 NOTIFY support.
let svc = NotifyMiddlewareSvc::new(svc, TestNotifyTarget);
// 6. TSIG message authentication.
// 6. Mandatory DNS behaviour (e.g. RFC 1034/35 rules).
let svc = MandatoryMiddlewareSvc::new(svc);
// 7. TSIG message authentication.
let svc = TsigMiddlewareSvc::new(svc, key_store.clone());
// NOTE: TSIG middleware *MUST* be the first middleware in the chain per
@@ -145,7 +178,8 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) {
// in order to verify the signature, and has to sign outgoing messages in
// their final state without any modification occuring thereafter.
// Create dgram and stream servers for answering requests
// 8. The dgram and stream servers that receive DNS queries and dispatch
// them to the service layers above.
let (dgram_srv, stream_srv) = mk_servers(
svc,
&server_config,
@@ -153,7 +187,11 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) {
stream_server_conn.clone(),
);
// Create a client factory for sending requests
// Create a client factory for creating DNS clients per Stelline STEP with
// the appropriate configuration (as defined by the .rpl content) to
// submit requests to our DNS servers. No actual network communication
// takes place, these clients and servers use a direct in-memory channel
// to exchange messages instead of actual network sockets.
let client_factory =
mk_client_factory(dgram_server_conn, stream_server_conn, key_store);
@@ -369,70 +407,40 @@ fn mk_server_configs(
#[allow(clippy::type_complexity)]
fn test_service<RequestMeta>(
request: Request<Vec<u8>, RequestMeta>,
zonefile: Zonefile,
zones: Arc<ZoneTree>,
) -> ServiceResult<Vec<u8>> {
fn as_record_and_dname(
r: ScannedRecord,
) -> Option<(ScannedRecord, Name<Vec<u8>>)> {
let dname = r.owner().to_name();
Some((r, dname))
}
let question = request.message().sole_question().unwrap();
fn as_records(
e: Result<Entry, crate::zonefile::inplace::Error>,
) -> Option<ScannedRecord> {
match e {
Ok(Entry::Record(r)) => Some(r),
Ok(_) => None,
Err(err) => panic!(
"Error while extracting records from the zonefile: {err}"
),
let answer = match zones.find_zone(question.qname(), question.qclass()) {
Some(zone) => {
let readable_zone = zone.read();
let qname = question.qname().to_bytes();
let qtype = question.qtype();
readable_zone.query(qname, qtype).unwrap()
}
}
None => Answer::new(Rcode::NXDOMAIN),
};
trace!("Service received request");
trace!("Service is constructing a single response");
// If given a single question:
let answer = request
.message()
.sole_question()
.ok()
.and_then(|q| {
// Walk the zone to find the queried name
zonefile
.clone()
.filter_map(as_records)
.filter_map(as_record_and_dname)
.find(|(_record, dname)| dname == q.qname())
})
.map_or_else(
|| {
// The Qname was not found in the zone:
mk_builder_for_target()
.start_answer(request.message(), Rcode::NXDOMAIN)
.unwrap()
},
|(record, _)| {
// Respond with the found record:
let mut answer = mk_builder_for_target()
.start_answer(request.message(), Rcode::NOERROR)
.unwrap();
answer.push(record).unwrap();
answer
},
);
Ok(CallResult::new(answer.additional()))
let builder = mk_builder_for_target();
let additional = answer.to_message(request.message(), builder);
Ok(CallResult::new(additional))
}
//----------- Stelline config block parsing -----------------------------------
#[derive(Default)]
struct ServerZone {
/// Used for an empty secondary zone. Ignored if zone_file is Some.
zone_name: Option<String>,
zone_file: Option<Zonefile>,
}
#[derive(Default)]
struct ServerConfig<'a> {
cookies: CookieConfig<'a>,
edns_tcp_keepalive: bool,
idle_timeout: Option<Duration>,
zonefile: Zonefile,
zone: ServerZone,
}
#[derive(Default)]
@@ -444,8 +452,9 @@ struct CookieConfig<'a> {
fn parse_server_config(config: &Config) -> ServerConfig {
let mut parsed_config = ServerConfig::default();
let mut zone_file_bytes = VecDeque::<u8>::new();
let mut in_server_block = false;
let mut zone_name = None;
let mut zone_file_bytes = VecDeque::<u8>::new();
for line in config.lines() {
if line.starts_with("server:") {
@@ -498,9 +507,6 @@ fn parse_server_config(config: &Config) -> ServerConfig {
}
}
("local-data", v) => {
if !zone_file_bytes.is_empty() {
zone_file_bytes.push_back(b'\n');
}
zone_file_bytes
.extend(v.trim_matches('"').as_bytes().iter());
zone_file_bytes.push_back(b'\n');
@@ -515,6 +521,10 @@ fn parse_server_config(config: &Config) -> ServerConfig {
);
}
}
("zone", v) => {
// zone: <name>
zone_name = Some(v.to_string());
}
_ => {
eprintln!("Ignoring unknown server setting '{setting}' with value: {value}");
}
@@ -523,10 +533,13 @@ fn parse_server_config(config: &Config) -> ServerConfig {
}
}
if !zone_file_bytes.is_empty() {
parsed_config.zonefile =
Zonefile::load(&mut zone_file_bytes).unwrap();
}
let zone_file = (!zone_file_bytes.is_empty())
.then(|| Zonefile::load(&mut zone_file_bytes).unwrap());
parsed_config.zone = ServerZone {
zone_name,
zone_file,
};
parsed_config
}
@@ -570,10 +583,6 @@ impl KeyStore for Arc<TestKeyStore> {
name: &N,
algorithm: Algorithm,
) -> Option<Self::Key> {
if let Ok(name) = name.try_to_name() {
self.get(&(name, algorithm)).cloned()
} else {
None
}
Arc::deref(self).get_key(name, algorithm)
}
}
+64 -1
View File
@@ -4,5 +4,68 @@
)]
// #![warn(missing_docs)]
// #![warn(clippy::missing_docs_in_private_items)]
//! XFR protocol related functionality.
//! XFR related functionality.
//!
//! # What is XFR?
//!
//! XFR refers to the protocols used to transfer entire zones between
//! nameservers.
//!
//! There are two XFR protocols and a couple of protocols often used in
//! combination with XFR:
//!
//! - AXFR defined by [RFC 5936] "DNS Zone Transfer Protocol (AXFR)"
//! - IXFR defined by [RFC 1995] "Incremental Zone Transfer in DNS"
//! - NOTIFY defined by [RFC 1996] "A Mechanism for Prompt Notification of
//! Zone Changes"
//! - TSIG defined by [RFC 8945] "Secret Key Transaction Authentication for
//! DNS (TSIG)"
//!
//! AXFR is used to transfer a complete zone via one or more DNS responses.
//!
//! IXFR is used to incrementally apply the changes that occur to a zone on
//! one nameserver to the same zone on another server, assuming that the
//! latter server has a reasonably up-to-date copy of the zone.
//!
//! NOTIFY allows the server that holds the primary copy of a zone to notify
//! interested servers that the zone has changed and should be re-fetched.
//!
//! TSIG can be used to sign XFR requests and responses to authenticate the
//! servers involved to each other.
//!
//! # XFR support available in this crate
//!
//! Sending requests & handling responses:
//! - [`net::client::stream`] supports sending of XFR requests and receiving
//! one or more responses via [`RequestMessageMulti`].
//! - [`net::client::tsig`] can be wrapped around another transport to add
//! TSIG request signing and response validation.
//! - [`net::xfr::protocol::XfrResponseInterpreter`] can be used to parse
//! those XFR responses into [`ZoneUpdate`]s.
//! - [`zonetree::update::ZoneUpdater`] can then be used to apply those
//! updates to a [`Zone`].
//!
//! Responding to requests:
//! - [`net::server::middleware::xfr::XfrMiddlewareSvc`] can respond to
//! XFR requests with zone transfer responses.
//! - [`net::server::middleware::tsig::TsigMiddlewareSvc`] can validate
//! request signatures and sign transer responses.
//! - [`net::server::middleware::notify::NotifyMiddlewareSvc`] can invoke
//! a user supplied callback when a NOTIFY request is received.
//!
//! [RFC 5936]: https://www.rfc-editor.org/info/rfc5936
//! [RFC 1995]: https://www.rfc-editor.org/info/rfc1995
//! [RFC 1996]: https://www.rfc-editor.org/info/rfc1996
//! [RFC 8945]: https://www.rfc-editor.org/info/rfc8945
//! [`net::client::stream`]: crate::net::client::stream
//! [`RequestMessageMulti`]: crate::net::client::request::RequestMessageMulti
//! [`net::client::tsig`]: crate::net::client::tsig
//! [`net::xfr::protocol::XfrResponseInterpreter`]: crate::net::xfr::protocol::XfrResponseInterpreter
//! [`ZoneUpdate`]: crate::zonetree::types::ZoneUpdate
//! [`zonetree::update::ZoneUpdater`]: crate::zonetree::update::ZoneUpdater
//! [`Zone`]: crate::zonetree::Zone
//! [`net::server::middleware::xfr::XfrMiddlewareSvc`]: crate::net::server::middleware::xfr::XfrMiddlewareSvc
//! [`net::server::middleware::tsig::TsigMiddlewareSvc`]: crate::net::server::middleware::tsig::TsigMiddlewareSvc
//! [`net::server::middleware::notify::NotifyMiddlewareSvc`]: crate::net::server::middleware::notify::NotifyMiddlewareSvc
pub mod protocol;
+33 -2
View File
@@ -5,10 +5,10 @@ use octseq::Octets;
use crate::base::iana::Rcode;
use crate::base::message_builder::AdditionalBuilder;
use crate::base::wire::Composer;
use crate::base::Message;
use crate::base::MessageBuilder;
use crate::base::{Message, Ttl};
use super::types::StoredName;
use super::types::{StoredName, StoredRecordData};
use super::{SharedRr, SharedRrset};
//------------ Answer --------------------------------------------------------
@@ -202,6 +202,37 @@ pub enum AnswerContent {
NoData,
}
impl AnswerContent {
/// Gets the first record TTL and data, if any.
///
/// This can be used to get both the data as a specific variant, and the
/// associated TTL, in a single step:
///
/// ```should_panic
/// # use domain::base::iana::Rcode;
/// # use domain::rdata::ZoneRecordData;
/// # use domain::zonetree::Answer;
/// # let some_answer = Answer::new(Rcode::NOERROR);
/// let Some((soa_ttl, ZoneRecordData::Soa(soa))) =
/// some_answer.content().first()
/// else {
/// panic!("some_answer is not a variant of AnswerContent that has data");
/// };
/// ```
pub fn first(&self) -> Option<(Ttl, StoredRecordData)> {
match self {
AnswerContent::Data(shared_rrset) => shared_rrset
.data()
.first()
.map(|data| (shared_rrset.ttl(), data.clone())),
AnswerContent::Cname(shared_rr) => {
Some((shared_rr.ttl(), shared_rr.data().clone()))
}
AnswerContent::NoData => None,
}
}
}
//------------ AnswerAuthority -----------------------------------------------
/// The authority section of a query answer.
+9 -2
View File
@@ -1,6 +1,13 @@
//! An in-memory backing store for [`Zone`]s.
//! A versioned in-memory backing store for [`Zone`]s.
//!
//! [`Zone`]: super::Zone
//! # Limitations
//!
//! * There is currently no support for removing old versions of zone data
//! stored in the tree. The only optionis to [`walk()`] the [`Zone`] cloning
//! the current version into a new [`Zone`] then dropping the old [`Zone`].
//!
//! [`Zone`]: crate::zonetree::Zone
//! [`walk()`]: crate::zoneree::ReadableZone::walk()
mod builder;
mod nodes;
mod read;
+31 -40
View File
@@ -9,6 +9,7 @@ use crate::base::iana::{Rcode, Rtype};
use crate::base::name::Label;
use crate::base::Name;
use crate::zonetree::answer::{Answer, AnswerAuthority};
use crate::zonetree::error::OutOfZone;
use crate::zonetree::types::ZoneCut;
use crate::zonetree::walk::WalkState;
use crate::zonetree::{ReadableZone, Rrset, SharedRr, SharedRrset, WalkOp};
@@ -16,7 +17,6 @@ use crate::zonetree::{ReadableZone, Rrset, SharedRr, SharedRrset, WalkOp};
use super::nodes::{NodeChildren, NodeRrsets, Special, ZoneApex, ZoneNode};
use super::versioned::Version;
use super::versioned::VersionMarker;
use crate::zonetree::error::OutOfZone;
//------------ ReadZone ------------------------------------------------------
@@ -86,19 +86,23 @@ impl ReadZone {
) -> NodeAnswer {
node.with_special(self.version, |special| match special {
Some(Special::Cut(ref cut)) => {
let answer = NodeAnswer::authority(AnswerAuthority::new(
cut.name.clone(),
None,
Some(cut.ns.clone()),
cut.ds.as_ref().cloned(),
));
walk.op(&cut.ns);
if let Some(ds) = &cut.ds {
walk.op(ds);
if walk.enabled() {
walk.op(&cut.ns);
if let Some(ds) = &cut.ds {
walk.op(ds);
}
NodeAnswer::no_data()
} else {
// There is nothing more in this zone, only a cut here.
// Respond with NODATA and an authority section referring the
// client to the nameserver that should know more.
NodeAnswer::authority(AnswerAuthority::new(
cut.name.clone(),
None,
Some(cut.ns.clone()),
cut.ds.as_ref().cloned(),
))
}
answer
}
Some(Special::NxDomain) => NodeAnswer::nx_domain(),
Some(Special::Cname(cname)) => {
@@ -133,25 +137,8 @@ impl ReadZone {
walk: WalkState,
) -> NodeAnswer {
node.with_special(self.version, |special| match special {
Some(Special::Cut(cut)) => {
let answer = self.query_at_cut(cut, qtype);
if walk.enabled() {
walk.op(&cut.ns);
if let Some(ds) = &cut.ds {
walk.op(ds);
}
}
answer
}
Some(Special::Cname(cname)) => {
let answer = NodeAnswer::cname(cname.clone());
if walk.enabled() {
let mut rrset = Rrset::new(Rtype::CNAME, cname.ttl());
rrset.push_data(cname.data().clone());
walk.op(&rrset);
}
answer
}
Some(Special::Cut(cut)) => self.query_at_cut(cut, qtype),
Some(Special::Cname(cname)) => NodeAnswer::cname(cname.clone()),
Some(Special::NxDomain) => NodeAnswer::nx_domain(),
None => self.query_rrsets(node.rrsets(), qtype, walk),
})
@@ -197,9 +184,9 @@ impl ReadZone {
// response."
//
// We choose for option 1 because option 2 would create lots of
// extra work in the offline signing case (because lots of HFINO
// extra work in the offline signing case (because lots of HINFO
// records would need to be synthesized prior to signing) and
// option 3 as stated may still result in a large response.
// option 3, as stated, may still result in a large response.
let guard = rrsets.iter();
guard
.iter()
@@ -299,15 +286,19 @@ impl ReadableZone for ReadZone {
}
fn walk(&self, op: WalkOp) {
// https://datatracker.ietf.org/doc/html/rfc8482 notes that the ANY
// query type is problematic and should be answered as minimally as
// possible. Rather than use ANY internally here to achieve a walk, as
// specific behaviour may actually be wanted for ANY we instead use
// the presence of a callback `op` to indicate that walking mode is
// The presence of a callback `op` indicates that walking mode is
// requested. We still have to pass an Rtype but it won't be used for
// matching when in walk mode, so we set it to Any as it most closely
// matches our intent and will be ignored anyway.
let walk = WalkState::new(op);
//
// The walk is single threaded. With an empty callback function on a
// "13th Gen Intel(R) Core(TM) i9-13900K" over 43,347,447 resource
// records the walk took ~6 seconds, compared to 47 seconds for the
// callback function to emit the same records as DNS messages and for
// dig to receive the entire zone via AXFR:
//
// dig -4 @127.0.0.1 -p 8053 +noanswer +tries=1 +noidnout AXFR de.
let walk = WalkState::new(op, self.apex.name().clone());
self.query_rrsets(self.apex.rrsets(), Rtype::ANY, walk.clone());
self.query_below_apex(Label::root(), iter::empty(), Rtype::ANY, walk);
}
+16 -10
View File
@@ -22,7 +22,9 @@ use crate::base::iana::Rtype;
use crate::base::name::Label;
use crate::base::{NameBuilder, Serial};
use crate::rdata::ZoneRecordData;
use crate::zonetree::types::{ZoneCut, ZoneDiff, ZoneDiffBuilder};
use crate::zonetree::types::{
InMemoryZoneDiff, InMemoryZoneDiffBuilder, ZoneCut,
};
use crate::zonetree::StoredName;
use crate::zonetree::{Rrset, SharedRr};
use crate::zonetree::{SharedRrset, WritableZone, WritableZoneNode};
@@ -77,7 +79,7 @@ pub struct WriteZone {
/// [`WriteNode::update_child()`] is called it creates a new [`WriteNode`]
/// which also needs to be able to add and remove things from the same
/// diff collection.
diff: Arc<Mutex<Option<Arc<Mutex<ZoneDiffBuilder>>>>>,
diff: Arc<Mutex<Option<Arc<Mutex<InMemoryZoneDiffBuilder>>>>>,
/// The zone is dirty if changes have been made but not yet committed.
///
@@ -142,7 +144,7 @@ impl WriteZone {
fn add_soa_remove_diff_entry(
&mut self,
old_soa_rr: Option<SharedRr>,
diff: &mut ZoneDiffBuilder,
diff: &mut InMemoryZoneDiffBuilder,
) -> Option<Serial> {
if let Some(old_soa_rr) = old_soa_rr {
let ZoneRecordData::Soa(old_soa) = old_soa_rr.data() else {
@@ -172,7 +174,7 @@ impl WriteZone {
fn add_soa_add_diff_entry(
&mut self,
new_soa_rr: Option<SharedRr>,
diff: &mut ZoneDiffBuilder,
diff: &mut InMemoryZoneDiffBuilder,
) -> Option<Serial> {
if let Some(new_soa_rr) = new_soa_rr {
let ZoneRecordData::Soa(new_soa) = new_soa_rr.data() else {
@@ -287,7 +289,7 @@ impl WritableZone for WriteZone {
bump_soa_serial: bool,
) -> Pin<
Box<
dyn Future<Output = Result<Option<ZoneDiff>, io::Error>>
dyn Future<Output = Result<Option<InMemoryZoneDiff>, io::Error>>
+ Send
+ Sync,
>,
@@ -303,10 +305,14 @@ impl WritableZone for WriteZone {
// that case be a SOA record in the new version of the zone anyway.
let old_soa_rr = self.apex.get_soa(self.last_published_version());
let new_soa_rr = self.apex.get_soa(self.new_version);
let mut new_soa_rr = self.apex.get_soa(self.new_version);
if bump_soa_serial && old_soa_rr.is_some() && new_soa_rr.is_none() {
if bump_soa_serial
&& old_soa_rr.is_some()
&& (new_soa_rr.is_none() || new_soa_rr == old_soa_rr)
{
self.bump_soa_serial(&old_soa_rr);
new_soa_rr = self.apex.get_soa(self.new_version);
}
// Extract (and finish) the created diff, if any.
@@ -385,7 +391,7 @@ pub struct WriteNode {
node: Either<Arc<ZoneApex>, Arc<ZoneNode>>,
/// The diff we are building, if enabled.
diff: Option<(StoredName, Arc<Mutex<ZoneDiffBuilder>>)>,
diff: Option<(StoredName, Arc<Mutex<InMemoryZoneDiffBuilder>>)>,
}
impl WriteNode {
@@ -398,7 +404,7 @@ impl WriteNode {
let diff = if create_diff {
Some((
zone.apex.name().clone(),
Arc::new(Mutex::new(ZoneDiffBuilder::new())),
Arc::new(Mutex::new(InMemoryZoneDiffBuilder::new())),
))
} else {
None
@@ -677,7 +683,7 @@ impl WriteNode {
Ok(())
}
fn diff(&self) -> Option<Arc<Mutex<ZoneDiffBuilder>>> {
fn diff(&self) -> Option<Arc<Mutex<InMemoryZoneDiffBuilder>>> {
self.diff
.as_ref()
.map(|(_, diff_builder)| diff_builder.clone())
+4 -2
View File
@@ -115,11 +115,13 @@ mod zone;
pub use self::answer::{Answer, AnswerAuthority, AnswerContent};
pub use self::in_memory::ZoneBuilder;
pub use self::traits::{
ReadableZone, WritableZone, WritableZoneNode, ZoneStore,
ReadableZone, WritableZone, WritableZoneNode, ZoneDiff, ZoneDiffItem,
ZoneStore,
};
pub use self::tree::{ZoneSetIter, ZoneTree};
pub use self::types::{
Rrset, SharedRr, SharedRrset, StoredName, StoredRecord, ZoneDiff,
InMemoryZoneDiff, InMemoryZoneDiffBuilder, Rrset, SharedRr, SharedRrset,
StoredName, StoredRecord,
};
pub use self::walk::WalkOp;
pub use self::zone::Zone;
+1 -1
View File
@@ -3,7 +3,6 @@
use std::collections::{BTreeMap, HashMap};
use std::vec::Vec;
use super::error::{ContextError, RecordError, ZoneErrors};
use crate::base::iana::{Class, Rtype};
use crate::base::name::{FlattenInto, ToName};
use crate::base::Name;
@@ -12,6 +11,7 @@ use crate::zonefile::inplace::{self, Entry};
use crate::zonetree::ZoneBuilder;
use crate::zonetree::{Rrset, SharedRr};
use super::error::{ContextError, RecordError, ZoneErrors};
use super::types::{StoredName, StoredRecord};
//------------ Zonefile ------------------------------------------------------
+144 -4
View File
@@ -9,6 +9,7 @@
//! [`ZoneTree`]: super::ZoneTree
use core::any::Any;
use core::future::ready;
use core::ops::Deref;
use core::pin::Pin;
use std::boxed::Box;
@@ -18,14 +19,15 @@ use std::io;
use std::sync::Arc;
use bytes::Bytes;
use futures_util::Stream;
use crate::base::iana::Class;
use crate::base::name::Label;
use crate::base::{Name, Rtype};
use crate::base::{Name, Rtype, Serial, ToName};
use super::answer::Answer;
use super::error::OutOfZone;
use super::types::{ZoneCut, ZoneDiff};
use super::types::{InMemoryZoneDiff, ZoneCut};
use super::{SharedRr, SharedRrset, StoredName, WalkOp};
//------------ ZoneStore -----------------------------------------------------
@@ -128,6 +130,16 @@ pub trait ReadableZone: Send + Sync {
/// [`Zone`]: super::Zone
pub trait WritableZone: Send + Sync {
/// Start a write operation for the zone.
///
/// If `create_diff` is true the zone backing store is requested to create
/// an [`InMemoryZoneDiff`] which will accumulate entries as changes are
/// made to the zone and will be returned finally when [`commit()`] is
/// invoked.
///
/// Creating a diff is optional. If the backing store doesn't support
/// diff creation [`commit()`] will return `None`.
///
/// [`commit()`]: Self::commit
#[allow(clippy::type_complexity)]
fn open(
&self,
@@ -149,13 +161,18 @@ pub trait WritableZone: Send + Sync {
/// _after_ invoking this function will be able to see the changes made
/// since [`open()`] was called.
///
/// If `create_diff` was set to `true` when [`open()`] was invoked then
/// this function _may_ return `Some` if a diff was created. `None` may be
/// returned if the zone backing store does not support creation of diffs
/// or was unable to create a diff for some reason.
///
/// [`open()`]: Self::open
fn commit(
&mut self,
bump_soa_serial: bool,
) -> Pin<
Box<
dyn Future<Output = Result<Option<ZoneDiff>, io::Error>>
dyn Future<Output = Result<Option<InMemoryZoneDiff>, io::Error>>
+ Send
+ Sync,
>,
@@ -229,8 +246,131 @@ pub trait WritableZoneNode: Send + Sync {
cname: SharedRr,
) -> Pin<Box<dyn Future<Output = Result<(), io::Error>> + Send + Sync>>;
/// Recursively make all content at and below this point appear to be removed.
/// Recursively make all content at and below this point appear to be
/// removed.
fn remove_all(
&self,
) -> Pin<Box<dyn Future<Output = Result<(), io::Error>> + Send + Sync>>;
}
//------------ ZoneDiffItem ---------------------------------------------------
/// One difference item in a set of changes made to a zone.
///
/// Conceptually a diff is like a set of keys and values, representing a change
/// to a resource record set with key (owner name, resource type) and the value
/// being the changed resource records at that owner and with that type.
pub trait ZoneDiffItem {
/// The owner name and resource record type.
fn key(&self) -> &(StoredName, Rtype);
/// The changed records.
///
/// Each record has the same key (owner name and resource record type).
fn value(&self) -> &SharedRrset;
}
//------------ ZoneDiff -------------------------------------------------------
/// A set of differences between two versions (SOA serial numbers) of a zone.
///
/// Often referred to simply as a "diff".
///
/// The default implementation of this trait supplied by the domain crate is
/// the [`InMemoryZoneDiff`]. As the name implies it stores its data in
/// memory.
///
/// In order however to support less local backing stores for diff data, such
/// as on-disk storage or in a database possibly reached via a network,
/// asynchronous access to the diff is supported via use of [`Future`]s and
/// [`Stream`]s.
pub trait ZoneDiff {
/// A single item in the diff.
type Item<'a>: ZoneDiffItem + Send
where
Self: 'a;
/// The type of [`Stream`] used to access the diff records.
type Stream<'a>: Stream<Item = Self::Item<'a>> + Send
where
Self: 'a;
/// The serial number of the zone which was modified.
fn start_serial(
&self,
) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>>;
/// The serial number of the zone that resulted from the modifications.
fn end_serial(&self)
-> Pin<Box<dyn Future<Output = Serial> + Send + '_>>;
/// An stream of RRsets that were added to the zone.
// TODO: Does this need to be Box<Pin<dyn Future<Output = Stream>>>?
fn added(&self) -> Self::Stream<'_>;
/// An stream of RRsets that were removed from the zone.
// TODO: Does this need to be Box<Pin<dyn Future<Output = Stream>>>?
fn removed(&self) -> Self::Stream<'_>;
/// Get an RRset that was added to the zone, if present in the diff.
fn get_added(
&self,
name: impl ToName,
rtype: Rtype,
) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>>;
/// Get an RRset that was removed from the zone, if present in the diff.
fn get_removed(
&self,
name: impl ToName,
rtype: Rtype,
) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>>;
}
//--- impl ZoneDiff for Arc
impl<T: ZoneDiff> ZoneDiff for Arc<T> {
type Item<'a> = T::Item<'a>
where
Self: 'a;
type Stream<'a> = T::Stream<'a>
where
Self: 'a;
fn start_serial(
&self,
) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>> {
Arc::deref(self).start_serial()
}
fn end_serial(
&self,
) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>> {
Arc::deref(self).end_serial()
}
fn added(&self) -> Self::Stream<'_> {
Arc::deref(self).added()
}
fn removed(&self) -> Self::Stream<'_> {
Arc::deref(self).removed()
}
fn get_added(
&self,
name: impl ToName,
rtype: Rtype,
) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>> {
Arc::deref(self).get_added(name, rtype)
}
fn get_removed(
&self,
name: impl ToName,
rtype: Rtype,
) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>> {
Arc::deref(self).get_removed(name, rtype)
}
}
+158 -12
View File
@@ -1,19 +1,26 @@
//! Zone tree related types.
use std::collections::HashMap;
use core::future::{ready, Future};
use core::pin::Pin;
use core::task::{Context, Poll};
use std::boxed::Box;
use std::collections::{hash_map, HashMap};
use std::ops;
use std::sync::Arc;
use std::vec::Vec;
use bytes::Bytes;
use futures_util::stream;
use serde::{Deserialize, Serialize};
use tracing::trace;
use super::traits::{ZoneDiff, ZoneDiffItem};
use crate::base::name::Name;
use crate::base::rdata::RecordData;
use crate::base::record::Record;
use crate::base::Serial;
use crate::base::{iana::Rtype, Ttl};
use crate::base::{Serial, ToName};
use crate::rdata::ZoneRecordData;
//------------ Type Aliases --------------------------------------------------
@@ -252,13 +259,13 @@ pub struct ZoneCut {
pub glue: Vec<StoredRecord>,
}
//------------ ZoneDiffBuilder -----------------------------------------------
//------------ InMemoryZoneDiffBuilder ----------------------------------------
/// A [`ZoneDiff`] builder.
/// An [`InMemoryZoneDiff`] builder.
///
/// Removes are assumed to occur before adds.
#[derive(Debug, Default)]
pub struct ZoneDiffBuilder {
pub struct InMemoryZoneDiffBuilder {
/// The records added to the Zone.
added: HashMap<(StoredName, Rtype), SharedRrset>,
@@ -266,7 +273,7 @@ pub struct ZoneDiffBuilder {
removed: HashMap<(StoredName, Rtype), SharedRrset>,
}
impl ZoneDiffBuilder {
impl InMemoryZoneDiffBuilder {
/// Creates a new instance of the builder.
pub fn new() -> Self {
Default::default()
@@ -301,22 +308,22 @@ impl ZoneDiffBuilder {
/// Note: No check is currently done that the start and end serials match
/// the SOA records in the removed and added records contained within the
/// diff.
pub fn build(self) -> Result<ZoneDiff, ZoneDiffError> {
ZoneDiff::new(self.added, self.removed)
pub fn build(self) -> Result<InMemoryZoneDiff, ZoneDiffError> {
InMemoryZoneDiff::new(self.added, self.removed)
}
}
//------------ ZoneDiff ------------------------------------------------------
//------------ InMemoryZoneDiff -----------------------------------------------
/// The differences between one serial and another for a DNS zone.
///
/// Removes are assumed to occur before adds.
#[derive(Clone, Debug)]
pub struct ZoneDiff {
pub struct InMemoryZoneDiff {
/// The serial number of the zone which was modified.
pub start_serial: Serial,
/// The serial number of the Zzone that resulted from the modifications.
/// The serial number of the zone that resulted from the modifications.
pub end_serial: Serial,
/// The RRsets added to the zone.
@@ -326,7 +333,7 @@ pub struct ZoneDiff {
pub removed: Arc<HashMap<(StoredName, Rtype), SharedRrset>>,
}
impl ZoneDiff {
impl InMemoryZoneDiff {
/// Creates a new immutable zone diff.
///
/// Returns `Err(ZoneDiffError::MissingStartSoa)` If the removed records
@@ -388,6 +395,145 @@ impl ZoneDiff {
}
}
//--- impl ZoneDiff
impl<'a> ZoneDiffItem for (&'a (StoredName, Rtype), &'a SharedRrset) {
fn key(&self) -> &(StoredName, Rtype) {
self.0
}
fn value(&self) -> &SharedRrset {
self.1
}
}
impl ZoneDiff for InMemoryZoneDiff {
type Item<'a> = (&'a (StoredName, Rtype), &'a SharedRrset)
where
Self: 'a;
type Stream<'a> = futures_util::stream::Iter<hash_map::Iter<'a, (StoredName, Rtype), SharedRrset>>
where
Self: 'a;
fn start_serial(
&self,
) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>> {
Box::pin(ready(self.start_serial))
}
fn end_serial(
&self,
) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>> {
Box::pin(ready(self.end_serial))
}
fn added(&self) -> Self::Stream<'_> {
stream::iter(self.added.iter())
}
fn removed(&self) -> Self::Stream<'_> {
stream::iter(self.removed.iter())
}
fn get_added(
&self,
name: impl ToName,
rtype: Rtype,
) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>> {
Box::pin(ready(self.added.get(&(name.to_name(), rtype))))
}
fn get_removed(
&self,
name: impl ToName,
rtype: Rtype,
) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>> {
Box::pin(ready(self.removed.get(&(name.to_name(), rtype))))
}
}
/// The item type used by [`EmptyZoneDiff`].
pub struct EmptyZoneDiffItem;
impl ZoneDiffItem for EmptyZoneDiffItem {
fn key(&self) -> &(StoredName, Rtype) {
unreachable!()
}
fn value(&self) -> &SharedRrset {
unreachable!()
}
}
/// The stream type used by [`EmptyZoneDiff`].
#[derive(Debug)]
pub struct EmptyZoneDiffStream;
impl futures_util::stream::Stream for EmptyZoneDiffStream {
type Item = EmptyZoneDiffItem;
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
Poll::Ready(None)
}
}
/// A [`ZoneDiff`] implementation that is always empty.
///
/// Useful when a [`ZoneDiff`] type is needed in a type declaration but for use
/// by a type that does not support zone difference data.
#[derive(Debug)]
pub struct EmptyZoneDiff;
impl ZoneDiff for EmptyZoneDiff {
type Item<'a> = EmptyZoneDiffItem
where
Self: 'a;
type Stream<'a> = EmptyZoneDiffStream
where
Self: 'a;
fn start_serial(
&self,
) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>> {
Box::pin(ready(Serial(0)))
}
fn end_serial(
&self,
) -> Pin<Box<dyn Future<Output = Serial> + Send + '_>> {
Box::pin(ready(Serial(0)))
}
fn added(&self) -> Self::Stream<'_> {
EmptyZoneDiffStream
}
fn removed(&self) -> Self::Stream<'_> {
EmptyZoneDiffStream
}
fn get_added(
&self,
_name: impl ToName,
_rtype: Rtype,
) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>> {
Box::pin(ready(None))
}
fn get_removed(
&self,
_name: impl ToName,
_rtype: Rtype,
) -> Pin<Box<dyn Future<Output = Option<&SharedRrset>> + Send + '_>> {
Box::pin(ready(None))
}
}
//------------ ZoneDiffError --------------------------------------------------
/// Creating a [`ZoneDiff`] failed for some reason.
+5 -3
View File
@@ -21,7 +21,7 @@ use crate::zonetree::{Rrset, SharedRrset};
use super::error::OutOfZone;
use super::types::ZoneUpdate;
use super::util::rel_name_rev_iter;
use super::{WritableZone, WritableZoneNode, Zone, ZoneDiff};
use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone};
/// Apply a sequence of [`ZoneUpdate`]s to update the content of a [`Zone`].
///
@@ -267,7 +267,7 @@ impl ZoneUpdater {
pub async fn apply(
&mut self,
update: ZoneUpdate<ParsedRecord>,
) -> Result<Option<ZoneDiff>, Error> {
) -> Result<Option<InMemoryZoneDiff>, Error> {
trace!("Update: {update}");
if self.state == ZoneUpdaterState::Finished {
@@ -337,6 +337,8 @@ impl ZoneUpdater {
/// Has zone updating finished?
///
/// If true, further calls to [`apply()`] will fail.
///
/// [`apply()`]: Self::apply
pub fn is_finished(&self) -> bool {
self.state == ZoneUpdaterState::Finished
}
@@ -521,7 +523,7 @@ impl ReopenableZoneWriter {
/// Commits any pending changes to the [`Zone`] being written to.
///
/// Returns the created diff, if any.
async fn commit(&mut self) -> Result<Option<ZoneDiff>, Error> {
async fn commit(&mut self) -> Result<Option<InMemoryZoneDiff>, Error> {
// Commit the deletes and adds that just occurred
if let Some(writable) = self.writable.take() {
// Ensure that there are no dangling references to the created
+10 -9
View File
@@ -2,28 +2,29 @@ use std::boxed::Box;
use std::sync::{Arc, Mutex};
use std::vec::Vec;
use bytes::Bytes;
use super::{SharedRrset, StoredName};
use super::Rrset;
use crate::base::name::OwnedLabel;
use crate::base::{Name, NameBuilder};
use crate::base::NameBuilder;
/// A callback function invoked for each leaf node visited while walking a
/// [`Zone`].
///
/// [`Zone`]: super::Zone
pub type WalkOp = Box<dyn Fn(Name<Bytes>, &Rrset) + Send + Sync>;
pub type WalkOp = Box<dyn Fn(StoredName, &SharedRrset) + Send + Sync>;
struct WalkStateInner {
op: WalkOp,
label_stack: Mutex<Vec<OwnedLabel>>,
apex_name: StoredName,
}
impl WalkStateInner {
fn new(op: WalkOp) -> Self {
fn new(op: WalkOp, apex_name: StoredName) -> Self {
Self {
op,
label_stack: Default::default(),
apex_name,
}
}
}
@@ -36,9 +37,9 @@ pub(super) struct WalkState {
impl WalkState {
pub(super) const DISABLED: WalkState = WalkState { inner: None };
pub(super) fn new(op: WalkOp) -> Self {
pub(super) fn new(op: WalkOp, apex_name: StoredName) -> Self {
Self {
inner: Some(Arc::new(WalkStateInner::new(op))),
inner: Some(Arc::new(WalkStateInner::new(op, apex_name))),
}
}
@@ -46,14 +47,14 @@ impl WalkState {
self.inner.is_some()
}
pub(super) fn op(&self, rrset: &Rrset) {
pub(super) fn op(&self, rrset: &SharedRrset) {
if let Some(inner) = &self.inner {
let labels = inner.label_stack.lock().unwrap();
let mut dname = NameBuilder::new_bytes();
for label in labels.iter().rev() {
dname.append_label(label.as_slice()).unwrap();
}
let owner = dname.into_name().unwrap();
let owner = dname.append_origin(&inner.apex_name).unwrap();
(inner.op)(owner, rrset);
}
}
File diff suppressed because it is too large Load Diff
+140
View File
@@ -0,0 +1,140 @@
; Based on https://github.com/NLnetLabs/unbound/blob/172b84f7ce6507e96fe51bd94448222a5a47274b/testdata/auth_xfr.rpl
;------------ Server configuration --------------------------------------------
server:
provide-xfr: 127.0.0.1 NOKEY
provide-xfr: 127.0.0.2 NOKEY COMPATIBLE
; Define an in-memory zone to be served by the server.
local-data: "com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1720688795 1800 900 604800 86400"
local-data: "com. 166972 IN NS a.gtld-servers.net."
local-data: "com. 166972 IN NS b.gtld-servers.net."
local-data: "com. 166972 IN NS c.gtld-servers.net."
local-data: "com. 166972 IN NS d.gtld-servers.net."
local-data: "com. 166972 IN NS e.gtld-servers.net."
local-data: "com. 166972 IN NS f.gtld-servers.net."
local-data: "com. 166972 IN NS g.gtld-servers.net."
local-data: "com. 166972 IN NS h.gtld-servers.net."
local-data: "com. 166972 IN NS i.gtld-servers.net."
local-data: "com. 166972 IN NS j.gtld-servers.net."
local-data: "com. 166972 IN NS k.gtld-servers.net."
local-data: "com. 166972 IN NS l.gtld-servers.net."
local-data: "com. 166972 IN NS m.gtld-servers.net."
local-data: "com. 86400 IN DNSKEY 257 3 13 tx8EZRAd2+K/DJRV0S+hbBzaRPS/G6JVNBitHzqpsGlz8huE61Ms9ANe 6NSDLKJtiTBqfTJWDAywEp1FCsEINQ=="
local-data: "com. 86400 IN DNSKEY 256 3 13 Nps5nxuQHRbY3e9hcbH36kxiELJH5wil+6dC4K1keQI9ci1nqyCP4k1X oXBBn2aeSK4KxwPEs0Opqc0dicuujg=="
local-data: "com. 86400 IN DNSKEY 256 3 13 cCRwZIITlPXwDm0yKpGVYSmWLL4OpEHxA7+Rt3jS0W1N4EMOaF8doSzr JuM7aDbgAR7jtQ9SNCvYZCH2xSyfaQ=="
local-data: "alt.com. 3600 IN CNAME example.com."
local-data: "a.alt.com. 3600 IN A 1.2.3.4"
local-data: "example.com. 172800 IN NS a.iana-servers.net."
local-data: "example.com. 172800 IN NS b.iana-servers.net."
local-data: "example.com. 86400 IN DS 370 13 2 BE74359954660069D5C63D200C39F5603827D7DD02B56F120EE9F3A8 6764247C"
local-data: "www.terminal.com. 3600 IN A 1.2.3.4"
local-data: "alt.terminal.com. 3600 IN CNAME www.example.com."
CONFIG_END
;------------ Test definition ------------------------------------------------
SCENARIO_BEGIN Test AXFR out.
; Note: It is not currently possible to construct a UDP AXFR query so we cannot
; test that the server refuses the request. This instead results in FORMERR
; during request construction rather than REFUSED from the server.
;STEP 10 QUERY
;ENTRY_BEGIN
;MATCH UDP
;SECTION QUESTION
; com. IN AXFR
;ENTRY_END
;
;STEP 11 CHECK_ANSWER
;ENTRY_BEGIN
;MATCH all
;REPLY QR AA REFUSED
;ENTRY_END
; Retrieve the zone via AXFR from the server
STEP 20 QUERY
ENTRY_BEGIN
MATCH TCP
SECTION QUESTION
com. IN AXFR
ENTRY_END
STEP 21 CHECK_ANSWER
ENTRY_BEGIN
MATCH all
REPLY QR AA NOERROR
SECTION QUESTION
com. IN AXFR
SECTION ANSWER
com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1720688795 1800 900 604800 86400
com. 166972 IN NS a.gtld-servers.net.
com. 166972 IN NS b.gtld-servers.net.
com. 166972 IN NS c.gtld-servers.net.
com. 166972 IN NS d.gtld-servers.net.
com. 166972 IN NS e.gtld-servers.net.
com. 166972 IN NS f.gtld-servers.net.
com. 166972 IN NS g.gtld-servers.net.
com. 166972 IN NS h.gtld-servers.net.
com. 166972 IN NS i.gtld-servers.net.
com. 166972 IN NS j.gtld-servers.net.
com. 166972 IN NS k.gtld-servers.net.
com. 166972 IN NS l.gtld-servers.net.
com. 166972 IN NS m.gtld-servers.net.
com. 86400 IN DNSKEY 257 3 13 tx8EZRAd2+K/DJRV0S+hbBzaRPS/G6JVNBitHzqpsGlz8huE61Ms9ANe 6NSDLKJtiTBqfTJWDAywEp1FCsEINQ==
com. 86400 IN DNSKEY 256 3 13 Nps5nxuQHRbY3e9hcbH36kxiELJH5wil+6dC4K1keQI9ci1nqyCP4k1X oXBBn2aeSK4KxwPEs0Opqc0dicuujg==
com. 86400 IN DNSKEY 256 3 13 cCRwZIITlPXwDm0yKpGVYSmWLL4OpEHxA7+Rt3jS0W1N4EMOaF8doSzr JuM7aDbgAR7jtQ9SNCvYZCH2xSyfaQ==
alt.com. 3600 IN CNAME example.com.
a.alt.com. 3600 IN A 1.2.3.4
example.com. 172800 IN NS a.iana-servers.net.
example.com. 172800 IN NS b.iana-servers.net.
example.com. 86400 IN DS 370 13 2 BE74359954660069D5C63D200C39F5603827D7DD02B56F120EE9F3A8 6764247C
www.terminal.com. 3600 IN A 1.2.3.4
alt.terminal.com. 3600 IN CNAME www.example.com.
com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1720688795 1800 900 604800 86400
ENTRY_END
; Retrieve the zone via backward compatible AXFR from the server
STEP 30 QUERY ADDRESS 127.0.0.2
ENTRY_BEGIN
MATCH TCP
SECTION QUESTION
com. IN AXFR
ENTRY_END
STEP 31 CHECK_ANSWER
ENTRY_BEGIN
MATCH all EXTRA_PACKETS
REPLY QR AA NOERROR
SECTION QUESTION
com. IN AXFR
SECTION ANSWER
com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1720688795 1800 900 604800 86400
com. 166972 IN NS a.gtld-servers.net.
com. 166972 IN NS b.gtld-servers.net.
com. 166972 IN NS c.gtld-servers.net.
com. 166972 IN NS d.gtld-servers.net.
com. 166972 IN NS e.gtld-servers.net.
com. 166972 IN NS f.gtld-servers.net.
com. 166972 IN NS g.gtld-servers.net.
com. 166972 IN NS h.gtld-servers.net.
com. 166972 IN NS i.gtld-servers.net.
com. 166972 IN NS j.gtld-servers.net.
com. 166972 IN NS k.gtld-servers.net.
com. 166972 IN NS l.gtld-servers.net.
com. 166972 IN NS m.gtld-servers.net.
com. 86400 IN DNSKEY 256 3 13 Nps5nxuQHRbY3e9hcbH36kxiELJH5wil+6dC4K1keQI9ci1nqyCP4k1X oXBBn2aeSK4KxwPEs0Opqc0dicuujg==
com. 86400 IN DNSKEY 256 3 13 cCRwZIITlPXwDm0yKpGVYSmWLL4OpEHxA7+Rt3jS0W1N4EMOaF8doSzr JuM7aDbgAR7jtQ9SNCvYZCH2xSyfaQ==
com. 86400 IN DNSKEY 257 3 13 tx8EZRAd2+K/DJRV0S+hbBzaRPS/G6JVNBitHzqpsGlz8huE61Ms9ANe 6NSDLKJtiTBqfTJWDAywEp1FCsEINQ==
alt.terminal.com. 3600 IN CNAME www.example.com.
a.alt.com. 3600 IN A 1.2.3.4
example.com. 172800 IN NS a.iana-servers.net.
example.com. 172800 IN NS b.iana-servers.net.
example.com. 86400 IN DS 370 13 2 BE74359954660069D5C63D200C39F5603827D7DD02B56F120EE9F3A8 6764247C
www.terminal.com. 3600 IN A 1.2.3.4
alt.com. 3600 IN CNAME example.com.
com. 900 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1720688795 1800 900 604800 86400
ENTRY_END
SCENARIO_END
+3 -1
View File
@@ -6,8 +6,10 @@ server:
cookie-secret: "000102030405060708090a0b0c0d0e0f"
access-control: 127.0.0.1 allow_cookie
access-control: 1.2.3.4 allow
local-data: "test. TXT test"
; Define an in-memory zone to be served by the server.
local-data: "test. 3600 IN SOA ns.test. hostmaster.test. 1 3600 900 86400 3600"
local-data: "test. TXT test"
CONFIG_END
SCENARIO_BEGIN Test downstream DNS Cookies
+6 -3
View File
@@ -1,8 +1,11 @@
; Based on: https://github.com/NLnetLabs/unbound/blob/49e425810275917e7fd09a24bae3b97d83b55c13/testdata/edns_keepalive.rpl
server:
edns-tcp-keepalive: yes
edns-tcp-keepalive-timeout: 30000
local-data: "test. TXT test"
edns-tcp-keepalive: yes
edns-tcp-keepalive-timeout: 30000
; Define an in-memory zone to be served by the server.
local-data: "test. 3600 IN SOA ns.test. hostmaster.test. 1 3600 900 86400 3600"
local-data: "test. TXT test"
CONFIG_END
SCENARIO_BEGIN TCP Keepalive
+7 -9
View File
@@ -1,7 +1,8 @@
$ORIGIN example.com. ; 'default' domain as FQDN for this zone
; From: https://nsd.docs.nlnetlabs.nl/en/latest/zonefile.html#creating-a-zone
$ORIGIN example.com.
$TTL 86400 ; default time-to-live for this zone
example.com. IN SOA ns.example.com. noc.dns.icann.org. (
example.com. IN SOA ns.example.com. noc.dns.example.org. (
2020080302 ;Serial
7200 ;Refresh
3600 ;Retry
@@ -9,17 +10,14 @@ example.com. IN SOA ns.example.com. noc.dns.icann.org. (
3600 ;Negative response caching TTL
)
; The nameserver that are authoritative for this zone.
; The nameservers that are authoritative for this zone.
NS example.com.
; these A records below are equivalent
; A and AAAA records are for IPv4 and IPv6 addresses respectively
example.com. A 192.0.2.1
@ A 192.0.2.1
A 192.0.2.1
AAAA 2001:db8::3
@ AAAA 2001:db8::3
; A CNAME redirect from www.exmaple.com to example.com
; A CNAME redirects from www.example.com to example.com
www CNAME example.com.
mail MX 10 example.com.