Files
NLnetLabs-dnst/src/commands/update.rs
T
2025-09-08 09:38:18 +02:00

1890 lines
69 KiB
Rust

// Relevant RFCs:
//
// - [RFC2136]: Dynamic Updates in the Domain Name System (DNS UPDATE)
// - [RFC3007]: Secure Domain Name System (DNS) Dynamic Update
//
// [RFC2136]: https://www.rfc-editor.org/rfc/rfc2136.html
// [RFC3007]: https://www.rfc-editor.org/rfc/rfc3007.html
//
// Important notes:
//
// - No duplicate protection through update ordering with marker RRs
use std::cmp::Ordering;
use std::ffi::OsString;
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
use std::time::Duration;
use clap::Subcommand;
use domain::base::iana::{Class, Opcode, Rcode};
use domain::base::message_builder::{AnswerBuilder, AuthorityBuilder};
use domain::base::name::{FlattenInto, UncertainName};
use domain::base::opt::AllOptData;
use domain::base::{
Message, MessageBuilder, Name, Question, Record, Rtype, ToName, Ttl, UnknownRecordData,
};
use domain::net::client::request::{RequestMessage, SendRequest};
use domain::net::client::{dgram, tsig};
use domain::rdata::{Aaaa, AllRecordData, Ns, Soa, ZoneRecordData, A};
use domain::resolv::stub::conf::{ResolvConf, ServerConf, Transport};
use domain::tsig::Key;
use domain::utils::base64;
use domain::zonefile::inplace::{Entry, Zonefile};
use tracing::{debug, info, trace};
use crate::env::Env;
use crate::error::{Context, Error};
use crate::parse::TSigInfo;
use crate::Args;
use super::{parse_os, parse_os_with, Command, LdnsCommand};
type ParsedRecord = Record<Name<Vec<u8>>, ZoneRecordData<Vec<u8>, Name<Vec<u8>>>>;
type NameTypeTuple = (Name<Vec<u8>>, Rtype);
const SERIAL_BITS: u32 = 32;
const SOA_SERIAL_CUTOFF: u32 = 1u32 << (SERIAL_BITS - 1);
//------------ Update --------------------------------------------------------
#[derive(Clone, Debug, clap::Args, PartialEq, Eq)]
pub struct Update {
/// Domain name to update
#[arg(value_name = "DOMAIN NAME")]
domain: Name<Vec<u8>>,
/// Update action
#[command(subcommand)]
action: UpdateAction,
/// Class
#[arg(short = 'c', long = "class", default_value_t = Class::IN)]
class: Class,
/// TTL in seconds or with unit suffix (s, m, h, d).
///
/// Is only used by the `add` command and ignored otherwise.
#[arg(short = 't', long = "ttl", value_parser = Update::parse_ttl, default_value = "3600")]
ttl: Ttl,
/// Name server to send the update to
#[arg(short = 's', long = "server", value_name = "IP")]
nameservers: Option<IpAddr>,
/// Zone the domain name belongs to (to skip SOA query)
#[arg(short = 'z', long = "zone", value_name = "ZONE")]
zone: Option<Name<Vec<u8>>>,
/// TSIG credentials for the UPDATE packet
#[arg(short = 'y', long = "tsig", value_name = "NAME:KEY[:ALGO]")]
tsig: Option<TSigInfo>,
/// Require that at least one RR with the given NAME and TYPE exists.
/// This option can be provided multiple times, with format "<DOMAIN_NAME>
/// <TYPE>" each, to build up a list of RRs.
///
/// If the domain name is relative, it will be relative to the zone's apex.
#[arg(
long = "rrset-exists",
visible_alias = "rrset",
value_name = "DOMAIN_NAME_AND_TYPE"
)]
rrset_exists: Option<Vec<String>>,
/// Require that an RRset exists and contains exactly the RRs with the given
/// NAME, TYPE, and RDATA. This option can be provided multiple times, each
/// with one RR in zonefile format, to build up one or more RRsets that is
/// required to exist. CLASS and TTL can be omitted.
///
/// If the domain name is relative, it will be relative to the zone's apex.
#[arg(
long = "rrset-exists-exact",
visible_alias = "rrset-exact",
value_name = "RESOURCE_RECORD"
)]
rrset_exists_exact: Option<Vec<String>>,
/// RRset does not exist. This option can be provided multiple times, with
/// format "<DOMAIN_NAME> <TYPE>" each, to build up a list of RRs that
/// specify that no RRs with a specified NAME and TYPE can exist.
///
/// If the domain name is relative, it will be relative to the zone's apex.
#[arg(
long = "rrset-non-existent",
visible_alias = "rrset-empty",
value_name = "DOMAIN_NAME_AND_TYPE"
)]
rrset_non_existent: Option<Vec<String>>,
/// Name is in use. This option can be provided multiple times, with format
/// "<DOMAIN_NAME>" each, to collect a list of NAMEs that must own at least
/// one RR.
///
/// Note that this prerequisite is NOT satisfied by empty nonterminals.
///
/// If the domain name is relative, it will be relative to the zone's apex.
#[arg(
long = "name-in-use",
visible_alias = "name-used",
value_name = "DOMAIN_NAME"
)]
name_in_use: Option<Vec<String>>,
/// Name is not in use. This option can be provided multiple times, with
/// format "<DOMAIN_NAME>" each, to collect a list of NAMEs that must NOT
/// own any RRs.
///
/// Note that this prerequisite IS satisfied by empty nonterminals.
///
/// If the domain name is relative, it will be relative to the zone's apex.
#[arg(
long = "name-not-in-use",
visible_alias = "name-unused",
value_name = "DOMAIN_NAME"
)]
name_not_in_use: Option<Vec<String>>,
}
impl Update {
pub fn execute(self, env: impl Env) -> Result<(), Error> {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(self.run(&env))
}
/// Run the command as an async function
pub async fn run(self, env: &impl Env) -> Result<(), Error> {
// 1. Know apex and name servers for zone
// 2. If update ordering desired, fetch existing SOA RR from primary
// - If updating SOA, must update in serial in positive direction and preserve other
// fields, unless intent to change them; serial must never be 0
// (not yet implemented)
// 3. Order nameserver list, listing primary first
// 4. Create UPDATE and send to first server in list
// 5. If response != SERVFAIL | NOTIMP, then return success
// 6. If response == SERVFAIL | NOTIMP, OR no response in software
// dependent timeout, OR ICMP error, THEN delete unusable server
// from list and try the next one. Repeat 4,5,6 until success, or
// list empty; return.
// 1. If not provided, determine zone apex and fetch name servers
info!("Determining primary name server name, zone apex, and soa record");
let (apex, mname, soa) = match &self.zone {
Some(zone) => {
let tmp = update_helpers::find_mname_and_soa(env, zone)
.await
.context("fetching the SOA record")?;
(zone.clone(), tmp.0, tmp.1)
}
None => update_helpers::find_mname_and_zone_and_soa(env, &self.domain)
.await
.context("fetching the SOA record and determining the zone apex")?,
};
// // mname is only used to put the primary first in the later fetched
// // list of name servers, therefore, if we have IP addresses to sent
// // the update to, we don't need to put the primary first and can let
// // the user determine the ordering, and therefore can skip the SOA
// // query.
// let (apex, mname) = match (&self.zone, self.nameservers.is_empty()) {
// (Some(zone), true) => (
// zone.clone(),
// Some(update_helpers::find_mname(env, &zone).await?),
// ),
// (Some(zone), false) => (zone.clone(), None),
// (None, _) => {
// let (a, b) = update_helpers::find_mname_and_zone(env, &self.domain).await?;
// (a, Some(b))
// }
// };
info!("Parsing prerequisite arguments");
// Parse prerequisites before fetching nameservers, in case parsing fails
let prerequisites = self
.parse_prerequisites(apex.clone().flatten_into())
.context("parsing the prerequisite arguments")?;
// 1. (Cont.) If not provided, determine zone apex and fetch name servers
let nsnames = if self.nameservers.is_none() {
info!("Fetching authoritative name servers for {apex}");
Some(
// 3. Order nameserver list, listing primary first
update_helpers::determine_nsnames(env, &apex, &mname)
.await
.context("fetching the authoritative nameservers for the zone")?,
)
} else {
None
};
info!("Creating DNS UPDATE message");
// 4. Create UPDATE and send to first server in list
let msg = self.create_update_message(&apex, prerequisites, soa)?;
info!("Sending DNS UPDATE message");
self.send_update(env, msg, nsnames).await?;
Ok(())
}
fn parse_prerequisite_name_type(
args: &Vec<String>,
origin: &Name<Vec<u8>>,
) -> Result<Vec<NameTypeTuple>, Error> {
let mut records = Vec::new();
for arg in args {
if let Some((name, typ)) = arg.split_once(' ') {
let typ = Rtype::from_str(typ)
.map_err(|e| format!("Invalid resource record type '{typ}': {e}"))?;
let uncertain = UncertainName::<Vec<u8>>::from_str(name)
.map_err(|e| format!("Invalid domain name '{name}': {e}"))?;
let name = uncertain
.chain(origin)
.map_err(|_| {
format!("Combining {name}.{origin} is too long for a domain name")
})?
.to_name();
records.push((name, typ))
} else {
return Err(format!(
"Invalid prerequisite argument format. Expected format '<DOMAIN_NAME> <TYPE>', was '{arg}'."
)
.into());
}
}
Ok(records)
}
fn parse_prerequisite_name(
args: &Vec<String>,
origin: &Name<Vec<u8>>,
) -> Result<Vec<Name<Vec<u8>>>, Error> {
let mut names = Vec::new();
for name in args {
let uncertain = UncertainName::<Vec<u8>>::from_str(name)
.map_err(|e| format!("Invalid domain name '{name}': {e}"))?;
let name = uncertain
.chain(origin)
.map_err(|_| format!("Combining {name}.{origin} is too long for a domain name"))?
.to_name();
names.push(name)
}
Ok(names)
}
fn parse_prerequisite_rrset_exists_exact(
args: &Vec<String>,
origin: &Name<Vec<u8>>,
class: Class,
) -> Result<Vec<ParsedRecord>, Error> {
let mut records = Vec::new();
for arg in args {
let mut zonefile = Zonefile::new();
zonefile.extend_from_slice(arg.as_bytes());
zonefile.extend_from_slice(b"\n");
zonefile.set_default_class(class);
zonefile.set_origin(origin.clone().flatten_into());
if let Ok(Some(Entry::Record(mut record))) = zonefile.next_entry() {
record.set_ttl(Ttl::from_secs(0));
records.push(record.flatten_into());
} else {
return Err(
format!("Provided argument is not a valid resource record: {arg}").into(),
);
}
}
Ok(records)
}
fn parse_ttl(arg: &str) -> Result<Ttl, Error> {
Ok(Ttl::from_secs(
if let Some(ttl) = arg.strip_suffix('s') {
ttl.parse()
} else if let Some(ttl) = arg.strip_suffix('m') {
ttl.parse::<u32>().map(|t| t * 60)
} else if let Some(ttl) = arg.strip_suffix('h') {
ttl.parse::<u32>().map(|t| t * 3600)
} else if let Some(ttl) = arg.strip_suffix('d') {
ttl.parse::<u32>().map(|t| t * 86400)
} else {
arg.parse()
}
.map_err(|err| Error::from(format!("Invalid TTL: {err}")))?,
))
}
/// Create the packet of the update message to send to the name servers
fn create_update_message(
&self,
zone: &Name<Vec<u8>>,
prerequisites: UpdatePrerequisites,
soa: Soa<Name<Vec<u8>>>,
) -> Result<Vec<u8>, Error> {
// UPDATE message sections:
// Zone Section (= Question),
// Prerequisite Section (= Answer)
// Update Section (= Authority)
// Additional (= Additional)
let mut message = MessageBuilder::new_vec();
let header = message.header_mut();
header.set_opcode(Opcode::UPDATE);
header.set_qr(false);
debug!("Adding '{zone}' to zone section");
let mut zone_section = message.question();
zone_section
.push(Question::new(zone, Rtype::SOA, Class::IN))
.unwrap();
debug!("Adding prerequisites to prerequisites section");
let mut prereq_section = zone_section.answer();
Self::insert_prerequisites(prerequisites, &mut prereq_section)?;
let mut update_section = prereq_section.authority();
debug!("Adding update messages to update section");
match self.action {
UpdateAction::Add { rtype, ref rdata } => {
self.insert_add_updates(rtype, rdata, &soa, &mut update_section)?;
}
UpdateAction::Delete { rtype, ref rdata } => {
self.insert_delete_updates(rtype, rdata, &mut update_section)?;
}
UpdateAction::Clear => {
debug!("Inserting update message: clear all RRsets on domain");
update_section
.push(Self::create_all_rrset_deletion(self.domain.clone()))
.map_err(|e| {
format!("Failed to add RRset deletion RR to UPDATE message: {e}")
})?
}
}
// TODO: Providing additional data is not yet implemented
// let mut additional_section = update_section.additional();
Ok(update_section.finish())
}
fn parse_rdata(
rtype: Rtype,
rdata: &str,
) -> Result<ZoneRecordData<Vec<u8>, Name<Vec<u8>>>, Error> {
// TODO: add from_rtype_and_str to ZoneRecordData to skip this
// workaround with zonefile?
let mut zonefile = Zonefile::new();
// The origin, ttl and class are irrelevant here and only needed to
// parse the rdata
let rr = format!(". 1 IN {rtype} {rdata}\n");
zonefile.extend_from_slice(rr.as_bytes());
match zonefile.next_entry() {
Ok(Some(Entry::Record(record))) => Ok(record.data().clone().flatten_into()),
Ok(_) => unreachable!("We always create a record"),
Err(e) => {
Err(format!("Failed to parse rdata for {rtype} {rdata} -- Error: {e}").into())
}
}
}
fn create_rr_addition(
domain: Name<Vec<u8>>,
class: Class,
ttl: Ttl,
rdata: ZoneRecordData<Vec<u8>, Name<Vec<u8>>>,
) -> ParsedRecord {
// From [RFC2136] Section 2.5.1:
// RRs are added to the Update Section whose NAME, TYPE, TTL, RDLENGTH
// and RDATA are those being added, and CLASS is the same as the zone
// class.
ParsedRecord::new(domain, class, ttl, rdata)
}
fn create_rr_deletion(
domain: Name<Vec<u8>>,
rdata: ZoneRecordData<Vec<u8>, Name<Vec<u8>>>,
) -> ParsedRecord {
// From [RFC2136] Section 2.5.4:
// The NAME, TYPE, RDLENGTH and RDATA must match the RR being deleted.
// TTL must be specified as zero (0) [...].
// CLASS must be specified as NONE to distinguish this from an addition.
ParsedRecord::new(domain, Class::NONE, Ttl::from_secs(0), rdata)
}
fn create_rrset_deletion(domain: Name<Vec<u8>>, rtype: Rtype) -> ParsedRecord {
// From [RFC2136] Section 2.5.2:
// - One RR is added to the Update Section whose NAME and TYPE are those
// of the RRset to be deleted.
// - TTL must be specified as zero (0) [...].
// - CLASS must be specified as ANY.
// - RDLENGTH must be zero (0) and RDATA must therefore be empty.
let rdata = ZoneRecordData::Unknown(
UnknownRecordData::from_octets(rtype, Vec::new())
.expect("Failed to create empty rdata"),
);
ParsedRecord::new(domain, Class::ANY, Ttl::from_secs(0), rdata)
}
fn create_all_rrset_deletion(domain: Name<Vec<u8>>) -> ParsedRecord {
// From [RFC2136] Section 2.5.3:
// - One RR is added to the Update Section whose NAME is that of the
// name to be cleansed of RRsets.
// - TYPE must be specified as ANY.
// - TTL must be specified as zero (0) [...]
// - CLASS must be specified as ANY.
// - RDLENGTH must be zero (0) and RDATA must therefore be empty.
let rdata = ZoneRecordData::Unknown(
UnknownRecordData::from_octets(Rtype::ANY, Vec::new())
.expect("Failed to create empty rdata"),
);
ParsedRecord::new(domain, Class::ANY, Ttl::from_secs(0), rdata)
}
fn parse_prerequisites(&self, origin: Name<Vec<u8>>) -> Result<UpdatePrerequisites, Error> {
let mut prerequisites = UpdatePrerequisites {
rrset_exists: None,
rrset_exists_exact: None,
rrset_non_existent: None,
name_in_use: None,
name_not_in_use: None,
};
if let Some(ref v) = self.rrset_exists {
prerequisites.rrset_exists = Some(Self::parse_prerequisite_name_type(v, &origin)?)
}
if let Some(ref v) = self.rrset_exists_exact {
prerequisites.rrset_exists_exact = Some(Self::parse_prerequisite_rrset_exists_exact(
v, &origin, self.class,
)?)
}
if let Some(ref v) = self.rrset_non_existent {
prerequisites.rrset_non_existent = Some(Self::parse_prerequisite_name_type(v, &origin)?)
}
if let Some(ref v) = self.name_in_use {
prerequisites.name_in_use = Some(Self::parse_prerequisite_name(v, &origin)?)
}
if let Some(ref v) = self.name_not_in_use {
prerequisites.name_not_in_use = Some(Self::parse_prerequisite_name(v, &origin)?)
}
Ok(prerequisites)
}
fn insert_prerequisites(
prerequisites: UpdatePrerequisites,
prereq_section: &mut AnswerBuilder<Vec<u8>>,
) -> Result<(), Error> {
if let Some(rrset_exists) = prerequisites.rrset_exists {
for (domain, rtype) in rrset_exists {
debug!("Adding prerequisite (RRset exists): RR with name '{domain}' and type '{rtype}'");
prereq_section
.push(Self::create_prereq_rrset_exists(domain, rtype))
.map_err(|e| format!("Failed to add RR to UPDATE message: {e}"))?
}
}
if let Some(rrset_exists_exact) = prerequisites.rrset_exists_exact {
for rr in rrset_exists_exact {
debug!("Adding prerequisite (RRset exists exact): RR '{rr}'");
// The ttl is set while parsing the record, but in case that
// changes, here an extra check.
debug_assert!(rr.ttl() == Ttl::from_secs(0));
// From [RFC2136] Section 2.4.2 - RRset Exists (Value Dependent)
// - [...] an entire RRset whose preexistence is required.
// - NAME and TYPE are that of the RRset being denoted.
// - CLASS is that of the zone.
// - TTL must be specified as zero (0) [...]
prereq_section
.push(rr)
.map_err(|e| format!("Failed to add RR to UPDATE message: {e}"))?
}
}
if let Some(rrset_non_existent) = prerequisites.rrset_non_existent {
for (domain, rtype) in rrset_non_existent {
debug!("Adding prerequisite (RRset does not exist): RR with name '{domain}' and type '{rtype}'");
prereq_section
.push(Self::create_prereq_rrset_non_existent(domain, rtype))
.map_err(|e| format!("Failed to add RR to UPDATE message: {e}"))?
}
}
if let Some(name_in_use) = prerequisites.name_in_use {
for domain in name_in_use {
debug!("Adding prerequisite (Name in use): with name '{domain}'");
prereq_section
.push(Self::create_prereq_name_in_use(domain))
.map_err(|e| format!("Failed to add RR to UPDATE message: {e}"))?
}
}
if let Some(name_not_in_use) = prerequisites.name_not_in_use {
for domain in name_not_in_use {
debug!("Adding prerequisite (Name not in use): with name '{domain}'");
prereq_section
.push(Self::create_prereq_name_not_in_use(domain))
.map_err(|e| format!("Failed to add RR to UPDATE message: {e}"))?
}
}
Ok(())
}
fn create_prereq_rrset_exists(domain: Name<Vec<u8>>, rtype: Rtype) -> ParsedRecord {
// From [RFC2136] Section 2.4.1 - RRset Exists (Value Independent):
// - [...] a single RR whose NAME and TYPE are equal to that of the zone
// RRset whose existence is required.
// - RDLENGTH is zero and RDATA is therefore empty.
// - CLASS must be specified as ANY [...]
// - TTL is specified as zero (0).
let rdata = ZoneRecordData::Unknown(
UnknownRecordData::from_octets(rtype, Vec::new())
.expect("Failed to create empty rdata"),
);
ParsedRecord::new(domain, Class::ANY, Ttl::from_secs(0), rdata)
}
fn create_prereq_rrset_non_existent(domain: Name<Vec<u8>>, rtype: Rtype) -> ParsedRecord {
// From [RFC2136] Section 2.4.3 - RRset Does Not Exist
// - [...] a single RR whose NAME and TYPE are equal to that of the
// RRset whose nonexistence is required.
// - The RDLENGTH of this record is zero (0), and RDATA field is
// therefore empty.
// - CLASS must be specified as NONE [...]
// - TTL must be specified as zero (0).
let rdata = ZoneRecordData::Unknown(
UnknownRecordData::from_octets(rtype, Vec::new())
.expect("Failed to create empty rdata"),
);
ParsedRecord::new(domain, Class::NONE, Ttl::from_secs(0), rdata)
}
fn create_prereq_name_in_use(domain: Name<Vec<u8>>) -> ParsedRecord {
// From [RFC2136] Section 2.4.4 - Name Is In Use
// - [...] a single RR whose NAME is equal to that of the name whose
// ownership of an RR is required.
// - RDLENGTH is zero and RDATA is therefore empty.
// - CLASS must be specified as ANY [...]
// - TYPE must be specified as ANY [...]
// - TTL is specified as zero (0).
let rdata = ZoneRecordData::Unknown(
UnknownRecordData::from_octets(Rtype::ANY, Vec::new())
.expect("Failed to create empty rdata"),
);
ParsedRecord::new(domain, Class::ANY, Ttl::from_secs(0), rdata)
}
fn create_prereq_name_not_in_use(domain: Name<Vec<u8>>) -> ParsedRecord {
// From [RFC2136] Section 2.4.5 - Name Is Not In Use
// - [...] a single RR whose NAME is equal to that of the name whose
// nonownership of any RRs is required.
// - RDLENGTH is zero and RDATA is therefore empty.
// - CLASS must be specified as NONE.
// - TYPE must be specified as ANY.
// - TTL must be specified as zero (0).
let rdata = ZoneRecordData::Unknown(
UnknownRecordData::from_octets(Rtype::ANY, Vec::new())
.expect("Failed to create empty rdata"),
);
ParsedRecord::new(domain, Class::NONE, Ttl::from_secs(0), rdata)
}
/// Send the update packet to the names in nsnames in order until one responds
async fn send_update(
&self,
env: impl Env,
msg: Vec<u8>,
nsnames: Option<Vec<Name<Vec<u8>>>>,
) -> Result<(), Error> {
let msg = Message::from_octets(msg).unwrap();
let resolver = env.stub_resolver().await;
let tsig_key = self
.tsig
.as_ref()
.map(|tsig| {
Key::new(tsig.algorithm, &tsig.key, tsig.name.clone(), None, None)
.map_err(|e| format!("TSIG key is invalid: {e}"))
})
.transpose()?;
async fn connect_and_send_request(
env: &impl Env,
socket: SocketAddr,
msg: &Message<Vec<u8>>,
tsig_key: &Option<Key>,
) -> Result<Message<bytes::Bytes>, domain::net::client::request::Error> {
// TODO: Use TCP directly or at least implement TCP fallback
// // Using TCP directly to skip check whether the request fits
// // in a UDP packet.
// let tcp_connect = TcpConnect::new(socket);
// let (tcp_connection, transport) = multi_stream::Connection::new(tcp_connect);
// tokio::spawn(transport.run());
// let connection: Box<dyn SendRequest<_>> = if let Some(k) = &tsig_key {
// Box::new(tsig::Connection::new(k.clone(), tcp_connection))
// } else {
// Box::new(tcp_connection)
// };
let local: SocketAddr = if socket.is_ipv4() {
([0u8; 4], 0).into()
} else {
([0u16; 8], 0).into()
};
let dgram_connection = dgram::Connection::new(env.dgram(local, socket));
let connection: Box<dyn SendRequest<_>> = if let Some(k) = tsig_key {
Box::new(tsig::Connection::new(k.clone(), dgram_connection))
} else {
Box::new(dgram_connection)
};
let msg_dig = Update::update_to_dig_fmt(msg);
trace!("Sending UPDATE:\n{msg_dig}");
connection
.send_request(RequestMessage::new(msg.clone()).unwrap())
.get_response()
.await
}
if let Some(nsnames) = nsnames {
for name in nsnames {
let found_ips = resolver
.lookup_host(&name)
.await
.map_err(|e| format!("unable to lookup addresses for {name}: {e}"))?;
for socket in found_ips.port_iter(53) {
let resp = match connect_and_send_request(&env, socket, &msg, &tsig_key).await {
Ok(resp) => resp,
Err(err) => {
writeln!(env.stderr(), "{name} @ {socket}: {err}");
continue;
}
};
let mut buf = String::with_capacity(4096);
Self::answer_to_dig_fmt(&resp, &mut buf, socket);
trace!("Got Response:\n{buf}");
let rcode = resp.header().rcode();
if rcode == Rcode::SERVFAIL || rcode == Rcode::NOTIMP {
writeln!(env.stderr(), "Skipping {name} @ {socket}: {rcode}");
continue;
} else if rcode != Rcode::NOERROR {
writeln!(env.stdout(), "UPDATE response was {rcode}");
}
return Ok(());
}
}
} else {
// This is always the case if we reach this branch, but just in case
// of later changes we can leave this check in place
if let Some(ip) = &self.nameservers {
let socket = SocketAddr::new(*ip, 53);
let resp = match connect_and_send_request(&env, socket, &msg, &tsig_key).await {
Ok(resp) => resp,
Err(err) => {
return Err(format!("Unable to send update to {socket}: {err}").into());
}
};
let mut buf = String::with_capacity(4096);
Self::answer_to_dig_fmt(&resp, &mut buf, socket);
trace!("Got Response:\n{buf}");
let rcode = resp.header().rcode();
if rcode == Rcode::SERVFAIL || rcode == Rcode::NOTIMP {
return Err(
format!("Name server {socket} was unable to handle the update. Got response: {rcode}").into()
);
} else if rcode != Rcode::NOERROR {
writeln!(env.stdout(), "UPDATE response was {rcode}");
}
return Ok(());
}
}
// Our list of nsnames has been exhausted, we can only report that
// we couldn't find anything.
writeln!(env.stdout(), "No successful responses");
Ok(())
}
/// Compares two 32-bit serial numbers as defined in [RFC1982]. Returns
/// Ordering::Less if a < b, Ordering::Equal if a == b, and
/// Ordering::Greater if a > b. The result is undefined if a != b but
/// neither is greater or smaller (see [RFC1982] Section 3.2.).
/// The maximumm increment for a SOA serial is 2147483647 as per [RFC1982].
///
/// [RFC1982]: https://www.rfc-editor.org/rfc/rfc1982.html
fn compare_serial(a: u32, b: u32) -> Ordering {
// Code from nsd:util.c:compare_serial
if a == b {
Ordering::Equal
} else if (a < b && b - a < SOA_SERIAL_CUTOFF) || (a > b && a - b > SOA_SERIAL_CUTOFF) {
Ordering::Less
} else {
Ordering::Greater
}
}
/// Verify that the SOA serial of the new rdata is within the bounds of an
/// allowed increment.
fn verify_soa_serial_increment(old: &Soa<Name<Vec<u8>>>, new: &Soa<Name<Vec<u8>>>) -> bool {
// The new soa serial should be greater than the old serial
Self::compare_serial(new.serial().into_int(), old.serial().into_int()) == Ordering::Greater
}
fn insert_add_updates(
&self,
rtype: Rtype,
rdata: &Vec<String>,
soa: &Soa<Name<Vec<u8>>>,
update_section: &mut AuthorityBuilder<Vec<u8>>,
) -> Result<(), Error> {
if rdata.is_empty() {
return Err("Provide at least one RDATA item to add".into());
}
for r in rdata {
debug!("Parsing RDATA '{r}'");
let rdata = Self::parse_rdata(rtype, r)?;
if let ZoneRecordData::Soa(new_soa) = &rdata {
if !Self::verify_soa_serial_increment(soa, new_soa) {
let old_serial = soa.serial();
let new_serial = new_soa.serial();
return Err(
format!(
"SOA serial is only allowed to be incremented (and maximally by {SOA_SERIAL_CUTOFF} at once), got old serial = {old_serial} and new serial = {new_serial}").into()
);
}
}
debug!("Inserting update message: add {rtype} RR with RDATA '{rdata}'");
update_section
.push(Self::create_rr_addition(
self.domain.clone(),
self.class,
self.ttl,
rdata,
))
.map_err(|e| format!("Failed to add RR to UPDATE message: {e}"))?
}
Ok(())
}
fn insert_delete_updates(
&self,
rtype: Rtype,
rdata: &Vec<String>,
update_section: &mut AuthorityBuilder<Vec<u8>>,
) -> Result<(), Error> {
if rdata.is_empty() {
debug!("Inserting update message: delete the {rtype} RRset");
update_section
.push(Self::create_rrset_deletion(self.domain.clone(), rtype))
.map_err(|e| format!("Failed to add RRset deletion RR to UPDATE message: {e}"))?
} else {
for r in rdata {
debug!("Parsing RDATA '{r}'");
let rdata = Self::parse_rdata(rtype, r)?;
debug!("Inserting update message: delete {rtype} RR with RDATA '{rdata}'");
update_section
.push(Self::create_rr_deletion(self.domain.clone(), rdata))
.map_err(|e| format!("Failed to add RR deletion RR to UPDATE message: {e}"))?
}
}
Ok(())
}
fn update_to_dig_fmt(msg: &Message<Vec<u8>>) -> String {
fn write_record_item_additional(
target: &mut String,
item: &domain::base::ParsedRecord<Vec<u8>>,
) {
let parsed = item.to_any_record::<AllRecordData<_, _>>();
if parsed.is_err() {
target.push_str("; ");
}
let data = match parsed {
Ok(item) => item.data().to_string(),
Err(_) => "<invalid data>".into(),
};
target.push_str(&format!(
"{} {} {} {} {}\n",
item.owner(),
item.ttl().as_secs(),
item.class(),
item.rtype(),
data
))
}
fn write_record_item_update(
target: &mut String,
item: &domain::base::ParsedRecord<Vec<u8>>,
class: Class,
) {
let parsed = item.to_any_record::<AllRecordData<_, _>>();
let data = match parsed {
Ok(item) => item.data().to_string(),
Err(_) => "<invalid data>".into(),
};
target.push_str("; ");
match (
item.class(),
item.ttl().as_secs(),
item.rtype(),
item.rdlen(),
) {
(Class::ANY, 0, Rtype::ANY, 0) => {
target.push_str(&format!("Clearing domain: {}\n", item.owner()))
}
(Class::ANY, 0, _, 0) => target.push_str(&format!(
"Deleting RRset: {} {} {}\n",
item.owner(),
class,
item.rtype()
)),
(Class::NONE, 0, _, _) => target.push_str(&format!(
"Deleting RR: {} {} {} {}\n",
item.owner(),
class,
item.rtype(),
data
)),
(_, _, _, _) => {
target.push_str("Adding RR: ");
target.push_str(&format!(
"{} {} {} {} {}\n",
item.owner(),
item.ttl().as_secs(),
class,
item.rtype(),
data
));
}
}
}
fn write_record_item_prerequisite(
target: &mut String,
item: &domain::base::ParsedRecord<Vec<u8>>,
class: Class,
) {
let parsed = item.to_any_record::<AllRecordData<_, _>>();
let data = match parsed {
Ok(item) => item.data().to_string(),
Err(_) => "<invalid data>".into(),
};
target.push_str("; ");
match (
item.class(),
item.ttl().as_secs(),
item.rtype(),
item.rdlen(),
) {
(Class::ANY, 0, Rtype::ANY, 0) => {
target.push_str(&format!("Name is in use: {}\n", item.owner()))
}
(Class::NONE, 0, Rtype::ANY, 0) => {
target.push_str(&format!("Name is NOT in use: {}\n", item.owner(),))
}
(Class::ANY, 0, _, 0) => target.push_str(&format!(
"RRset exists (regardless of content): {} {}\n",
item.owner(),
item.rtype()
)),
(Class::NONE, 0, _, 0) => target.push_str(&format!(
"RRset does not exist: {} {}\n",
item.owner(),
item.rtype()
)),
(_, 0, _, _) => target.push_str(&format!(
"RRset exists with exact members: {} {} {} {}\n",
item.owner(),
class,
item.rtype(),
data
)),
(_, _, _, _) => unreachable!("Prerequisites all have a TTL of 0"),
}
}
let mut target = String::with_capacity(4096);
// Header
let counts = msg.header_counts();
target.push_str(&format!(
"; ZONE: {}, PREREQUISITE: {}, UPDATE: {}, ADDITIONAL: {}\n",
counts.qdcount(),
counts.ancount(),
counts.nscount(),
counts.arcount()
));
let opt = msg.opt(); // We need it further down ...
if let Some(opt) = opt.as_ref() {
target.push_str("\n;; OPT PSEUDOSECTION:\n");
target.push_str(&format!(
"; EDNS: version {}, flags: {}; udp: {}\n",
opt.version(),
if opt.dnssec_ok() { "do" } else { "" },
opt.udp_payload_size()
));
}
let mut class = None;
// Zone section
let questions = msg.question();
target.push_str(";; ZONE SECTION:\n");
// Should only be one...
for item in questions {
let item = item.expect("we created this message, it should be valid");
target.push_str(&format!("; {item}\n"));
class = Some(item.qclass());
}
// There is always a zone section in an update and therefore always
// a class
let class = class.expect("There is always a zone section");
// Prerequisite section
let answer = questions
.answer()
.expect("We created this message outselves, it should be correct");
if counts.ancount() > 0 {
target.push_str("\n;; PREREQUISITE SECTION:\n");
for item in answer {
let item = item.expect("we created this message, it should be valid");
write_record_item_prerequisite(&mut target, &item, class);
}
}
// Update section
let authority = answer
.next_section()
.expect("we created this message, it should be valid")
.unwrap();
if counts.nscount() > 0 {
target.push_str("\n;; UPDATE SECTION:\n");
for item in authority {
let item = item.expect("we created this message, it should be valid");
write_record_item_update(&mut target, &item, class);
}
}
// Additional
let additional = authority
.next_section()
.expect("we created this message, it should be valid")
.unwrap();
if counts.arcount() > 0 || (opt.is_none() && counts.arcount() > 0) {
target.push_str("\n;; ADDITIONAL SECTION:\n");
for item in additional {
let item = item.expect("we created this message, it should be valid");
if item.rtype() != Rtype::OPT {
write_record_item_additional(&mut target, &item);
}
}
}
target
}
// Code from dnsi:src/output/dig.rs with io:Write replaced with push_str
fn answer_to_dig_fmt(msg: &Message<bytes::Bytes>, target: &mut String, socket: SocketAddr) {
fn write_record_item(target: &mut String, item: &domain::base::ParsedRecord<bytes::Bytes>) {
let parsed = item.to_any_record::<AllRecordData<_, _>>();
if parsed.is_err() {
target.push_str("; ");
}
let data = match parsed {
Ok(item) => item.data().to_string(),
Err(_) => "<invalid data>".into(),
};
target.push_str(&format!(
"{} {} {} {} {}\n",
item.owner(),
item.ttl().as_secs(),
item.class(),
item.rtype(),
data
))
}
// Header
let header = msg.header();
let counts = msg.header_counts();
target.push_str(&format!(
";; ->>HEADER<<- opcode: {}, rcode: {}, id: {}\n",
header.opcode(),
header.rcode(),
header.id()
));
target.push_str(&format!(";; flags: {}", header.flags()));
target.push_str(&format!(
"; QUERY: {}, ANSWER: {}, AUTHORITY: {}, ADDITIONAL: {}\n",
counts.qdcount(),
counts.ancount(),
counts.nscount(),
counts.arcount()
));
let opt = msg.opt(); // We need it further down ...
if let Some(opt) = opt.as_ref() {
target.push_str("\n;; OPT PSEUDOSECTION:\n");
target.push_str(&format!(
"; EDNS: version {}, flags: {}; udp: {}\n",
opt.version(),
if opt.dnssec_ok() { "do" } else { "" },
opt.udp_payload_size()
));
for option in opt.opt().iter::<AllOptData<_, _>>() {
use AllOptData::*;
match option {
Ok(opt) => match opt {
Nsid(nsid) => target.push_str(&format!("; NSID: {nsid}\n")),
Dau(dau) => target.push_str(&format!("; DAU: {dau}\n")),
Dhu(dhu) => target.push_str(&format!("; DHU: {dhu}\n")),
N3u(n3u) => target.push_str(&format!("; N3U: {n3u}\n")),
Expire(expire) => target.push_str(&format!("; EXPIRE: {expire}\n")),
TcpKeepalive(opt) => target.push_str(&format!(
"; TCP KEEPALIVE: {}\n",
opt.timeout().map_or("".to_string(), |t| format!(
"{:.1} secs",
Duration::from(t).as_secs_f64()
))
)),
Padding(padding) => target.push_str(&format!("; PADDING: {padding}\n")),
ClientSubnet(opt) => target.push_str(&format!("; CLIENTSUBNET: {opt}\n")),
Cookie(cookie) => target.push_str(&format!("; COOKIE: {cookie}\n")),
Chain(chain) => target.push_str(&format!("; CHAIN: {chain}\n")),
KeyTag(keytag) => target.push_str(&format!("; KEYTAG: {keytag}\n")),
ExtendedError(extendederror) => {
target.push_str(&format!("; EDE: {extendederror}\n"))
}
Other(other) => {
target.push_str(&format!("; {}\n", other.code()));
}
_ => target.push_str("Unknown OPT\n"),
},
Err(err) => {
target.push_str(&format!("; ERROR: bad option: {err}.\n"));
}
}
}
}
// Question
let questions = msg.question();
if counts.qdcount() > 0 {
target.push_str(";; QUESTION SECTION:\n");
for item in questions {
let item = item.expect("we created this message, it should be valid");
target.push_str(&format!(";{item}\n"));
}
}
// Answer
let section = questions
.answer()
.expect("we created this message, it should be valid");
if counts.ancount() > 0 {
target.push_str("\n;; ANSWER SECTION:\n");
for item in section {
let item = item.expect("we created this message, it should be valid");
write_record_item(target, &item);
}
}
// Authority
let section = section
.next_section()
.expect("we created this message, it should be valid")
.unwrap();
if counts.nscount() > 0 {
target.push_str("\n;; AUTHORITY SECTION:\n");
for item in section {
let item = item.expect("we created this message, it should be valid");
write_record_item(target, &item);
}
}
// Additional
let section = section
.next_section()
.expect("we created this message, it should be valid")
.unwrap();
if counts.arcount() > 1 || (opt.is_none() && counts.arcount() > 0) {
target.push_str("\n;; ADDITIONAL SECTION:\n");
for item in section {
let item = item.expect("we created this message, it should be valid");
if item.rtype() != Rtype::OPT {
write_record_item(target, &item);
}
}
}
target.push_str(&format!(";; SERVER: {}#{}\n", socket.ip(), socket.port(),));
target.push_str(&format!(";; MSG SIZE rcvd: {}\n", msg.as_slice().len()));
}
}
//------------ UpdateAction --------------------------------------------------
#[derive(Clone, Debug, PartialEq, Eq, Subcommand)]
enum UpdateAction {
/// Add RRs to a domain
Add {
/// RRtype
#[arg(value_name = "RRTYPE")]
rtype: Rtype,
/// RDATA (One or more). Each argument corresponds to a single RR's
/// RDATA, so beware of (shell and DNS) quoting rules.
///
/// Each RDATA argument will be parsed as if it was read from a zone file.
///
/// Examples:
/// $ dnst update some.example.com add TXT \
/// '"Spacious String" "Another string for the same TXT record"' \
/// '"This is another TXT RR"'
#[arg(value_name = "RDATA", verbatim_doc_comment)]
rdata: Vec<String>,
},
/// Delete specific RRs or a complete RRsets from a domain
Delete {
/// RRtype
#[arg(value_name = "RRTYPE")]
rtype: Rtype,
/// RDATA (Optional. Delete whole RRset, if none provided). Each
/// argument corresponds to a single RR's RDATA, so beware of (shell
/// and DNS) quoting rules.
///
/// Each RDATA argument will be parsed as if it was read from a zone file.
///
/// For quoting example see `dnst update add --help`
#[arg(value_name = "RDATA")]
rdata: Vec<String>,
},
/// Clear domain, aka delete all RRsets on the domain
Clear,
}
//------------ UpdatePrerequisites -------------------------------------------
#[derive(Clone, Debug)]
struct UpdatePrerequisites {
rrset_exists: Option<Vec<NameTypeTuple>>,
rrset_exists_exact: Option<Vec<ParsedRecord>>,
rrset_non_existent: Option<Vec<NameTypeTuple>>,
name_in_use: Option<Vec<Name<Vec<u8>>>>,
name_not_in_use: Option<Vec<Name<Vec<u8>>>>,
}
//------------ LdnsUpdate ----------------------------------------------------
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LdnsUpdate {
/// Domain name to update
domain: Name<Vec<u8>>,
/// IP address to associate with the given domain name.
/// Use `none` to delete the records for the domain name.
ip: Option<IpAddr>,
/// Zone to update
zone: Option<Name<Vec<u8>>>,
/// TSIG credentials for the UPDATE packet
tsig: Option<TSigInfo>,
}
const LDNS_HELP: &str = "\
ldns-update domain [zone] ip tsig_name tsig_alg tsig_hmac
send a dynamic update packet to <ip>
Use 'none' instead of ip to remove any previous address
If 'zone' is not specified, try to figure it out from the zone's SOA
Example: ldns-update my.example.org 1.2.3.4
This command exists for compatibility purposes.
For a more modern version of this command try `dnst update`\
";
impl LdnsCommand for LdnsUpdate {
const NAME: &'static str = "update";
const HELP: &'static str = LDNS_HELP;
const COMPATIBLE_VERSION: &'static str = "1.8.4";
fn parse_ldns<I: IntoIterator<Item = OsString>>(args: I) -> Result<Args, Error> {
let args: Vec<_> = args.into_iter().collect();
// We have this signature
// <DOMAIN> [ZONE] <IP> [TSIG_NAME TSIG_ALG TSIG_HMAC]
// So we parse it by splitting the cases by number of arguments:
// 1: DOMAIN IP
// 2: DOMAIN ZONE IP
// 4: DOMAIN IP TSIG_NAME TSIG_ALG TSIG_HMAC
// 5: DOMAIN ZONE IP TSIG_NAME TSIG_ALG TSIG_HMAC
let (domain, zone, ip, tsig) = match &args[..] {
[domain, ip] => (domain, None, ip, None),
[domain, zone, ip] => (domain, Some(zone), ip, None),
[domain, ip, tsig_name, tsig_key, tsig_hmac] => {
(domain, None, ip, Some((tsig_name, tsig_key, tsig_hmac)))
}
[domain, zone, ip, tsig_name, tsig_alg, tsig_hmac] => (
domain,
Some(zone),
ip,
Some((tsig_name, tsig_alg, tsig_hmac)),
),
_ => {
return if args.len() < 2 {
Err("Not enough arguments. ldns-update requires at least 2 arguments".into())
} else if args.len() > 6 {
Err("Too many arguments. ldns-update requires at most 6 arguments".into())
} else {
Err("Cannot take 4 arguments. ldns-update needs 2, 3, 5 or 6 arguments".into())
}
}
};
let domain = parse_os("domain name", domain)?;
let ip = if *ip != "none" {
Some(parse_os("IP address", ip)?)
} else {
None
};
let zone = match zone {
Some(z) => Some(parse_os("zone", z)?),
None => None,
};
Ok(Args::from(Command::LdnsUpdate(Self {
domain,
ip,
zone,
tsig: match tsig {
Some((name, algorithm, key)) => Some(TSigInfo {
name: parse_os("TSIG name", name)?,
key: parse_os_with("TSIG key", key, base64::decode)?,
algorithm: parse_os("TSIG algorithm", algorithm)?,
}),
None => None,
},
})))
}
}
impl LdnsUpdate {
pub fn execute(self, env: impl Env) -> Result<(), Error> {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(self.run(&env))
}
/// Run the command as an async function
pub async fn run(self, env: &impl Env) -> Result<(), Error> {
writeln!(
env.stdout(),
";; trying UPDATE with FQDN \"{}\" and IP \"{}\"",
self.domain,
self.ip.map_or("<none>".into(), |ip| ip.to_string())
);
let soa_zone;
let soa_mname;
if let Some(zone) = &self.zone {
(soa_mname, _) = update_helpers::find_mname_and_soa(env, zone).await?;
soa_zone = zone.clone();
} else {
let name = self.domain.clone();
(soa_zone, soa_mname, _) =
update_helpers::find_mname_and_zone_and_soa(env, &name).await?;
};
let nsnames = update_helpers::determine_nsnames(env, &soa_zone, &soa_mname).await?;
let msg = self.create_update_message(&soa_zone);
self.send_update(env, msg, nsnames).await
}
/// Create the packet of the update message to send to the name servers
fn create_update_message(&self, zone: &Name<Vec<u8>>) -> Vec<u8> {
let mut message = MessageBuilder::new_vec();
let header = message.header_mut();
header.set_opcode(Opcode::UPDATE);
header.set_qr(false);
let mut zone_section = message.question();
zone_section
.push(Question::new(zone, Rtype::SOA, Class::IN))
.unwrap();
let mut update_section = zone_section.authority();
// If we have an IP address, remove that ip address
// else remove A and/or AAAA as defined in RFC2136 2.5.2.
if let Some(ip) = self.ip {
let rdata: AllRecordData<&[u8], Name<&[u8]>> = match ip {
IpAddr::V4(ip) => AllRecordData::A(A::new(ip)),
IpAddr::V6(ip) => AllRecordData::Aaaa(Aaaa::new(ip)),
};
update_section
.push(Record::new(
&self.domain,
Class::IN,
Ttl::from_secs(300),
rdata,
))
.unwrap();
} else {
let tmp = Record::new(
&self.domain,
Class::ANY,
Ttl::from_secs(0),
UnknownRecordData::from_octets(Rtype::A, &[]).unwrap(),
);
update_section.push(tmp).unwrap();
update_section
.push(Record::new(
&self.domain,
Class::ANY,
Ttl::from_secs(0),
UnknownRecordData::from_octets(Rtype::AAAA, &[]).unwrap(),
))
.unwrap();
}
update_section.finish()
}
/// Send the update packet to the names in nsnames in order until one responds
async fn send_update(
&self,
env: impl Env,
msg: Vec<u8>,
nsnames: Vec<Name<Vec<u8>>>,
) -> Result<(), Error> {
let msg = Message::from_octets(msg).unwrap();
let resolver = env.stub_resolver().await;
let tsig_key = self
.tsig
.as_ref()
.map(|tsig| {
Key::new(tsig.algorithm, &tsig.key, tsig.name.clone(), None, None)
.map_err(|e| format!("TSIG key is invalid: {e}"))
})
.transpose()?;
for name in nsnames {
let found_ips = resolver
.lookup_host(&name)
.await
.map_err(|e| format!("unable to look up addresses for {name}: {e}"))?;
for socket in found_ips.port_iter(53) {
let local: SocketAddr = if socket.is_ipv4() {
([0u8; 4], 0).into()
} else {
([0u16; 8], 0).into()
};
let dgram_connection = dgram::Connection::new(env.dgram(local, socket));
let connection: Box<dyn SendRequest<_>> = if let Some(k) = &tsig_key {
Box::new(tsig::Connection::new(k.clone(), dgram_connection))
} else {
Box::new(dgram_connection)
};
let response = connection
.send_request(RequestMessage::new(msg.clone()).unwrap())
.get_response()
.await;
let resp = match response {
Ok(resp) => resp,
Err(err) => {
writeln!(env.stderr(), "{name} @ {socket}: {err}");
continue;
}
};
let rcode = resp.header().rcode();
if rcode != Rcode::NOERROR {
writeln!(env.stdout(), ";; UPDATE response was {rcode}");
}
return Ok(());
}
}
// Our list of nsnames has been exhausted, we can only report that
// we couldn't find anything.
writeln!(env.stdout(), ";; No responses");
Ok(())
}
}
//------------ update_helpers ------------------------------------------------
mod update_helpers {
use super::*;
/// Find the MNAME by sending a SOA query for the zone
pub async fn find_mname_and_soa(
env: &impl Env,
zone: &Name<Vec<u8>>,
) -> Result<(Name<Vec<u8>>, Soa<Name<Vec<u8>>>), Error> {
let resolver = env.stub_resolver().await;
debug!("Querying resolver for SOA of {zone}");
let response = resolver
.query(Question::new(&zone, Rtype::SOA, Class::IN))
.await
.map_err(|e| format!("unable to lookup {zone}/SOA: {e}"))?;
debug!("Reading response from resolver");
let mut answer = response.answer()?.limit_to::<Soa<_>>();
if let Some(Ok(soa)) = answer.next() {
Ok((
soa.data().mname().to_name(),
soa.data().clone().flatten_into(),
))
} else {
Err(format!("No SOA record found for {zone}").into())
}
}
/// Find the MNAME and zone
///
/// This is achieved in 3 steps:
/// 1. Get the MNAME with a SOA query for the domain name
/// 2. Get the IP addresses for the MNAME
/// 3. Send a SOA query to that IP address and use the owner as zone
/// and the MNAME from that response.
pub async fn find_mname_and_zone_and_soa(
env: &impl Env,
name: &Name<Vec<u8>>,
) -> Result<(Name<Vec<u8>>, Name<Vec<u8>>, Soa<Name<Vec<u8>>>), Error> {
let resolver = env.stub_resolver().await;
debug!("Querying resolver for SOA of {name}");
// Step 1 - first find a name server that should know *something*
let response = resolver
.query(Question::new(&name, Rtype::SOA, Class::IN))
.await
.map_err(|e| format!("unable to lookup {name}/SOA: {e}"))?;
debug!("Reading response from resolver");
// We look in both the answer and authority sections.
// The answer section is used if the domain name is the zone apex,
// otherwise the SOA is in the authority section.
let mut sections = response
.answer()?
.limit_to_in::<Soa<_>>()
.chain(response.authority()?.limit_to_in::<Soa<_>>());
let Some(soa) = sections.next() else {
return Err("no SOA found".into());
};
let soa_mname: Name<Vec<u8>> = soa?.data().mname().to_name();
debug!("Querying for the IP address of {soa_mname}");
// Step 2 - find SOA MNAME IP address, add to resolver
let response = resolver
.lookup_host(&soa_mname)
.await
.map_err(|e| format!("unable to look up addresses for {soa_mname}: {e}"))?;
let Some(ipaddr) = response.iter().next() else {
return Err(format!("No A or AAAA record found for {soa_mname}").into());
};
// Step 3 - Redo SOA query, sending to SOA MNAME directly.
let mut conf = ResolvConf::new();
conf.servers = vec![ServerConf::new(
SocketAddr::new(ipaddr, 53),
Transport::UdpTcp,
)];
// Querying the SOA RR again, but from the primary directly, makes
// sure that we have an up-to-date SOA record and not a cached
// version. This would be relevant if we'd want to implement update
// ordering, or want to update the SOA serial.
let resolver = env.stub_resolver_from_conf(conf).await;
debug!("Querying primary name server directly for SOA of {name}");
let response = resolver
.query(Question::new(&name, Rtype::SOA, Class::IN))
.await
.map_err(|e| format!("unable to lookup {name}/SOA: {e}"))?;
debug!("Reading response from primary name server");
// We look in both the answer and authority sections.
// The answer section is used if the domain name is the zone apex,
// otherwise the SOA is in the authority section.
let mut sections = response
.answer()?
.limit_to_in::<Soa<_>>()
.chain(response.authority()?.limit_to_in::<Soa<_>>());
let Some(soa) = sections.next() else {
return Err("no SOA found".into());
};
let soa = soa?;
let zone = soa.owner().to_name();
let mname = soa.data().mname().to_name();
Ok((zone, mname, soa.data().clone().flatten_into()))
}
/// Send an NS query to find all name servers for the given zone
///
/// The name server with the given MNAME is put at the start of the list.
// async fn determine_nsnames(
pub async fn determine_nsnames(
env: &impl Env,
zone: &Name<Vec<u8>>,
mname: &Name<Vec<u8>>,
) -> Result<Vec<Name<Vec<u8>>>, Error> {
debug!("Querying {mname} for NS RRset of {zone}");
let response = env
.stub_resolver()
.await
.query(Question::new(&zone, Rtype::NS, Class::IN))
.await
.map_err(|e| format!("unable to lookup {zone}/NS: {e}"))?;
debug!("Reading response from {mname}");
let mut nsnames = response
.answer()?
.limit_to_in::<Ns<_>>()
.map(|ns| Ok(ns?.data().nsdname().to_name::<Vec<u8>>()))
.collect::<Result<Vec<_>, Error>>()?;
// The MNAME should be tried first according to RFC2136 4.3
// so we put that NSNAME first in the list.
if let Some(mname_idx) = nsnames.iter().position(|name| name == mname) {
nsnames.swap(0, mname_idx);
}
Ok(nsnames)
}
}
//------------ test ----------------------------------------------------------
#[cfg(test)]
mod test {
use std::str::FromStr;
use domain::base::iana::Class;
use domain::base::{Name, Rtype, Ttl};
use domain::{tsig::Algorithm, utils::base64};
use crate::commands::update::{LdnsUpdate, UpdateAction};
use crate::{commands::Command, env::fake::FakeCmd};
use super::{TSigInfo, Update};
#[track_caller]
fn parse_ldns(cmd: FakeCmd) -> LdnsUpdate {
let res = cmd.parse().unwrap();
let Command::LdnsUpdate(x) = res.command else {
panic!("Not an Update!");
};
x
}
#[track_caller]
fn parse(cmd: FakeCmd) -> Update {
let res = cmd.parse().unwrap();
let Command::Update(x) = res.command else {
panic!("Not an Update!");
};
x
}
// TODO: Add tests triggering the runtime checks
// TODO: Add stelline tests
#[test]
fn dnst_parse() {
let cmd = FakeCmd::new(["dnst", "update"]);
cmd.parse().unwrap_err();
cmd.args(["example.test"]).parse().unwrap_err();
cmd.args(["--zone", "example.test"]).parse().unwrap_err();
cmd.args(["--zone", "example.test", "ns.example.test"])
.parse()
.unwrap_err();
cmd.args(["example.test", "bar.test", "none"])
.parse()
.unwrap_err();
// Error when missing rtype to add (missing rdata is a runtime error)
cmd.args(["example.test", "add"]).parse().unwrap_err();
// Error when missing rtype
cmd.args(["example.test", "delete"]).parse().unwrap_err();
let base = Update {
domain: "example.test".parse().unwrap(),
zone: None,
tsig: None,
// This is not actually a default, but I need to add something here
action: UpdateAction::Add {
rtype: Rtype::A,
rdata: vec![String::from("127.0.0.1")],
},
class: Class::IN,
ttl: Ttl::from_secs(3600),
nameservers: Default::default(),
rrset_exists: None,
rrset_exists_exact: None,
rrset_non_existent: None,
name_in_use: None,
name_not_in_use: None,
};
let res = parse(cmd.args(["example.test", "add", "A", "127.0.0.1"]));
assert_eq!(res, base);
let res = parse(cmd.args(["example.test", "add", "A", "127.0.0.1", "127.0.0.2"]));
assert_eq!(
res,
Update {
action: UpdateAction::Add {
rtype: Rtype::A,
rdata: vec!["127.0.0.1".into(), "127.0.0.2".into()],
},
..base.clone()
}
);
let res = parse(cmd.args(["example.test", "delete", "AAAA", "::1"]));
assert_eq!(
res,
Update {
action: UpdateAction::Delete {
rtype: Rtype::AAAA,
rdata: vec!["::1".into()],
},
..base.clone()
}
);
let res = parse(cmd.args(["example.test", "clear"]));
assert_eq!(
res,
Update {
action: UpdateAction::Clear,
..base.clone()
}
);
let res = parse(cmd.args([
"example.test",
"--tsig",
"somekey:1234",
"--ttl",
"60",
"--server",
"127.0.0.9",
"add",
"TXT",
"Hallo",
]));
assert_eq!(
res,
Update {
tsig: Some(TSigInfo {
name: "somekey".parse().unwrap(),
key: base64::decode("1234").unwrap(),
algorithm: Algorithm::Sha256,
}),
ttl: Ttl::from_secs(60),
nameservers: Some([127, 0, 0, 9].into()),
action: UpdateAction::Add {
rtype: Rtype::TXT,
rdata: vec!["Hallo".into()]
},
..base.clone()
}
);
let res = parse(cmd.args(["example.test", "--zone", "test", "add", "A", "127.0.0.1"]));
assert_eq!(
res,
Update {
zone: Some(Name::from_str("test").unwrap()),
..base.clone()
}
);
// Parsing the prerequisites arguments here doesn't make much sense as
// they only get validated at runtime
}
#[test]
fn ldns_parse() {
let cmd = FakeCmd::new(["ldns-update"]);
let base = LdnsUpdate {
domain: "foo.test".parse().unwrap(),
ip: None,
zone: None,
tsig: None,
};
cmd.args(["foo.test"]).parse().unwrap_err();
let res = parse_ldns(cmd.args(["foo.test", "none"]));
assert_eq!(res, base.clone());
let res = parse_ldns(cmd.args(["foo.test", "1.1.1.1"]));
assert_eq!(
res,
LdnsUpdate {
ip: Some("1.1.1.1".parse().unwrap()),
..base.clone()
}
);
let res = parse_ldns(cmd.args(["foo.test", "base.test", "1.1.1.1"]));
assert_eq!(
res,
LdnsUpdate {
ip: Some("1.1.1.1".parse().unwrap()),
zone: Some("base.test".parse().unwrap()),
..base.clone()
}
);
}
#[test]
fn run_with_stelline() {
let rpl = "
CONFIG_END
SCENARIO_BEGIN
RANGE_BEGIN 0 100
ENTRY_BEGIN
MATCH question
ADJUST copy_id copy_query
REPLY QR
SECTION QUESTION
foo.test IN SOA
SECTION ANSWER
foo.test 0 IN SOA ns.foo.test admin.foo.test 1 1 1 1 1
SECTION AUTHORITY
foo.test 0 IN SOA ns.foo.test admin.foo.test 1 1 1 1 1
ENTRY_END
ENTRY_BEGIN
MATCH question
ADJUST copy_id copy_query
REPLY QR
SECTION QUESTION
zone.foo.test IN SOA
SECTION ANSWER
zone.foo.test 0 IN SOA ns.foo.test admin.foo.test 1 1 1 1 1
ENTRY_END
ENTRY_BEGIN
MATCH question
ADJUST copy_id copy_query
REPLY QR
SECTION QUESTION
zone.foo.test IN NS
SECTION ANSWER
zone.foo.test IN 0 NS ns.foo.test
ENTRY_END
ENTRY_BEGIN
MATCH question
ADJUST copy_id copy_query
REPLY QR
SECTION QUESTION
foo.test IN NS
SECTION ANSWER
foo.test IN 0 NS ns.foo.test
ENTRY_END
ENTRY_BEGIN
MATCH question
ADJUST copy_id copy_query
REPLY QR
SECTION QUESTION
ns.foo.test IN A
SECTION ANSWER
ns.foo.test IN 0 A 12.34.56.78
ENTRY_END
ENTRY_BEGIN
MATCH question
ADJUST copy_id copy_query
REPLY QR
SECTION QUESTION
ns.foo.test IN AAAA
SECTION ANSWER
ENTRY_END
ENTRY_BEGIN
MATCH question opcode
ADJUST copy_id copy_query
OPCODE UPDATE
REPLY QR
SECTION QUESTION
zone.foo.test IN SOA
SECTION ANSWER
ENTRY_END
RANGE_END
SCENARIO_END
";
let cmd = FakeCmd::new(["ldns-update", "foo.test", "zone.foo.test", "none"])
.stelline(rpl.as_bytes(), "update.rpl");
let res = cmd.run();
assert_eq!(res.exit_code, 0);
assert_eq!(
res.stdout,
";; trying UPDATE with FQDN \"foo.test\" and IP \"<none>\"\n"
);
assert_eq!(res.stderr, "");
let cmd = FakeCmd::new(["ldns-update", "foo.test", "none"])
.stelline(rpl.as_bytes(), "update.rpl");
let res = cmd.run();
assert_eq!(res.exit_code, 0);
assert_eq!(
res.stdout,
";; trying UPDATE with FQDN \"foo.test\" and IP \"<none>\"\n"
);
assert_eq!(res.stderr, "");
}
}