Skip to content

Commit

Permalink
Merge pull request #28 from thibault-cne/27-fix-tsig-scope-validation…
Browse files Browse the repository at this point in the history
…-and-dynamic-dns

Fix tsig scope validation and dynamic dns
  • Loading branch information
thibault-cne authored Aug 2, 2024
2 parents d422a71 + bb3d12a commit 7260446
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 268 deletions.
16 changes: 0 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ edition = "2021"
[dependencies]
base64 = "0.22.1"
bytes = "1.6.1"
convert_case = "0.6.0"
domain = { features = [
"zonefile",
"net",
Expand Down
2 changes: 1 addition & 1 deletion config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ log:
# The log level. This can be one of the following: trace, debug, info, warn, error, or off.
level: debug
# Enable the metrics.
enable_metrics: true
enable_metrics: false
# Enable thread ID in logs.
enable_thread_id: true
# Log on stderr.
Expand Down
4 changes: 4 additions & 0 deletions key
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
key "key1" {
algorithm hmac-sha512;
secret "F2BR0YaL2hiOpEDlUdv6U9mi4dI/xPZvuWQRbvC3G9MT9xLOoGzjk5lcXvmv4sekas8cq1kHutvwvlDOSnG3Aw==";
};
26 changes: 24 additions & 2 deletions src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, RwLock};

use bytes::BytesMut;
use bytes::{Bytes, BytesMut};
use domain::base::iana::Class;
use domain::base::{Record, Serial, ToName, Ttl};
use domain::base::{Name, Record, Serial, ToName, Ttl};
use domain::rdata::Soa;
use domain::tsig::{Algorithm, Key, KeyName};
use domain::zonetree::types::{StoredName, StoredRecord};
Expand Down Expand Up @@ -52,6 +52,16 @@ pub struct DomainInfo {
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
pub struct DomainName(String);

impl DomainName {
pub fn strip_prefix(self) -> Self {
if let Some(dname) = self.0.strip_prefix("_acme-challenge.") {
Self(dname.to_string())
} else {
self
}
}
}

pub trait TryInto<T> {
fn try_into_t(self) -> Result<T>;
}
Expand Down Expand Up @@ -113,6 +123,12 @@ impl TryInto<StoredName> for &DomainName {
}
}

impl From<&Name<Bytes>> for DomainName {
fn from(value: &Name<Bytes>) -> Self {
DomainName(value.to_string())
}
}

impl<B> TryInto<StoredName> for B
where
B: AsRef<[u8]>,
Expand Down Expand Up @@ -152,6 +168,12 @@ impl TryFrom<&KeyFile> for KeyName {
}
}

impl From<&KeyName> for KeyFile {
fn from(kn: &KeyName) -> Self {
Self(kn.to_string())
}
}

impl TryFrom<&KeyFile> for (KeyName, Algorithm) {
type Error = crate::error::Error;

Expand Down
203 changes: 0 additions & 203 deletions src/service/handler.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
use std::sync::{Arc, Mutex};

use domain::base::iana::{Class, Opcode, Rcode};
use domain::base::message_builder::AdditionalBuilder;
use domain::base::{Message, Name, Rtype, ToName};
use domain::dep::octseq::OctetsBuilder;
use domain::net::server::message::Request;
use domain::net::server::service::{CallResult, ServiceError};
use domain::net::server::util::mk_builder_for_target;
use domain::zonetree::{Answer, Rrset};
use futures::channel::mpsc::UnboundedSender;

pub type HandlerResult<T> = Result<T, ServiceError>;
Expand All @@ -20,198 +12,3 @@ pub trait HandleDNS {
sender: UnboundedSender<HandlerResult<CallResult<Vec<u8>>>>,
) -> HandlerResult<()>;
}

impl HandleDNS for super::Dnsr {
fn handle_non_axfr(&self, request: Request<Vec<u8>>) -> HandlerResult<CallResult<Vec<u8>>> {
let answer = {
let question = request.message().sole_question().unwrap();
self.zones
.find_zone_f(question.qname(), question.qclass(), |zone| match zone {
Some(zone) => {
let qname = question.qname().to_bytes();
let qtype = question.qtype();
zone.query(qname, qtype).unwrap()
}
None => Answer::new(Rcode::NXDOMAIN),
})
};

let builder = mk_builder_for_target();
let additional = answer.to_message(request.message(), builder);

Ok(CallResult::new(additional))
}

fn handle_axfr(
&self,
request: Request<Vec<u8>>,
sender: UnboundedSender<HandlerResult<CallResult<Vec<u8>>>>,
) -> HandlerResult<()> {
let mut message = request.message().clone();
let message = Arc::make_mut(&mut message);

let request = Request::new(
request.client_addr(),
request.received_at(),
message.to_owned(),
request.transport_ctx().to_owned(),
);

// Look up the zone for the queried name.
let question = request.message().sole_question().unwrap();

if question.qclass() == Class::IN {
let answer = Answer::new(Rcode::NXDOMAIN);
add_to_stream(answer, request.message(), &sender);
return Ok(());
}

let zone = self.zones.find_zone(question.qname());

// If not found, return an NXDOMAIN error response.
let Some(zone) = zone else {
let answer = Answer::new(Rcode::NXDOMAIN);
add_to_stream(answer, request.message(), &sender);
return Ok(());
};

// https://datatracker.ietf.org/doc/html/rfc5936#section-2.2
// 2.2: AXFR Response
//
// "An AXFR response that is transferring the zone's contents
// will consist of a series (which could be a series of
// length 1) of DNS messages. In such a series, the first
// message MUST begin with the SOA resource record of the
// zone, and the last message MUST conclude with the same SOA
// resource record. Intermediate messages MUST NOT contain
// the SOA resource record. The AXFR server MUST copy the
// Question section from the corresponding AXFR query message
// into the first response message's Question section. For
// subsequent messages, it MAY do the same or leave the
// Question section empty."

// Get the SOA record as AXFR transfers must start and end with the SOA
// record. If not found, return a SERVFAIL error response.
let qname = question.qname().to_bytes();
let zone = zone.read();
let Ok(soa_answer) = zone.query(qname, Rtype::SOA) else {
let answer = Answer::new(Rcode::SERVFAIL);
add_to_stream(answer, request.message(), &sender);
return Ok(());
};

// Push the begin SOA response message into the stream
add_to_stream(soa_answer.clone(), request.message(), &sender);

// "The AXFR protocol treats the zone contents as an unordered
// collection (or to use the mathematical term, a "set") of
// RRs. Except for the requirement that the transfer must
// begin and end with the SOA RR, there is no requirement to
// send the RRs in any particular order or grouped into
// response messages in any particular way. Although servers
// typically do attempt to send related RRs (such as the RRs
// forming an RRset, and the RRsets of a name) as a
// contiguous group or, when message space allows, in the
// same response message, they are not required to do so, and
// clients MUST accept any ordering and grouping of the
// non-SOA RRs. Each RR SHOULD be transmitted only once, and
// AXFR clients MUST ignore any duplicate RRs received.
//
// Each AXFR response message SHOULD contain a sufficient
// number of RRs to reasonably amortize the per-message
// overhead, up to the largest number that will fit within a
// DNS message (taking the required content of the other
// sections into account, as described below).
//
// Some old AXFR clients expect each response message to
// contain only a single RR. To interoperate with such
// clients, the server MAY restrict response messages to a
// single RR. As there is no standard way to automatically
// detect such clients, this typically requires manual
// configuration at the server."

let sender = Arc::new(Mutex::new(sender));
let cloned_sender = sender.clone();
let cloned_msg = request.message().clone();

let op = Box::new(move |owner: Name<_>, rrset: &Rrset| {
if rrset.rtype() != Rtype::SOA {
let builder = mk_builder_for_target();
let mut answer = builder.start_answer(&cloned_msg, Rcode::NOERROR).unwrap();
for item in rrset.data() {
answer.push((owner.clone(), rrset.ttl(), item)).unwrap();
}

let additional = answer.additional();
let sender = cloned_sender.lock().unwrap();
add_additional_to_stream(additional, &cloned_msg, &sender);
}
});
zone.walk(op);

let mutex = Arc::try_unwrap(sender).unwrap();
let sender = mutex.into_inner().unwrap();

// Push the end SOA response message into the stream
add_to_stream(soa_answer, request.message(), &sender);

Ok(())
}
}

fn add_to_stream(
answer: Answer,
msg: &Message<Vec<u8>>,
sender: &UnboundedSender<HandlerResult<CallResult<Vec<u8>>>>,
) {
let builder = mk_builder_for_target();
let additional = answer.to_message(msg, builder);
add_additional_to_stream(additional, msg, sender);
}

fn add_additional_to_stream(
mut additional: AdditionalBuilder<domain::base::StreamTarget<Vec<u8>>>,
msg: &Message<Vec<u8>>,
sender: &UnboundedSender<HandlerResult<CallResult<Vec<u8>>>>,
) {
set_axfr_header(msg, &mut additional);
let item = Ok(CallResult::new(additional));
sender.unbounded_send(item).unwrap();
}

fn set_axfr_header<Target>(msg: &Message<Vec<u8>>, additional: &mut AdditionalBuilder<Target>)
where
Target: AsMut<[u8]>,
Target: OctetsBuilder,
{
// 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();
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);
}
Loading

0 comments on commit 7260446

Please sign in to comment.