mirror of
https://github.com/NLnetLabs/domain.git
synced 2026-09-27 04:04:53 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,3 +47,5 @@ pub mod notify;
|
||||
pub mod stream;
|
||||
#[cfg(feature = "tsig")]
|
||||
pub mod tsig;
|
||||
#[cfg(feature = "unstable-xfr")]
|
||||
pub mod xfr;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
>;
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user