Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

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

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ ring = ["domain/ring"]
bytes = "1.8.0"
chrono = "0.4.38"
clap = { version = "4.3.4", features = ["cargo", "derive"] }
domain = { git = "https://github.com/NLnetLabs/domain.git", branch = "initial-nsec3-generation", features = [
domain = { git = "https://github.com/NLnetLabs/domain.git", branch = "extend-stelline-for-ldns-testns", features = [
"bytes",
"net",
"resolv",
"tsig",
"unstable-client-transport",
"unstable-sign",
"unstable-stelline",
"unstable-validate",
"unstable-validator",
"zonefile",
Expand All @@ -43,6 +44,6 @@ _unused_lazy_static = { package = "lazy_static", version = "1.0.2" }
test_bin = "0.4.0"
tempfile = "3.14.0"
regex = "1.11.1"
domain = { git = "https://github.com/NLnetLabs/domain.git", branch = "initial-nsec3-generation", features = [
domain = { git = "https://github.com/NLnetLabs/domain.git", branch = "extend-stelline-for-ldns-testns", features = [
"unstable-stelline",
] }
6 changes: 6 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod key2ds;
pub mod keygen;
pub mod notify;
pub mod nsec3hash;
pub mod testns;
pub mod update;

use clap::crate_version;
Expand Down Expand Up @@ -65,6 +66,10 @@ pub enum Command {
#[command(name = "key2ds")]
Key2ds(key2ds::Key2ds),

/// Simple fake nameserver tool
#[command(name = "testns")]
TestNs(self::testns::TestNs),

/// Send an UPDATE packet
#[command(name = "update")]
Update(self::update::Update),
Expand All @@ -87,6 +92,7 @@ impl Command {
Self::Nsec3Hash(nsec3hash) => nsec3hash.execute(env),
Self::Key2ds(key2ds) => key2ds.execute(env),
Self::Notify(notify) => notify.execute(env),
Self::TestNs(testns) => testns.execute(env),
Self::Update(update) => update.execute(env),
Self::Help(help) => help.execute(),
Self::Report(s) => {
Expand Down
243 changes: 243 additions & 0 deletions src/commands/testns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::Arc;

use crate::env::Env;
use crate::error::Error;
use crate::Args;

use domain::base::wire::Composer;
use domain::dep::octseq::{OctetsBuilder, Truncate};
use domain::net::server::buf::VecBufSource;
use domain::net::server::dgram::DgramServer;
use domain::net::server::message::Request;
use domain::net::server::service::{CallResult, ServiceError, ServiceResult};
use domain::net::server::stream::StreamServer;
use domain::net::server::util::service_fn;
use domain::stelline::client::CurrStepValue;
use domain::stelline::parse_stelline::{self, Stelline};
use domain::stelline::server::do_server;
use lexopt::Arg;
use tokio::net::{TcpListener, UdpSocket};
use tokio::sync::mpsc::Sender;

use super::{parse_os, Command, LdnsCommand};

#[derive(Clone, Debug, clap::Args, PartialEq, Eq)]
pub struct TestNs {
/// Listens on the specified port, default 53.
#[arg(short = 'p', value_name = "PORT")]
port: Option<u16>,

/// Verbose output.
#[arg(short = 'v')]
verbose: bool,

/// Datafile
#[arg()]
datafile: PathBuf,

/// Running in LDNS mode?
#[arg(skip)]
is_ldns: bool,
}

const LDNS_HELP: &str = "\
Usage: ldns-testns [options] <datafile>
-p listens on the specified port, default 53.
The program answers queries with canned replies from the datafile.\
";

impl LdnsCommand for TestNs {
const NAME: &'static str = "testns";
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 mut port = 53;
let mut verbose = false;
let mut datafile = None;

let mut parser = lexopt::Parser::from_args(args);

while let Some(arg) = parser.next()? {
match arg {
Arg::Short('p') => {
let val = parser.value()?;
port = parse_os("port (-p)", &val)?;
}
Arg::Short('v') => {
verbose = true;
}
Arg::Value(val) => {
if datafile.is_some() {
return Err("Only one datafile is allowed".into());
}
datafile = Some(val);
}
Arg::Short(x) => return Err(format!("Invalid short option: -{x}").into()),
Arg::Long(x) => {
return Err(format!("Long options are not supported, but `--{x}` given").into())
}
}
}

let Some(datafile) = datafile else {
return Err("No datafile given".into());
};

Ok(Args::from(Command::TestNs(Self {
port: Some(port),
verbose,
datafile: datafile.into(),
is_ldns: true,
})))
}
}

impl TestNs {
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> {
let port = self.port.unwrap();
let mut datafile = std::fs::read_to_string(&self.datafile)?;

if !datafile.contains("RANGE_BEGIN") {
datafile.insert_str(0, "RANGE_BEGIN 0 999\n");
datafile.push_str("RANGE_END\n");
}
if !datafile.contains("SCENARIO_BEGIN") {
datafile.insert_str(0, "SCENARIO_BEGIN Scenario to emulate\n");
datafile.push_str("SCENARIO_END\n");
}
if !datafile.contains("CONFIG_END") {
datafile.insert_str(0, "CONFIG_END\n");
}

let stelline = Arc::new(parse_stelline::parse_file(
datafile.as_bytes(),
self.datafile.to_str().unwrap(),
));

let (tx, mut rx) = tokio::sync::mpsc::channel(1);

let svc = service_fn(answer_from_datafile, (stelline, tx));

let sock = UdpSocket::bind(format!("127.0.0.1:{port}")).await.unwrap();
let listener = TcpListener::bind(format!("127.0.0.1:{port}"))
.await
.unwrap();

if self.is_ldns && self.verbose {
writeln!(env.stdout(), "Listening on port {port}");
}

let udp_srv = DgramServer::new(sock, VecBufSource, svc.clone());
tokio::spawn(async move { udp_srv.run().await });

let tcp_srv = StreamServer::new(listener, VecBufSource, svc);
tokio::spawn(async move { tcp_srv.run().await });

while let Some(msg) = rx.recv().await {
if self.is_ldns && self.verbose {
writeln!(env.stdout(), "{}", msg);
}
}

Ok(())
}
}

fn answer_from_datafile(
req: Request<Vec<u8>>,
(stelline, tx): (Arc<Stelline>, Sender<String>),
) -> ServiceResult<AtLeastTwoBytesVec> {
let step_value = CurrStepValue::new();
let tx = tx.clone();
let (res, msg) = match do_server(&req, &stelline, &step_value) {
Some((builder, (range_idx, entry_idx))) => {
let q = req.message().first_question();
let hdr = builder.header();
let hdr_opcode = hdr.opcode();
let hdr_rcode = hdr.rcode();
let hdr_flags = hdr.flags();
(Ok(CallResult::new(builder)),
format!(
"comparepkt: match! at range {range_idx} entry {entry_idx} for question {q:?} with reply header {hdr_opcode}/{hdr_rcode}/{hdr_flags}",
))
}
None => (
Err(ServiceError::Refused),
format!(
"comparepkt: no match to question {:?}",
req.message().first_question()
),
),
};
tokio::spawn(async move {
tx.send(msg).await.unwrap();
});
res
}

// Hacky work around for the fact that StreamTarget::default() calls
// Target::default() which in our case creates an empty Vec, which will then
// cause a panic when the Stelline code creates an empty target via the
// Default trait thereby invoking truncate() which attempts to truncate to len
// + 2, but truncation of len + 2 is a NO-OP and then the Vec doesn't have the
// expected two leading TCP stream length header bytes and an out of bounds
// access occurs.
#[derive(Clone)]
struct AtLeastTwoBytesVec(Vec<u8>);

impl Default for AtLeastTwoBytesVec {
fn default() -> Self {
Self(vec![0, 0])
}
}

impl std::ops::Deref for AtLeastTwoBytesVec {
type Target = Vec<u8>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl std::ops::DerefMut for AtLeastTwoBytesVec {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl Composer for AtLeastTwoBytesVec {}

impl Truncate for AtLeastTwoBytesVec {
fn truncate(&mut self, len: usize) {
self.0.truncate(len);
}
}

impl AsMut<[u8]> for AtLeastTwoBytesVec {
fn as_mut(&mut self) -> &mut [u8] {
self.0.as_mut()
}
}

impl AsRef<[u8]> for AtLeastTwoBytesVec {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}

impl OctetsBuilder for AtLeastTwoBytesVec {
type AppendError = <Vec<u8> as OctetsBuilder>::AppendError;

fn append_slice(&mut self, slice: &[u8]) -> Result<(), Self::AppendError> {
self.0.append_slice(slice)
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use env::Env;
use error::Error;

pub use self::args::Args;
use commands::testns::TestNs;

pub mod args;
pub mod commands;
Expand Down Expand Up @@ -39,6 +40,7 @@ pub fn try_ldns_compatibility<I: IntoIterator<Item = OsString>>(
"notify" => Notify::parse_ldns_args(args_iter),
"keygen" => Keygen::parse_ldns_args(args_iter),
"nsec3-hash" => Nsec3Hash::parse_ldns_args(args_iter),
"testns" => TestNs::parse_ldns_args(args_iter),
"update" => Update::parse_ldns_args(args_iter),
_ => return Err(format!("Unrecognized ldns command 'ldns-{binary_name}'").into()),
};
Expand Down