From 5874f11dcc4eb38dc47f27c13412d5638df12cd9 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 12 Nov 2024 16:34:08 +0100 Subject: [PATCH 1/8] Add `ldns` binary for easier testing of ldns tools (#20) --- Cargo.toml | 4 ++++ src/bin/ldns.rs | 23 +++++++++++++++++++++++ src/commands/mod.rs | 15 +++++++-------- src/commands/nsec3hash.rs | 5 +++-- src/lib.rs | 24 ++++++++++++++++++++++++ src/main.rs | 24 ++---------------------- 6 files changed, 63 insertions(+), 32 deletions(-) create mode 100644 src/bin/ldns.rs diff --git a/Cargo.toml b/Cargo.toml index 6bbb7d0..6f6f54c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,10 @@ name = "dnst" version = "0.1.0" edition = "2021" +[[bin]] +name = "ldns" +path = "src/bin/ldns.rs" + [dependencies] clap = { version = "4", features = ["derive"] } domain = "0.10.1" diff --git a/src/bin/ldns.rs b/src/bin/ldns.rs new file mode 100644 index 0000000..6a8a5cc --- /dev/null +++ b/src/bin/ldns.rs @@ -0,0 +1,23 @@ +//! This binary is intended for testing the `ldns-*` commands +//! +//! The `ldns` command is passed as the first argument, so that it can be +//! executed without symlinking. This binary should not be included in any +//! packaged version of `dnst` as it is meant for internal testing only. + +use std::process::ExitCode; + +use dnst::try_ldns_compatibility; + +fn main() -> ExitCode { + let mut args = std::env::args_os(); + args.next().unwrap(); + let args = try_ldns_compatibility(args).expect("ldns commmand is not recognized"); + + match args.execute() { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + err.pretty_print(); + ExitCode::FAILURE + } + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 5b58650..c602fe3 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,7 +3,7 @@ pub mod help; pub mod nsec3hash; -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::str::FromStr; use nsec3hash::Nsec3Hash; @@ -36,17 +36,16 @@ impl Command { /// These commands do their own argument parsing, because clap cannot always /// (easily) parse arguments in the same way that the ldns tools do. /// -/// The `LdnsCommand::parse_ldns` function should parse arguments obtained -/// with [`std::env::args`] or [`std::env::args_os`] and return an error in -/// case of invalid arguments. The help string provided as -/// [`LdnsCommand::HELP`] is automatically appended to returned errors. +/// The [`LdnsCommand::parse_ldns`] function should parse arguments and +/// return an error in case of a parsing failure. The help string provided +/// as [`LdnsCommand::HELP`] is automatically appended to returned errors. pub trait LdnsCommand: Into { const HELP: &'static str; - fn parse_ldns() -> Result; + fn parse_ldns>(args: I) -> Result; - fn parse_ldns_args() -> Result { - match Self::parse_ldns() { + fn parse_ldns_args>(args: I) -> Result { + match Self::parse_ldns(args) { Ok(c) => Ok(Args::from(c.into())), Err(e) => Err(format!("Error: {e}\n\n{}", Self::HELP).into()), } diff --git a/src/commands/nsec3hash.rs b/src/commands/nsec3hash.rs index 0a75621..b673d50 100644 --- a/src/commands/nsec3hash.rs +++ b/src/commands/nsec3hash.rs @@ -8,6 +8,7 @@ use lexopt::Arg; // use domain::validator::nsec::nsec3_hash; use octseq::OctetsBuilder; use ring::digest; +use std::ffi::OsString; use std::str::FromStr; use super::{parse_os, parse_os_with, LdnsCommand}; @@ -55,13 +56,13 @@ ldns-nsec3-hash [OPTIONS] impl LdnsCommand for Nsec3Hash { const HELP: &'static str = LDNS_HELP; - fn parse_ldns() -> Result { + fn parse_ldns>(args: I) -> Result { let mut algorithm = Nsec3HashAlg::SHA1; let mut iterations = 1; let mut salt = Nsec3Salt::empty(); let mut name = None; - let mut parser = lexopt::Parser::from_env(); + let mut parser = lexopt::Parser::from_args(args); while let Some(arg) = parser.next()? { match arg { diff --git a/src/lib.rs b/src/lib.rs index de6645c..a329971 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,29 @@ +use std::{ffi::OsString, path::Path}; + +use commands::{nsec3hash::Nsec3Hash, LdnsCommand}; + pub use self::args::Args; pub mod args; pub mod commands; pub mod error; + +pub fn try_ldns_compatibility>(args: I) -> Option { + let mut args_iter = args.into_iter(); + let binary_path = args_iter.next()?; + + let binary_name = Path::new(&binary_path).file_name()?.to_str()?; + + let res = match binary_name { + "ldns-nsec3-hash" => Nsec3Hash::parse_ldns_args(args_iter), + _ => return None, + }; + + match res { + Ok(args) => Some(args), + Err(err) => { + err.pretty_print(); + std::process::exit(1) + } + } +} diff --git a/src/main.rs b/src/main.rs index 34d4d50..4a14114 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,12 @@ -use std::path::Path; use std::process::ExitCode; use clap::Parser; -use dnst::commands::{nsec3hash::Nsec3Hash, LdnsCommand}; fn main() -> ExitCode { // If none of the ldns-* tools matched, then we continue with clap // argument parsing. - let args = try_ldns_compatibility().unwrap_or_else(dnst::Args::parse); + let env_args = std::env::args_os(); + let args = dnst::try_ldns_compatibility(env_args).unwrap_or_else(dnst::Args::parse); match args.execute() { Ok(()) => ExitCode::SUCCESS, @@ -17,22 +16,3 @@ fn main() -> ExitCode { } } } - -fn try_ldns_compatibility() -> Option { - let binary_path = std::env::args_os().next()?; - - let binary_name = Path::new(&binary_path).file_name()?.to_str()?; - - let res = match binary_name { - "ldns-nsec3-hash" => Nsec3Hash::parse_ldns_args(), - _ => return None, - }; - - match res { - Ok(args) => Some(args), - Err(err) => { - err.pretty_print(); - std::process::exit(1) - } - } -} From 20027e22b4d5a83f1001b7ba174c016c14740292 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 12 Nov 2024 17:26:19 +0100 Subject: [PATCH 2/8] fix not having a default-run target --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 6f6f54c..58116f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "dnst" version = "0.1.0" edition = "2021" +default-run = "dnst" [[bin]] name = "ldns" From 41157b30577bc710fa53f3a41fa2c7b1302a9b45 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 13 Nov 2024 21:43:58 +0100 Subject: [PATCH 3/8] Skeleton pkg workflow. To cause GH to enable the pkg workflow so that it can be further developed and tested in PR #22. --- .github/workflows/pkg-rust.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/pkg-rust.yml diff --git a/.github/workflows/pkg-rust.yml b/.github/workflows/pkg-rust.yml new file mode 100644 index 0000000..c4b1489 --- /dev/null +++ b/.github/workflows/pkg-rust.yml @@ -0,0 +1,23 @@ +name: Packaging + +on: + push: + branches: + - "main" + tags: + - v* + + # Triggering on PRs and arbitrary branch pushes is not enabled because most of the time only the CI build should be + # triggered, not the packaging build. In cases where you want to test changes to this workflow this trigger enables + # you to manually invoke this workflow on an arbitrary branch as needed. + workflow_dispatch: + +jobs: + package: + # + # Set @vN to the latest released version. + # For more information see: https://github.com/NLnetLabs/ploutos/blob/main/README.md + # + uses: NLnetLabs/ploutos/.github/workflows/pkg-rust.yml@v7 + + # TODO From 459a758d18a2dde28a5d5151c69437476c0e56b3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 13 Nov 2024 21:44:59 +0100 Subject: [PATCH 4/8] Rename pkg-rust.yml to pkg.yml --- .github/workflows/{pkg-rust.yml => pkg.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{pkg-rust.yml => pkg.yml} (100%) diff --git a/.github/workflows/pkg-rust.yml b/.github/workflows/pkg.yml similarity index 100% rename from .github/workflows/pkg-rust.yml rename to .github/workflows/pkg.yml From 18d56a643105fbaff0dde4897776d0dde7ef245c Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 15 Nov 2024 13:47:18 +0100 Subject: [PATCH 5/8] Introduce an Env trait to mock the outside world (#23) * introduce and Env trait to mock the outside world * make argument parsing unit-testable * add get_stderr to FakeEnv * fakecmd and print errors into the env * change ExitCode::from to into Co-authored-by: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> * update some comments * cargo fmt * document some more of the testing utilities * use exit_code method on clap errors * bump minimal clap version * reexport RealEnv * remove .lock() calls for stdout and stderr * remove args field from FakeCmd * improve docs for Env * cargo fmt * use mutex in FakeStream * improve FmtWriter implementation for a tiny bit more performance * car go ffffffmmmmmmtttttt --------- Co-authored-by: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> --- Cargo.toml | 2 +- src/args.rs | 6 +- src/bin/ldns.rs | 9 ++- src/commands/mod.rs | 5 +- src/commands/nsec3hash.rs | 41 +++++++++++- src/env/fake.rs | 134 ++++++++++++++++++++++++++++++++++++++ src/env/mod.rs | 57 ++++++++++++++++ src/env/real.rs | 34 ++++++++++ src/error.rs | 70 +++++++++++++++++--- src/lib.rs | 40 +++++++++--- src/main.rs | 16 +---- 11 files changed, 373 insertions(+), 41 deletions(-) create mode 100644 src/env/fake.rs create mode 100644 src/env/mod.rs create mode 100644 src/env/real.rs diff --git a/Cargo.toml b/Cargo.toml index 58116f9..6ba3667 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ name = "ldns" path = "src/bin/ldns.rs" [dependencies] -clap = { version = "4", features = ["derive"] } +clap = { version = "4.3.4", features = ["derive"] } domain = "0.10.1" lexopt = "0.3.0" diff --git a/src/args.rs b/src/args.rs index 0659044..3883f51 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,3 +1,5 @@ +use crate::env::Env; + use super::commands::Command; use super::error::Error; @@ -9,8 +11,8 @@ pub struct Args { } impl Args { - pub fn execute(self) -> Result<(), Error> { - self.command.execute() + pub fn execute(self, env: impl Env) -> Result<(), Error> { + self.command.execute(env) } } diff --git a/src/bin/ldns.rs b/src/bin/ldns.rs index 6a8a5cc..beda509 100644 --- a/src/bin/ldns.rs +++ b/src/bin/ldns.rs @@ -9,14 +9,17 @@ use std::process::ExitCode; use dnst::try_ldns_compatibility; fn main() -> ExitCode { + let env = dnst::env::RealEnv; + let mut args = std::env::args_os(); args.next().unwrap(); - let args = try_ldns_compatibility(args).expect("ldns commmand is not recognized"); + let args = + try_ldns_compatibility(args).map(|args| args.expect("ldns commmand is not recognized")); - match args.execute() { + match args.and_then(|args| args.execute(&env)) { Ok(()) => ExitCode::SUCCESS, Err(err) => { - err.pretty_print(); + err.pretty_print(env); ExitCode::FAILURE } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index c602fe3..b7dbb3d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -8,6 +8,7 @@ use std::str::FromStr; use nsec3hash::Nsec3Hash; +use crate::env::Env; use crate::Args; use super::error::Error; @@ -23,9 +24,9 @@ pub enum Command { } impl Command { - pub fn execute(self) -> Result<(), Error> { + pub fn execute(self, env: impl Env) -> Result<(), Error> { match self { - Self::Nsec3Hash(nsec3hash) => nsec3hash.execute(), + Self::Nsec3Hash(nsec3hash) => nsec3hash.execute(env), Self::Help(help) => help.execute(), } } diff --git a/src/commands/nsec3hash.rs b/src/commands/nsec3hash.rs index b673d50..1b175fe 100644 --- a/src/commands/nsec3hash.rs +++ b/src/commands/nsec3hash.rs @@ -1,3 +1,4 @@ +use crate::env::Env; use crate::error::Error; use clap::builder::ValueParser; use domain::base::iana::nsec3::Nsec3HashAlg; @@ -9,6 +10,7 @@ use lexopt::Arg; use octseq::OctetsBuilder; use ring::digest; use std::ffi::OsString; +use std::fmt::Write; use std::str::FromStr; use super::{parse_os, parse_os_with, LdnsCommand}; @@ -128,11 +130,13 @@ impl Nsec3Hash { } impl Nsec3Hash { - pub fn execute(self) -> Result<(), Error> { + pub fn execute(self, env: impl Env) -> Result<(), Error> { let hash = nsec3_hash(&self.name, self.algorithm, self.iterations, &self.salt) .to_string() .to_lowercase(); - println!("{}.", hash); + + let mut out = env.stdout(); + writeln!(out, "{}.", hash).unwrap(); Ok(()) } } @@ -179,3 +183,36 @@ where // For normal hash algorithms this should not fail. OwnerHash::from_octets(h.as_ref().to_vec()).expect("should not fail") } + +#[cfg(test)] +mod test { + use crate::env::fake::FakeCmd; + + #[test] + fn dnst_parse() { + let cmd = FakeCmd::new(["dnst", "nsec3-hash"]); + + assert!(cmd.parse().is_err()); + assert!(cmd.args(["-a"]).parse().is_err()); + } + + #[test] + fn dnst_run() { + let cmd = FakeCmd::new(["dnst", "nsec3-hash"]); + + let res = cmd.run(); + assert_eq!(res.exit_code, 2); + + let res = cmd.args(["example.test"]).run(); + assert_eq!(res.exit_code, 0); + assert_eq!(res.stdout, "o09614ibh1cq1rcc86289olr22ea0fso.\n") + } + + #[test] + fn ldns_parse() { + let cmd = FakeCmd::new(["ldns-nsec3-hash"]); + + assert!(cmd.parse().is_err()); + assert!(cmd.args(["-a"]).parse().is_err()); + } +} diff --git a/src/env/fake.rs b/src/env/fake.rs new file mode 100644 index 0000000..f02c66a --- /dev/null +++ b/src/env/fake.rs @@ -0,0 +1,134 @@ +use std::ffi::OsString; +use std::fmt; +use std::sync::Arc; +use std::sync::Mutex; + +use crate::{error::Error, parse_args, run, Args}; + +use super::Env; + +/// A command to run in a [`FakeEnv`] +/// +/// This is used for testing the utilities, running the real code in a fake +/// environment. +#[derive(Clone)] +pub struct FakeCmd { + /// The command to run, including `argv[0]` + cmd: Vec, +} + +/// The result of running a [`FakeCmd`] +/// +/// The fields are public to allow for easy assertions in tests. +pub struct FakeResult { + pub exit_code: u8, + pub stdout: String, + pub stderr: String, +} + +/// An environment that mocks interaction with the outside world +pub struct FakeEnv { + /// Description of the command being run + pub cmd: FakeCmd, + + /// The mocked stdout + pub stdout: FakeStream, + + /// The mocked stderr + pub stderr: FakeStream, + // pub stelline: Option, + // pub curr_step_value: Option>, +} + +impl Env for FakeEnv { + fn args_os(&self) -> impl Iterator { + self.cmd.cmd.iter().map(Into::into) + } + + fn stdout(&self) -> impl fmt::Write { + self.stdout.clone() + } + + fn stderr(&self) -> impl fmt::Write { + self.stderr.clone() + } +} + +impl FakeCmd { + /// Construct a new [`FakeCmd`] with a given command. + /// + /// The command can consist of multiple strings to specify a subcommand. + pub fn new>(cmd: impl IntoIterator) -> Self { + Self { + cmd: cmd.into_iter().map(Into::into).collect(), + } + } + + /// Add arguments to a clone of the [`FakeCmd`] + /// + /// ```rust,ignore + /// let cmd = FakeCmd::new(["dnst"]) + /// let sub1 = cmd.args(["sub1"]); // dnst sub1 + /// let sub2 = cmd.args(["sub2"]); // dnst sub2 + /// let sub3 = sub2.args(["sub3"]); // dnst sub2 sub3 + /// ``` + pub fn args>(&self, args: impl IntoIterator) -> Self { + let mut new = self.clone(); + new.cmd.extend(args.into_iter().map(Into::into)); + new + } + + /// Parse the arguments of this [`FakeCmd`] and return the result + pub fn parse(&self) -> Result { + let env = FakeEnv { + cmd: self.clone(), + stdout: Default::default(), + stderr: Default::default(), + }; + parse_args(env) + } + + /// Run the [`FakeCmd`] in a [`FakeEnv`], returning a [`FakeResult`] + pub fn run(&self) -> FakeResult { + let env = FakeEnv { + cmd: self.clone(), + stdout: Default::default(), + stderr: Default::default(), + }; + + let exit_code = run(&env); + + FakeResult { + exit_code, + stdout: env.get_stdout(), + stderr: env.get_stderr(), + } + } +} + +impl FakeEnv { + pub fn get_stdout(&self) -> String { + self.stdout.0.lock().unwrap().clone() + } + + pub fn get_stderr(&self) -> String { + self.stderr.0.lock().unwrap().clone() + } +} + +/// A type to used to mock stdout and stderr +#[derive(Clone, Default)] +pub struct FakeStream(Arc>); + +impl fmt::Write for FakeStream { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.0.lock().unwrap().push_str(s); + Ok(()) + } +} + +impl fmt::Display for FakeStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.0.lock().unwrap().as_ref()) + } +} diff --git a/src/env/mod.rs b/src/env/mod.rs new file mode 100644 index 0000000..717554e --- /dev/null +++ b/src/env/mod.rs @@ -0,0 +1,57 @@ +use std::ffi::OsString; +use std::fmt; + +mod real; + +#[cfg(test)] +pub mod fake; + +pub use real::RealEnv; + +pub trait Env { + // /// Make a network connection + // fn make_connection(&self); + + // /// Make a new [`StubResolver`] + // fn make_stub_resolver(&self); + + /// Get an iterator over the command line arguments passed to the program + /// + /// Equivalent to [`std::env::args_os`] + fn args_os(&self) -> impl Iterator; + + /// Get a reference to stdout + /// + /// Equivalent to [`std::io::stdout`] + fn stdout(&self) -> impl fmt::Write; + + /// Get a reference to stderr + /// + /// Equivalent to [`std::io::stderr`] + fn stderr(&self) -> impl fmt::Write; + + // /// Get a reference to stdin + // fn stdin(&self) -> impl io::Read; +} + +impl Env for &E { + // fn make_connection(&self) { + // todo!() + // } + + // fn make_stub_resolver(&self) { + // todo!() + // } + + fn args_os(&self) -> impl Iterator { + (**self).args_os() + } + + fn stdout(&self) -> impl fmt::Write { + (**self).stdout() + } + + fn stderr(&self) -> impl fmt::Write { + (**self).stderr() + } +} diff --git a/src/env/real.rs b/src/env/real.rs new file mode 100644 index 0000000..69fb5e0 --- /dev/null +++ b/src/env/real.rs @@ -0,0 +1,34 @@ +use std::ffi::OsString; +use std::fmt; +use std::io; + +use super::Env; + +/// Use real I/O +pub struct RealEnv; + +impl Env for RealEnv { + fn args_os(&self) -> impl Iterator { + std::env::args_os() + } + + fn stdout(&self) -> impl fmt::Write { + FmtWriter(io::stdout()) + } + + fn stderr(&self) -> impl fmt::Write { + FmtWriter(io::stderr()) + } +} + +struct FmtWriter(T); + +impl fmt::Write for FmtWriter { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.0.write_all(s.as_bytes()).map_err(|_| fmt::Error) + } + + fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { + self.0.write_fmt(args).map_err(|_| fmt::Error) + } +} diff --git a/src/error.rs b/src/error.rs index e2c2470..e800a59 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,18 +1,18 @@ -use std::{error, fmt, io}; +use crate::env::Env; +use std::fmt::{self, Write}; +use std::{error, io}; //------------ Error --------------------------------------------------------- /// A program error. /// /// Such errors are highly likely to halt the program. -#[derive(Clone)] pub struct Error(Box); /// Information about an error. -#[derive(Clone)] struct Information { /// The primary error message. - primary: Box, + primary: PrimaryError, /// Layers of context to the error. /// @@ -20,13 +20,28 @@ struct Information { context: Vec>, } +#[derive(Debug)] +enum PrimaryError { + Clap(clap::Error), + Other(Box), +} + +impl fmt::Display for PrimaryError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PrimaryError::Clap(e) => e.fmt(f), + PrimaryError::Other(e) => e.fmt(f), + } + } +} + //--- Interaction impl Error { /// Construct a new error from a string. pub fn new(error: &str) -> Self { Self(Box::new(Information { - primary: error.into(), + primary: PrimaryError::Other(error.into()), context: Vec::new(), })) } @@ -38,8 +53,21 @@ impl Error { } /// Pretty-print this error. - pub fn pretty_print(self) { + pub fn pretty_print(&self, env: impl Env) { use std::io::IsTerminal; + let mut err = env.stderr(); + + let error = match &self.0.primary { + // Clap errors are already styled. We don't want our own pretty + // styling around that and context does not make sense for command + // line arguments either. So we just print the styled string that + // clap produces and return. + PrimaryError::Clap(e) => { + let _ = writeln!(err, "{}", e.render().ansi()); + return; + } + PrimaryError::Other(error) => error, + }; // NOTE: This is a multicall binary, so argv[0] is necessary for // program operation. We would fail very early if it didn't exist. @@ -52,9 +80,24 @@ impl Error { "ERROR:" }; - eprint!("[{prog}] {error_marker} {}", self.0.primary); + let _ = write!(err, "[{prog}] {error_marker} {error}"); for context in &self.0.context { - eprint!("\n... while {context}"); + let _ = writeln!(err, "\n... while {context}"); + } + } + + pub fn exit_code(&self) -> u8 { + // Clap uses the exit code 2 and we want to keep that, but we aren't + // actually returning the clap error, so we replicate that behaviour + // here. + // + // Argument parsing errors from the ldns-xxx commands will not be clap + // errors and therefore be printed with an exit code of 1. This is + // expected because ldns also exits with 1. + if let PrimaryError::Clap(e) = &self.0.primary { + e.exit_code() as u8 + } else { + 1 } } } @@ -85,11 +128,20 @@ impl From for Error { } } +impl From for Error { + fn from(value: clap::Error) -> Self { + Self(Box::new(Information { + primary: PrimaryError::Clap(value), + context: Vec::new(), + })) + } +} + //--- Display, Debug impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&self.0.primary) + self.0.primary.fmt(f) } } diff --git a/src/lib.rs b/src/lib.rs index a329971..8a14999 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,29 +1,53 @@ -use std::{ffi::OsString, path::Path}; +use std::ffi::OsString; +use std::path::Path; +use clap::Parser; use commands::{nsec3hash::Nsec3Hash, LdnsCommand}; +use env::Env; +use error::Error; pub use self::args::Args; pub mod args; pub mod commands; +pub mod env; pub mod error; -pub fn try_ldns_compatibility>(args: I) -> Option { +pub fn try_ldns_compatibility>( + args: I, +) -> Result, Error> { let mut args_iter = args.into_iter(); - let binary_path = args_iter.next()?; + let binary_path = args_iter.next().ok_or("Missing binary name")?; - let binary_name = Path::new(&binary_path).file_name()?.to_str()?; + let binary_name = Path::new(&binary_path) + .file_name() + .ok_or("Missing binary file name")? + .to_str() + .ok_or("Binary file name is not valid unicode")?; let res = match binary_name { "ldns-nsec3-hash" => Nsec3Hash::parse_ldns_args(args_iter), - _ => return None, + _ => return Ok(None), }; + res.map(Some) +} + +fn parse_args(env: impl Env) -> Result { + if let Some(args) = try_ldns_compatibility(env.args_os())? { + return Ok(args); + } + let args = Args::try_parse_from(env.args_os())?; + Ok(args) +} + +pub fn run(env: impl Env) -> u8 { + let res = parse_args(&env).and_then(|args| args.execute(&env)); match res { - Ok(args) => Some(args), + Ok(()) => 0, Err(err) => { - err.pretty_print(); - std::process::exit(1) + err.pretty_print(&env); + err.exit_code() } } } diff --git a/src/main.rs b/src/main.rs index 4a14114..b368f32 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,6 @@ use std::process::ExitCode; -use clap::Parser; - fn main() -> ExitCode { - // If none of the ldns-* tools matched, then we continue with clap - // argument parsing. - let env_args = std::env::args_os(); - let args = dnst::try_ldns_compatibility(env_args).unwrap_or_else(dnst::Args::parse); - - match args.execute() { - Ok(()) => ExitCode::SUCCESS, - Err(err) => { - err.pretty_print(); - ExitCode::FAILURE - } - } + let env = dnst::env::RealEnv; + dnst::run(env).into() } From a014f13c09996116360be16224f568372cc33289 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 15 Nov 2024 16:13:35 +0100 Subject: [PATCH 6/8] Make the output of `Env::std{out, err}` a concrete type (#27) * make the output of Env::std{out, err} a concrete type * document why the unwrap in write_fmt is okay --- src/commands/nsec3hash.rs | 3 +-- src/env/fake.rs | 9 +++++---- src/env/mod.rs | 27 +++++++++++++++++++++++---- src/env/real.rs | 9 +++++---- src/error.rs | 8 ++++---- 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/commands/nsec3hash.rs b/src/commands/nsec3hash.rs index 1b175fe..50917cc 100644 --- a/src/commands/nsec3hash.rs +++ b/src/commands/nsec3hash.rs @@ -10,7 +10,6 @@ use lexopt::Arg; use octseq::OctetsBuilder; use ring::digest; use std::ffi::OsString; -use std::fmt::Write; use std::str::FromStr; use super::{parse_os, parse_os_with, LdnsCommand}; @@ -136,7 +135,7 @@ impl Nsec3Hash { .to_lowercase(); let mut out = env.stdout(); - writeln!(out, "{}.", hash).unwrap(); + writeln!(out, "{}.", hash); Ok(()) } } diff --git a/src/env/fake.rs b/src/env/fake.rs index f02c66a..f9d4cde 100644 --- a/src/env/fake.rs +++ b/src/env/fake.rs @@ -6,6 +6,7 @@ use std::sync::Mutex; use crate::{error::Error, parse_args, run, Args}; use super::Env; +use super::Stream; /// A command to run in a [`FakeEnv`] /// @@ -45,12 +46,12 @@ impl Env for FakeEnv { self.cmd.cmd.iter().map(Into::into) } - fn stdout(&self) -> impl fmt::Write { - self.stdout.clone() + fn stdout(&self) -> Stream { + Stream(self.stdout.clone()) } - fn stderr(&self) -> impl fmt::Write { - self.stderr.clone() + fn stderr(&self) -> Stream { + Stream(self.stderr.clone()) } } diff --git a/src/env/mod.rs b/src/env/mod.rs index 717554e..b00b57d 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -23,17 +23,36 @@ pub trait Env { /// Get a reference to stdout /// /// Equivalent to [`std::io::stdout`] - fn stdout(&self) -> impl fmt::Write; + fn stdout(&self) -> Stream; /// Get a reference to stderr /// /// Equivalent to [`std::io::stderr`] - fn stderr(&self) -> impl fmt::Write; + fn stderr(&self) -> Stream; // /// Get a reference to stdin // fn stdin(&self) -> impl io::Read; } +/// A type with an infallible `write_fmt` method for use with [`write!`] macros +/// +/// This ensures that we don't have to `use` either [`std::fmt::Write`] or +/// [`std::io::Write`]. Additionally, this `write_fmt` does not return a +/// result. This means that we can use the [`write!`] and [`writeln`] macros +/// without handling errors. +pub struct Stream(T); + +impl Stream { + pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) { + // This unwrap is not _really_ safe, but we are using this as stdout. + // The `println` macro also ignores errors and `push_str` of the + // fake stream also does not return an error. If this fails, it means + // we can't write to stdout anymore so a graceful exit will be very + // hard anyway. + self.0.write_fmt(args).unwrap(); + } +} + impl Env for &E { // fn make_connection(&self) { // todo!() @@ -47,11 +66,11 @@ impl Env for &E { (**self).args_os() } - fn stdout(&self) -> impl fmt::Write { + fn stdout(&self) -> Stream { (**self).stdout() } - fn stderr(&self) -> impl fmt::Write { + fn stderr(&self) -> Stream { (**self).stderr() } } diff --git a/src/env/real.rs b/src/env/real.rs index 69fb5e0..c854db4 100644 --- a/src/env/real.rs +++ b/src/env/real.rs @@ -3,6 +3,7 @@ use std::fmt; use std::io; use super::Env; +use super::Stream; /// Use real I/O pub struct RealEnv; @@ -12,12 +13,12 @@ impl Env for RealEnv { std::env::args_os() } - fn stdout(&self) -> impl fmt::Write { - FmtWriter(io::stdout()) + fn stdout(&self) -> Stream { + Stream(FmtWriter(io::stdout())) } - fn stderr(&self) -> impl fmt::Write { - FmtWriter(io::stderr()) + fn stderr(&self) -> Stream { + Stream(FmtWriter(io::stderr())) } } diff --git a/src/error.rs b/src/error.rs index e800a59..fe9d286 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,5 @@ use crate::env::Env; -use std::fmt::{self, Write}; +use std::fmt; use std::{error, io}; //------------ Error --------------------------------------------------------- @@ -63,7 +63,7 @@ impl Error { // line arguments either. So we just print the styled string that // clap produces and return. PrimaryError::Clap(e) => { - let _ = writeln!(err, "{}", e.render().ansi()); + writeln!(err, "{}", e.render().ansi()); return; } PrimaryError::Other(error) => error, @@ -80,9 +80,9 @@ impl Error { "ERROR:" }; - let _ = write!(err, "[{prog}] {error_marker} {error}"); + write!(err, "[{prog}] {error_marker} {error}"); for context in &self.0.context { - let _ = writeln!(err, "\n... while {context}"); + writeln!(err, "\n... while {context}"); } } From 511c8f1f6afcadf43a53ddac7bf9bee11cf1d24d Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Tue, 19 Nov 2024 13:45:18 +0100 Subject: [PATCH 7/8] Implement Key2ds (#2) --- Cargo.lock | 97 +++++++- Cargo.toml | 11 +- src/args.rs | 2 +- src/commands/key2ds.rs | 498 +++++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 17 ++ src/env/fake.rs | 19 ++ src/env/mod.rs | 8 + src/env/real.rs | 5 + src/lib.rs | 3 +- 9 files changed, 647 insertions(+), 13 deletions(-) create mode 100644 src/commands/key2ds.rs diff --git a/Cargo.lock b/Cargo.lock index 979564c..dd9c461 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" [[package]] name = "anstream" @@ -51,6 +57,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "byteorder" version = "1.5.0" @@ -65,9 +77,9 @@ checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" -version = "1.1.36" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8" dependencies = [ "shlex", ] @@ -142,20 +154,39 @@ dependencies = [ "lexopt", "octseq", "ring", + "tempfile", ] [[package]] name = "domain" version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64008666d9f3b6a88a63cd28ad8f3a5a859b8037e11bfb680c1b24945ea1c28d" +source = "git+https://github.com/NLnetLabs/domain.git#39a04b6f6af9496a2ec91b6e5707ecf255fe0c38" dependencies = [ "bytes", + "hashbrown", "octseq", "rand", + "ring", + "serde", "time", ] +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + [[package]] name = "getrandom" version = "0.2.15" @@ -167,6 +198,15 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "allocator-api2", +] + [[package]] name = "heck" version = "0.5.0" @@ -191,6 +231,12 @@ version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "num-conv" version = "0.1.0" @@ -204,8 +250,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "126c3ca37c9c44cec575247f43a3e4374d8927684f129d2beeb0d2cef262fe12" dependencies = [ "bytes", + "serde", ] +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "powerfmt" version = "0.2.0" @@ -284,20 +337,33 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustix" +version = "0.38.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -333,6 +399,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "time" version = "0.3.36" diff --git a/Cargo.toml b/Cargo.toml index 6ba3667..8a3a3e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,16 @@ path = "src/bin/ldns.rs" [dependencies] clap = { version = "4.3.4", features = ["derive"] } -domain = "0.10.1" +domain = { version = "0.10.3", git = "https://github.com/NLnetLabs/domain.git", features = [ + "zonefile", + "bytes", + "unstable-validate", +] } lexopt = "0.3.0" # for implementation of nsec3 hash until domain has it stabilized -octseq = { version = "0.5.1", features = ["std"] } +octseq = { version = "0.5.2", features = ["std"] } ring = { version = "0.17" } + +[dev-dependencies] +tempfile = "3.14.0" diff --git a/src/args.rs b/src/args.rs index 3883f51..7c868b5 100644 --- a/src/args.rs +++ b/src/args.rs @@ -7,7 +7,7 @@ use super::error::Error; #[command(version, disable_help_subcommand = true)] pub struct Args { #[command(subcommand)] - command: Command, + pub command: Command, } impl Args { diff --git a/src/commands/key2ds.rs b/src/commands/key2ds.rs new file mode 100644 index 0000000..b10f7bc --- /dev/null +++ b/src/commands/key2ds.rs @@ -0,0 +1,498 @@ +use std::ffi::OsString; +use std::fs::File; +use std::io::{self, Write as _}; +use std::path::PathBuf; + +use clap::builder::ValueParser; +use clap::Parser; +use domain::base::iana::{DigestAlg, SecAlg}; +use domain::base::zonefile_fmt::ZonefileFmt; +use domain::base::Record; +use domain::rdata::Ds; +use domain::validate::DnskeyExt; +use domain::zonefile::inplace::{Entry, ScannedRecordData}; +use lexopt::Arg; + +use crate::env::Env; +use crate::error::Error; + +use super::LdnsCommand; + +#[derive(Clone, Debug, Parser, PartialEq, Eq)] +#[command(version)] +pub struct Key2ds { + /// ignore SEP flag (i.e. make DS records for any key) + #[arg(long = "ignore-sep")] + ignore_sep: bool, + + /// do not write DS records to file(s) but to stdout + #[arg(short = 'n')] + write_to_stdout: bool, + + /// Overwrite existing DS files + #[arg(short = 'f', long = "force")] + force_overwrite: bool, + + /// algorithm to use for digest + #[arg( + short = 'a', + long = "algorithm", + value_parser = ValueParser::new(parse_digest_alg) + )] + algorithm: Option, + + /// Keyfile to read + #[arg()] + keyfile: PathBuf, +} + +pub fn parse_digest_alg(arg: &str) -> Result { + if let Ok(num) = arg.parse() { + let alg = DigestAlg::from_int(num); + if alg.to_mnemonic().is_some() { + Ok(alg) + } else { + Err(Error::from("unknown algorithm number")) + } + } else { + DigestAlg::from_mnemonic(arg.as_bytes()).ok_or(Error::from("unknown algorithm mnemonic")) + } +} + +const LDNS_HELP: &str = "\ +ldns-key2ds [-fn] [-1|-2|-4] keyfile + Generate a DS RR from the DNSKEYS in keyfile + The following file will be created for each key: + `K++.ds`. The base name `K++` + will be printed to stdout. + +Options: + -f: ignore SEP flag (i.e. make DS records for any key) + -n: do not write DS records to file(s) but to stdout + (default) use similar hash to the key algorithm + -1: use SHA1 for the DS hash + -2: use SHA256 for the DS hash + -4: use SHA384 for the DS hash\ +"; + +impl LdnsCommand for Key2ds { + const HELP: &'static str = LDNS_HELP; + + fn parse_ldns>(args: I) -> Result { + let mut ignore_sep = false; + let mut write_to_stdout = false; + let mut algorithm = None; + let mut keyfile = None; + + let mut parser = lexopt::Parser::from_args(args); + + while let Some(arg) = parser.next()? { + match arg { + Arg::Short('1') => algorithm = Some(DigestAlg::SHA1), + Arg::Short('2') => algorithm = Some(DigestAlg::SHA256), + Arg::Short('4') => algorithm = Some(DigestAlg::SHA384), + Arg::Short('f') => ignore_sep = true, + Arg::Short('n') => write_to_stdout = true, + Arg::Value(val) => { + if keyfile.is_some() { + return Err("Only one keyfile is allowed".into()); + } + keyfile = 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(keyfile) = keyfile else { + return Err("No keyfile given".into()); + }; + + Ok(Self { + ignore_sep, + write_to_stdout, + algorithm, + // Preventing overwriting files is a dnst feature that is not + // present in the ldns version of this command. + force_overwrite: true, + keyfile: keyfile.into(), + }) + } +} + +impl Key2ds { + pub fn execute(self, env: impl Env) -> Result<(), Error> { + let mut file = File::open(env.in_cwd(&self.keyfile)).map_err(|e| { + format!( + "Failed to open public key file \"{}\": {e}", + self.keyfile.display() + ) + })?; + let zonefile = domain::zonefile::inplace::Zonefile::load(&mut file).unwrap(); + for entry in zonefile { + let entry = entry.map_err(|e| { + format!( + "Error while reading public key from file \"{}\": {e}", + self.keyfile.display() + ) + })?; + + // We only care about records in a zonefile + let Entry::Record(record) = entry else { + continue; + }; + + let class = record.class(); + let ttl = record.ttl(); + let owner = record.owner(); + + // Of the records that we see, we only care about DNSKEY records + let ScannedRecordData::Dnskey(dnskey) = record.data() else { + continue; + }; + + // if ignore_sep is specified, we accept any key + // otherwise, we only want SEP keys + if !self.ignore_sep && !dnskey.is_secure_entry_point() { + continue; + } + + let key_tag = dnskey.key_tag(); + let sec_alg = dnskey.algorithm(); + let digest_alg = self + .algorithm + .unwrap_or_else(|| determine_hash_from_sec_alg(sec_alg)); + + if digest_alg == DigestAlg::GOST { + return Err("Error: the GOST algorithm is deprecated and must not be used. Try a different algorithm.".into()); + } + + let digest = dnskey + .digest(&owner, digest_alg) + .map_err(|e| format!("Error computing digest: {e}"))?; + + let ds = Ds::new(key_tag, sec_alg, digest_alg, digest).expect( + "Infallible because the digest won't be too long since it's a valid digest", + ); + + let rr = Record::new(owner, class, ttl, ds); + + if self.write_to_stdout { + writeln!(env.stdout(), "{}", rr.display_zonefile(false)); + } else { + let owner = owner.fmt_with_dot(); + let sec_alg = sec_alg.to_int(); + + let keyname = format!("K{owner}+{sec_alg:03}+{key_tag:05}"); + let filename = format!("{keyname}.ds"); + + let res = if self.force_overwrite { + File::create(env.in_cwd(&filename)) + } else { + let res = File::create_new(env.in_cwd(&filename)); + + // Create a bit of a nicer message than a "File exists" IO + // error. + if let Err(e) = &res { + if e.kind() == io::ErrorKind::AlreadyExists { + return Err(format!( + "The file '{filename}' already exists, use the --force to overwrite" + ) + .into()); + } + } + + res + }; + + let mut out_file = + res.map_err(|e| format!("Could not create file \"{filename}\": {e}"))?; + + writeln!(out_file, "{}", rr.display_zonefile(false)) + .map_err(|e| format!("Could not write to file \"{filename}\": {e}"))?; + + writeln!(env.stdout(), "{keyname}"); + } + } + + Ok(()) + } +} + +fn determine_hash_from_sec_alg(sec_alg: SecAlg) -> DigestAlg { + match sec_alg { + SecAlg::RSASHA256 + | SecAlg::RSASHA512 + | SecAlg::ED25519 + | SecAlg::ED448 + | SecAlg::ECDSAP256SHA256 => DigestAlg::SHA256, + SecAlg::ECDSAP384SHA384 => DigestAlg::SHA384, + SecAlg::ECC_GOST => DigestAlg::GOST, + _ => DigestAlg::SHA1, + } +} + +#[cfg(test)] +mod test { + use domain::base::iana::DigestAlg; + use tempfile::TempDir; + + use crate::commands::Command; + use crate::env::fake::FakeCmd; + use std::fs::File; + use std::io::Write; + use std::path::PathBuf; + + use super::Key2ds; + + #[track_caller] + fn parse(args: FakeCmd) -> Key2ds { + let res = args.parse(); + let Command::Key2ds(x) = res.unwrap().command else { + panic!("Not a Key2ds!"); + }; + x + } + + #[test] + fn dnst_parse() { + let cmd = FakeCmd::new(["dnst", "key2ds"]); + + cmd.parse().unwrap_err(); + cmd.args(["keyfile1.key", "keyfile2.key"]) + .parse() + .unwrap_err(); + + let base = Key2ds { + ignore_sep: false, + write_to_stdout: false, + force_overwrite: false, + algorithm: None, + keyfile: PathBuf::from("keyfile1.key"), + }; + + // Check the defaults + let res = parse(cmd.args(["keyfile1.key"])); + assert_eq!(res, base); + + let res = parse(cmd.args(["keyfile1.key", "-f"])); + assert_eq!( + res, + Key2ds { + force_overwrite: true, + ..base.clone() + } + ); + + let res = parse(cmd.args(["keyfile1.key", "--force"])); + assert_eq!( + res, + Key2ds { + force_overwrite: true, + ..base.clone() + } + ); + + let res = parse(cmd.args(["keyfile1.key", "--ignore-sep"])); + assert_eq!( + res, + Key2ds { + ignore_sep: true, + ..base.clone() + } + ); + + let res = parse(cmd.args(["keyfile1.key", "-n"])); + assert_eq!( + res, + Key2ds { + write_to_stdout: true, + ..base.clone() + } + ); + + let res = parse(cmd.args(["keyfile1.key", "-a", "SHA-1"])); + assert_eq!( + res, + Key2ds { + algorithm: Some(DigestAlg::SHA1), + ..base.clone() + } + ); + + let res = parse(cmd.args(["keyfile1.key", "--algorithm", "SHA-1"])); + assert_eq!( + res, + Key2ds { + algorithm: Some(DigestAlg::SHA1), + ..base.clone() + } + ); + + let res = parse(cmd.args(["keyfile1.key", "--algorithm", "1"])); + assert_eq!( + res, + Key2ds { + algorithm: Some(DigestAlg::SHA1), + ..base.clone() + } + ); + } + + #[test] + fn ldns_parse() { + let cmd = FakeCmd::new(["ldns-key2ds"]); + + cmd.parse().unwrap_err(); + cmd.args(["keyfile1.key", "keyfile2.key"]) + .parse() + .unwrap_err(); + cmd.args(["-a", "keyfile2.key"]).parse().unwrap_err(); + cmd.args(["-fdoesnottakeavalue", "keyfile2.key"]) + .parse() + .unwrap_err(); + + let base = Key2ds { + ignore_sep: false, + write_to_stdout: false, + force_overwrite: true, // note that this is true + algorithm: None, + keyfile: PathBuf::from("keyfile1.key"), + }; + + // Check the defaults + let res = parse(cmd.args(["keyfile1.key"])); + assert_eq!(res, base,); + + let res = parse(cmd.args(["keyfile1.key", "-f"])); + assert_eq!( + res, + Key2ds { + ignore_sep: true, + ..base.clone() + } + ); + + let res = parse(cmd.args(["keyfile1.key", "-fn"])); + assert_eq!( + res, + Key2ds { + ignore_sep: true, + write_to_stdout: true, + ..base.clone() + } + ); + + let res = parse(cmd.args(["keyfile1.key", "-1"])); + assert_eq!( + res, + Key2ds { + algorithm: Some(DigestAlg::SHA1), + ..base.clone() + } + ); + + let res = parse(cmd.args(["keyfile1.key", "-fnfn421"])); + assert_eq!( + res, + Key2ds { + ignore_sep: true, + write_to_stdout: true, + algorithm: Some(DigestAlg::SHA1), + ..base.clone() + } + ); + } + + fn run_setup() -> TempDir { + let dir = tempfile::TempDir::new().unwrap(); + let mut file = File::create(dir.path().join("key1.key")).unwrap(); + file + .write_all(b"example.test. IN DNSKEY 257 3 15 8AWQIqSo35guqX6WPIFsUlOnbiqGC5sydeBTVMdLGMs= ;{id = 60136 (ksk), size = 256b}\n") + .unwrap(); + + let mut file = File::create(dir.path().join("key2.key")).unwrap(); + file.write_all( + b"\ + one.test. IN DNSKEY 257 3 15 JKVltzkO0wxbjrY1dNKjEHrXvPqahmbmqwXaNrSwXsI=\n\ + two.test. IN DNSKEY 257 3 15 F0jH0dfoYXe9/tKqoghlZTY5+K/uRQReTkjvBmr7gy8=\n\ + ", + ) + .unwrap(); + + dir + } + + #[test] + fn file_with_single_key() { + let dir = run_setup(); + + let res = FakeCmd::new(["dnst", "key2ds", "key1.key"]).cwd(&dir).run(); + + assert_eq!(res.exit_code, 0, "{res:?}"); + assert_eq!(res.stdout, "Kexample.test.+015+60136\n"); + assert_eq!(res.stderr, ""); + + let out = std::fs::read_to_string(dir.path().join("Kexample.test.+015+60136.ds")).unwrap(); + assert_eq!(out, "example.test. 3600 IN DS 60136 15 2 52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n"); + } + + #[test] + fn file_with_two_keys() { + let dir = run_setup(); + + let res = FakeCmd::new(["dnst", "key2ds", "key2.key"]).cwd(&dir).run(); + + assert_eq!(res.exit_code, 0, "{res:?}"); + assert_eq!(res.stdout, "Kone.test.+015+38429\nKtwo.test.+015+00425\n",); + assert_eq!(res.stderr, ""); + + let out = std::fs::read_to_string(dir.path().join("Kone.test.+015+38429.ds")).unwrap(); + assert_eq!(out, "one.test. 3600 IN DS 38429 15 2 B85F7D27C48A7B84D633C7A41C3022EA0F7FC80896227B61AE7BFC59BF5F0256\n"); + + let out = std::fs::read_to_string(dir.path().join("Ktwo.test.+015+00425.ds")).unwrap(); + assert_eq!(out, "two.test. 3600 IN DS 425 15 2 AA2030287A7C5C56CB3C0E9C64BE55616729C0C78DE2B83613D03B10C0F1EA93\n"); + } + + #[test] + fn print_to_stdout() { + let dir = run_setup(); + + let res = FakeCmd::new(["dnst", "key2ds", "-n", "key1.key"]) + .cwd(&dir) + .run(); + + assert_eq!(res.exit_code, 0); + assert_eq!( + res.stdout, + "example.test. 3600 IN DS 60136 15 2 52BD3BF40C8220BF1A3E2A3751C423BC4B69BCD7F328D38C4CD021A85DE65AD4\n" + ); + assert_eq!(res.stderr, ""); + } + + #[test] + fn overwrite_file() { + let dir = run_setup(); + + // Make sure the file already exists + File::create(dir.path().join("Kexample.test.+015+60136.ds")).unwrap(); + + let res = FakeCmd::new(["dnst", "key2ds", "key1.key"]).cwd(&dir).run(); + + assert_eq!(res.exit_code, 1); + assert_eq!(res.stdout, ""); + assert!(res.stderr.contains( + "The file 'Kexample.test.+015+60136.ds' already exists, use the --force to overwrite" + )); + + let res = FakeCmd::new(["dnst", "key2ds", "--force", "key1.key"]) + .cwd(&dir) + .run(); + + assert_eq!(res.exit_code, 0); + assert_eq!(res.stdout, "Kexample.test.+015+60136\n"); + assert_eq!(res.stderr, ""); + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b7dbb3d..53ac766 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,11 +1,13 @@ //! The command of _dnst_. pub mod help; +pub mod key2ds; pub mod nsec3hash; use std::ffi::{OsStr, OsString}; use std::str::FromStr; +use key2ds::Key2ds; use nsec3hash::Nsec3Hash; use crate::env::Env; @@ -19,6 +21,14 @@ pub enum Command { #[command(name = "nsec3-hash")] Nsec3Hash(self::nsec3hash::Nsec3Hash), + /// Generate a DS RR from the DNSKEYS in keyfile + /// + /// The following file will be created for each key: + /// `K++.ds`. The base name `K++` + /// will be printed to stdout. + #[command(name = "key2ds")] + Key2ds(key2ds::Key2ds), + /// Show the manual pages Help(self::help::Help), } @@ -27,6 +37,7 @@ impl Command { pub fn execute(self, env: impl Env) -> Result<(), Error> { match self { Self::Nsec3Hash(nsec3hash) => nsec3hash.execute(env), + Self::Key2ds(key2ds) => key2ds.execute(env), Self::Help(help) => help.execute(), } } @@ -59,6 +70,12 @@ impl From for Command { } } +impl From for Command { + fn from(val: Key2ds) -> Self { + Command::Key2ds(val) + } +} + /// Utility function to parse an [`OsStr`] with a custom function fn parse_os_with(opt: &str, val: &OsStr, f: impl Fn(&str) -> Result) -> Result where diff --git a/src/env/fake.rs b/src/env/fake.rs index f9d4cde..0715ee5 100644 --- a/src/env/fake.rs +++ b/src/env/fake.rs @@ -1,5 +1,7 @@ +use std::borrow::Cow; use std::ffi::OsString; use std::fmt; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::Mutex; @@ -16,11 +18,13 @@ use super::Stream; pub struct FakeCmd { /// The command to run, including `argv[0]` cmd: Vec, + cwd: Option, } /// The result of running a [`FakeCmd`] /// /// The fields are public to allow for easy assertions in tests. +#[derive(Debug)] pub struct FakeResult { pub exit_code: u8, pub stdout: String, @@ -53,6 +57,13 @@ impl Env for FakeEnv { fn stderr(&self) -> Stream { Stream(self.stderr.clone()) } + + fn in_cwd<'a>(&self, path: &'a impl AsRef) -> Cow<'a, Path> { + match &self.cmd.cwd { + Some(cwd) => cwd.join(path).into(), + None => path.as_ref().into(), + } + } } impl FakeCmd { @@ -62,6 +73,14 @@ impl FakeCmd { pub fn new>(cmd: impl IntoIterator) -> Self { Self { cmd: cmd.into_iter().map(Into::into).collect(), + cwd: None, + } + } + + pub fn cwd(&self, path: impl AsRef) -> Self { + Self { + cwd: Some(path.as_ref().to_path_buf()), + ..self.clone() } } diff --git a/src/env/mod.rs b/src/env/mod.rs index b00b57d..dd62dcb 100644 --- a/src/env/mod.rs +++ b/src/env/mod.rs @@ -1,5 +1,7 @@ +use std::borrow::Cow; use std::ffi::OsString; use std::fmt; +use std::path::Path; mod real; @@ -32,6 +34,8 @@ pub trait Env { // /// Get a reference to stdin // fn stdin(&self) -> impl io::Read; + + fn in_cwd<'a>(&self, path: &'a impl AsRef) -> Cow<'a, Path>; } /// A type with an infallible `write_fmt` method for use with [`write!`] macros @@ -73,4 +77,8 @@ impl Env for &E { fn stderr(&self) -> Stream { (**self).stderr() } + + fn in_cwd<'a>(&self, path: &'a impl AsRef) -> Cow<'a, Path> { + (**self).in_cwd(path) + } } diff --git a/src/env/real.rs b/src/env/real.rs index c854db4..26c01aa 100644 --- a/src/env/real.rs +++ b/src/env/real.rs @@ -1,6 +1,7 @@ use std::ffi::OsString; use std::fmt; use std::io; +use std::path::Path; use super::Env; use super::Stream; @@ -20,6 +21,10 @@ impl Env for RealEnv { fn stderr(&self) -> Stream { Stream(FmtWriter(io::stderr())) } + + fn in_cwd<'a>(&self, path: &'a impl AsRef) -> std::borrow::Cow<'a, std::path::Path> { + path.as_ref().into() + } } struct FmtWriter(T); diff --git a/src/lib.rs b/src/lib.rs index 8a14999..48e4075 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ use std::ffi::OsString; use std::path::Path; use clap::Parser; -use commands::{nsec3hash::Nsec3Hash, LdnsCommand}; +use commands::{key2ds::Key2ds, nsec3hash::Nsec3Hash, LdnsCommand}; use env::Env; use error::Error; @@ -26,6 +26,7 @@ pub fn try_ldns_compatibility>( .ok_or("Binary file name is not valid unicode")?; let res = match binary_name { + "ldns-key2ds" => Key2ds::parse_ldns_args(args_iter), "ldns-nsec3-hash" => Nsec3Hash::parse_ldns_args(args_iter), _ => return Ok(None), }; From bfbf492e32210f5cb65d7c461d1142659f8fbfbe Mon Sep 17 00:00:00 2001 From: Jannik Date: Tue, 19 Nov 2024 14:26:24 +0100 Subject: [PATCH 8/8] Add manual pages (#26) * Fix sphinx default language * Add ldns-nsec3-hash man page based on the original, and adjust the dnst-nsec3-hash page to match the current help output of the command. * Update dnst-nsec3-hash.rst * Add key2ds manual * Add dnst-keygen manual * Change dnst-keygen algorithms to list from table * Change dnst-keygen algorithms back to table * Add ldns-keygen manual * Add notify manuals * Add signzone manuals * Add subcommands to dnst manual and table of contents * Update dnst-nsec3-hash manual * Add update manual * Apply feedback * Apply further feedback * Move signzone date description into own section * Update signzone hash iterations manual text * Add Arguments sections * Add basic intro text for dnst * Fix ldns-signzone default nsec3 hash iterations * Update nse3-hash defaults and wording * Update dnst-key2ds ignore-sep and force * Update nse3-hash default to what it is currently in main --------- Co-authored-by: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Co-authored-by: Terts Diepraam --- doc/manual/source/conf.py | 16 ++- doc/manual/source/index.rst | 24 ++++- doc/manual/source/man/dnst-key2ds.rst | 45 +++++++++ doc/manual/source/man/dnst-keygen.rst | 74 ++++++++++++++ doc/manual/source/man/dnst-notify.rst | 50 +++++++++ doc/manual/source/man/dnst-nsec3-hash.rst | 27 +++-- doc/manual/source/man/dnst-signzone.rst | 118 ++++++++++++++++++++++ doc/manual/source/man/dnst-update.rst | 46 +++++++++ doc/manual/source/man/dnst.rst | 27 ++++- doc/manual/source/man/ldns-key2ds.rst | 43 ++++++++ doc/manual/source/man/ldns-keygen.rst | 60 +++++++++++ doc/manual/source/man/ldns-notify.rst | 60 +++++++++++ doc/manual/source/man/ldns-nsec3-hash.rst | 30 ++++++ doc/manual/source/man/ldns-signzone.rst | 109 ++++++++++++++++++++ doc/manual/source/man/ldns-update.rst | 46 +++++++++ 15 files changed, 758 insertions(+), 17 deletions(-) create mode 100644 doc/manual/source/man/dnst-key2ds.rst create mode 100644 doc/manual/source/man/dnst-keygen.rst create mode 100644 doc/manual/source/man/dnst-notify.rst create mode 100644 doc/manual/source/man/dnst-signzone.rst create mode 100644 doc/manual/source/man/dnst-update.rst create mode 100644 doc/manual/source/man/ldns-key2ds.rst create mode 100644 doc/manual/source/man/ldns-keygen.rst create mode 100644 doc/manual/source/man/ldns-notify.rst create mode 100644 doc/manual/source/man/ldns-nsec3-hash.rst create mode 100644 doc/manual/source/man/ldns-signzone.rst create mode 100644 doc/manual/source/man/ldns-update.rst diff --git a/doc/manual/source/conf.py b/doc/manual/source/conf.py index ffa9b5d..a42d54e 100644 --- a/doc/manual/source/conf.py +++ b/doc/manual/source/conf.py @@ -100,7 +100,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -189,8 +189,18 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('man/dnst', 'dnst', 'DNS Management Tools', author, 1), - ('man/dnst-nsec3-hash', 'dnst-nsec3-hash', 'DNS Management Tools', author, - 1), + ('man/dnst-key2ds', 'dnst-key2ds', 'Generate DS RRs from the DNSKEYs in a keyfile', author, 1), + ('man/ldns-key2ds', 'ldns-key2ds', 'Generate DS RRs from the DNSKEYs in a keyfile', author, 1), + ('man/dnst-keygen', 'dnst-keygen', 'Generate a new key pair for a domain name', author, 1), + ('man/ldns-keygen', 'ldns-keygen', 'Generate a new key pair for a domain name', author, 1), + ('man/dnst-notify', 'dnst-notify', 'Send a NOTIFY message to a list of name servers', author, 1), + ('man/ldns-notify', 'ldns-notify', 'Send a NOTIFY message to a list of name servers', author, 1), + ('man/dnst-nsec3-hash', 'dnst-nsec3-hash', 'Print out the NSEC3 hash of a domain name', author, 1), + ('man/ldns-nsec3-hash', 'ldns-nsec3-hash', 'Print out the NSEC3 hash of a domain name', author, 1), + ('man/dnst-signzone', 'dnst-signzone', 'Sign the zone with the given key(s)', author, 1), + ('man/ldns-signzone', 'ldns-signzone', 'Sign the zone with the given key(s)', author, 1), + ('man/dnst-update', 'dnst-update', 'Send a dynamic update packet to update an IP (or delete all existing IPs) for a domain name', author, 1), + ('man/ldns-update', 'ldns-update', 'Send a dynamic update packet to update an IP (or delete all existing IPs) for a domain name', author, 1), ] diff --git a/doc/manual/source/index.rst b/doc/manual/source/index.rst index 8f8bcf0..227ccff 100644 --- a/doc/manual/source/index.rst +++ b/doc/manual/source/index.rst @@ -1,7 +1,12 @@ dnst |version| ============== -The manual goes here ... +**dnst** is a DNS administration toolbox. It offers DNS and DNSSEC related +functions like key generation, zone signing, printing NSEC3 hashed domain +names, and sending UPDATE or NOTIFY messages to your name servers. More is +coming soon. + +It depends on OpenSSL for its cryptography related functions. .. toctree:: :maxdepth: 2 @@ -10,5 +15,22 @@ The manual goes here ... :name: toc-reference man/dnst + man/dnst-key2ds + man/dnst-keygen + man/dnst-notify man/dnst-nsec3-hash + man/dnst-signzone + man/dnst-update + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: LDNS Tools reference + :name: toc-reference-ldns + man/ldns-key2ds + man/ldns-keygen + man/ldns-notify + man/ldns-nsec3-hash + man/ldns-signzone + man/ldns-update diff --git a/doc/manual/source/man/dnst-key2ds.rst b/doc/manual/source/man/dnst-key2ds.rst new file mode 100644 index 0000000..844880c --- /dev/null +++ b/doc/manual/source/man/dnst-key2ds.rst @@ -0,0 +1,45 @@ +dnst key2ds +=============== + +Synopsis +-------- + +:program:`dnst key2ds` ``[OPTIONS]`` ```` + +Description +----------- + +**dnst key2ds** generates a DS RR for each DNSKEY in ````. + +The following file will be created for each key: ``K++.ds``. The +base name ``K++`` will be printed to stdout. + + +Options +------- + +.. option:: -a , --algorithm + + Use the given algorithm for the digest. Defaults to the digest algorithm + used for the DNSKEY, and if it can't be determined SHA-1. + +.. option:: -f, --force + + Overwrite existing ``.ds`` files. + +.. option:: --ignore-sep + + Ignore the SEP flag and make DS records for any key. + +.. option:: -n + + Write the generated DS records to stdout instead of a file. + +.. option:: -h, --help + + Print the help text (short summary with ``-h``, long help with + ``--help``). + +.. option:: -V, --version + + Print the version. diff --git a/doc/manual/source/man/dnst-keygen.rst b/doc/manual/source/man/dnst-keygen.rst new file mode 100644 index 0000000..e47b761 --- /dev/null +++ b/doc/manual/source/man/dnst-keygen.rst @@ -0,0 +1,74 @@ +dnst keygen +=============== + +Synopsis +-------- + +:program:`dnst keygen` ``[OPTIONS]`` ``-a `` ```` + +Description +----------- + +**dnst keygen** generates a new key pair for a given domain name. + +The following files will be created: + +- ``K++.key``: The public key file containing a DNSKEY RR in + zone file format. + +- ``K++.private``: The private key file containing the private + key data fields in BIND's *Private-key-format*. + +- ``K++.ds``: The public key digest file containing the DS RR + in zone file format. It is only created for key signing keys. + +| ```` is the fully-qualified owner name for the key (with a trailing dot). +| ```` is the algorithm number of the key, zero-padded to 3 digits. +| ```` is the 16-bit tag of the key, zero-padded to 5 digits. + +Upon completion, ``K++`` will be printed. + +Options +------- + +.. option:: -a + + Use the given signing algorithm. + + Possible values are: + + =================== ========== ========================= + **Mnemonic** **Number** **Description** + =================== ========== ========================= + ``list`` List available algorithms + ``RSASHA256`` 8 RSA with SHA-256 + ``ECDSAP256SHA256`` 13 ECDSA P-256 with SHA-256 + ``ECDSAP384SHA384`` 14 ECDSA P-384 with SHA-384 + ``ED25519`` 15 ED25519 + ``ED448`` 16 ED448 + =================== ========== ========================= + +.. option:: -k + + Generate a key signing key (KSK) instead of a zone signing key (ZSK). + +.. option:: -b + + The length of the key (for RSA keys only). Defaults to 2048. + +.. option:: -r + + The randomness source to use for generation. Defaults to ``/dev/urandom``. + +.. option:: -s + + Create symlinks ``.key`` and ``.private`` to the generated keys. + +.. option:: -f + + Overwrite existing symlinks (for use with ``-s``). + +.. option:: -h, --help + + Print the help text (short summary with ``-h``, long help with + ``--help``). diff --git a/doc/manual/source/man/dnst-notify.rst b/doc/manual/source/man/dnst-notify.rst new file mode 100644 index 0000000..1fa979c --- /dev/null +++ b/doc/manual/source/man/dnst-notify.rst @@ -0,0 +1,50 @@ +dnst notify +=============== + +Synopsis +-------- + +:program:`dnst notify` ``[OPTIONS]`` ``-z `` ``...`` + +Description +----------- + +**dnst notify** sends a NOTIFY message to the specified name servers. A name +server can be specified as a domain name or IP address. + +This tells them that an updated zone is available at the primaries. It can +perform TSIG signatures, and it can add a SOA serial number of the updated +zone. If a server already has that serial number it will disregard the message. + +Options +------- + +.. option:: -z + + The zone to send the NOTIFY for. + +.. option:: -s + + SOA version number to include in the NOTIFY message. + +.. option:: -y, --tsig + + A base64 TSIG key and optional algorithm to use for the NOTIFY message. + The algorithm defaults to **hmac-sha512**. + +.. option:: -p, --port + + Destination port to send the UDP packet to. Defaults to 53. + +.. option:: -d, --debug + + Print debug information. + +.. option:: -r, --retries + + Max number of retries. Defaults to 15. + +.. option:: -h, --help + + Print the help text (short summary with ``-h``, long help with + ``--help``). diff --git a/doc/manual/source/man/dnst-nsec3-hash.rst b/doc/manual/source/man/dnst-nsec3-hash.rst index 2001447..78d35da 100644 --- a/doc/manual/source/man/dnst-nsec3-hash.rst +++ b/doc/manual/source/man/dnst-nsec3-hash.rst @@ -1,30 +1,39 @@ -dnst-nsec3-hash +dnst nsec3-hash =============== Synopsis -------- -:program:`dnst nsec3-hash` [``options``] :samp:`domain-name` +:program:`dnst nsec3-hash` ``[OPTIONS]`` ```` Description ----------- -**dnst nsec3-hash** prints the NSEC3 hash for the given domain name. +**dnst nsec3-hash** prints the NSEC3 hash of a given domain name. Options ------- -.. option:: -a number-or-mnemonic, --algorithm=number-or-mnemonic +.. option:: -a , --algorithm Use the given algorithm number for the hash calculation. Defaults to - ``sha1``. + 1 (SHA-1). -.. option:: -s salt, --salt=count +.. option:: -i , -t , --iterations + + Use the given number of additional iterations for the hash + calculation. Defaults to 1. + +.. option:: -s , --salt Use the given salt for the hash calculation. The salt value should be - in hexadecimal format. + in hexadecimal format. Defaults to an empty salt. + +.. option:: -h, --help -.. option:: -i count, -t count, --iterations=count + Print the help text (short summary with ``-h``, long help with + ``--help``). - Use *count* iterations for the hash calculation. +.. option:: -V, --version + Print the version. diff --git a/doc/manual/source/man/dnst-signzone.rst b/doc/manual/source/man/dnst-signzone.rst new file mode 100644 index 0000000..606b33f --- /dev/null +++ b/doc/manual/source/man/dnst-signzone.rst @@ -0,0 +1,118 @@ +dnst signzone +=============== + +Synopsis +-------- + +:program:`dnst signzone` ``[OPTIONS]`` ```` ``...`` + +Description +----------- + +**dnst signzone** signs the zonefile with the given key(s). + +Keys must be specified by their base name (usually ``K++``), +i.e. WITHOUT the ``.private`` or ``.key`` extension. Both ``.private`` and +``.key`` files are required. + +Arguments +--------- + +.. option:: + + The zonefile to sign. + +.. option:: ... + + The keys to sign the zonefile with. + +Options +------- + +.. option:: -b + + Add comments on DNSSEC records. Without this option only DNSKEY RRs + will have their key tag annotated in the comment. + +.. option:: -d + + Do not add used keys to the resulting zonefile. + +.. option:: -e + + Set the expiration date of signatures to this date (see + :ref:`dnst-signzone-dates`). Defaults to 4 weeks from now. + +.. option:: -f + + Write signed zone to file. Use ``-f -`` to output to stdout. Defaults to + ``.signed``. + +.. option:: -i + + Set the inception date of signatures to this date (see + :ref:`dnst-signzone-dates`). Defaults to now. + +.. option:: -o + + Set the origin for the zone (only necessary for zonefiles with relative + names and no $ORIGIN). + +.. option:: -u + + Set SOA serial to the number of seconds since Jan 1st 1970. + + If this would NOT result in the SOA serial increasing it will be + incremented instead. + +.. option:: -n + + Use NSEC3 instead of NSEC. By default, RFC 9276 best practice settings + are used: SHA-1, no extra iterations, empty salt. To use different NSEC3 + settings see :ref:`dnst-signzone-nsec3-options`. + +.. option:: -H + + Hash only, don't sign. + +.. option:: -h, --help + + Print the help text (short summary with ``-h``, long help with + ``--help``). + + +.. _dnst-signzone-nsec3-options: + +NSEC3 options +-------------------------------- + +The following options can be used with ``-n`` to override the default NSEC3 +settings used. + +.. option:: -a + + Specify the hashing algorithm. Defaults to SHA-1. + +.. option:: -t + + Set the number of extra hash iterations. Defaults to 0. + +.. option:: -s + + Specify the salt as a hex string. Defaults to ``-``, meaning empty salt. + +.. option:: -p + + Set the opt-out flag on all NSEC3 RRs. + +.. option:: -A + + Set the opt-out flag on all NSEC3 RRs and skip unsigned delegations. + +.. _dnst-signzone-dates: + +DATES +----- + +A date can be a UNIX timestamp as seconds since the Epoch (1970-01-01 +00:00 UTC), or of the form ````. diff --git a/doc/manual/source/man/dnst-update.rst b/doc/manual/source/man/dnst-update.rst new file mode 100644 index 0000000..6d4752b --- /dev/null +++ b/doc/manual/source/man/dnst-update.rst @@ -0,0 +1,46 @@ +dnst update +=============== + +Synopsis +-------- + +:program:`dnst update` ```` ``[ZONE]`` ```` +``[ ]`` + +Description +----------- + +**dnst update** sends a dynamic update packet to update an IP (or delete all +existing IPs) for a domain name. + +Arguments +--------- + +.. option:: + + The domain name to update the IP address of + +.. option:: + + The zone to send the update to (if omitted, derived from SOA record) + +.. option:: + + The IP to update the domain with (``none`` to remove any existing IPs) + +.. option:: + + TSIG key name + +.. option:: + + TSIG algorithm (e.g. "hmac-sha256") + +.. option:: + + Base64 encoded TSIG key data. + +.. option:: -h, --help + + Print the help text (short summary with ``-h``, long help with + ``--help``). diff --git a/doc/manual/source/man/dnst.rst b/doc/manual/source/man/dnst.rst index f132f2d..09b4bb7 100644 --- a/doc/manual/source/man/dnst.rst +++ b/doc/manual/source/man/dnst.rst @@ -4,15 +4,15 @@ dnst Synopsis -------- -:program:`dnst` [``options``] ``command`` [``args``] +:program:`dnst` ``[OPTIONS]`` ```` ``[ARGS]`` Description ----------- Manage various aspects of the Domain Name System (DNS). -dnst provides a number of commands that perform various tasks related -managing DNS server and DNS zones. +**dnst** provides a number of commands that perform various tasks related to +managing DNS servers and DNS zones. Please consult the manual pages for these individual commands for more information. @@ -22,7 +22,26 @@ dnst Commands .. glossary:: + :doc:`dnst-key2ds ` (1) + + Generate DS RRs from the DNSKEYs in a keyfile. + + :doc:`dnst-keygen ` (1) + + Generate a new key pair for a domain name. + + :doc:`dnst-notify ` (1) + + Send a NOTIFY message to a list of name servers. + :doc:`dnst-nsec3-hash ` (1) - Prints the NSEC3 hash for a domain name. + Print out the NSEC3 hash of a domain name. + + :doc:`dnst-signzone ` (1) + + Sign the zone with the given key(s). + + :doc:`dnst-update ` (1) + Send a dynamic update packet to update an IP (or delete all existing IPs) for a domain name. diff --git a/doc/manual/source/man/ldns-key2ds.rst b/doc/manual/source/man/ldns-key2ds.rst new file mode 100644 index 0000000..daf14b4 --- /dev/null +++ b/doc/manual/source/man/ldns-key2ds.rst @@ -0,0 +1,43 @@ +ldns-key2ds +=============== + +Synopsis +-------- + +:program:`ldns-key2ds` ``[OPTIONS]`` ```` + +Description +----------- + +**ldns-key2ds** is used to transform a public DNSKEY RR to a DS RR. When run +it will read ```` with a DNSKEY RR in it, and it will create a .ds +file with the DS RR in it. + +It prints out the basename for this file (``K++``). + +By default, it takes a pick of algorithm similar to the key algorithm, +SHA1 for RSASHA1, and so on. + + +Options +------- + +.. option:: -f + + Ignore SEP flag (i.e. make DS records for any key) + +.. option:: -n + + Write the result DS Resource Record to stdout instead of a file + +.. option:: -1 + + Use SHA1 as the hash function. + +.. option:: -2 + + Use SHA256 as the hash function + +.. option:: -4 + + Use SHA383 as the hash function diff --git a/doc/manual/source/man/ldns-keygen.rst b/doc/manual/source/man/ldns-keygen.rst new file mode 100644 index 0000000..6f3703d --- /dev/null +++ b/doc/manual/source/man/ldns-keygen.rst @@ -0,0 +1,60 @@ +ldns-keygen +=============== + +Synopsis +-------- + +:program:`ldns-keygen` ``[OPTIONS]`` ```` + +Description +----------- + +**ldns-keygen** is used to generate a private/public keypair. When run, it will +create 3 files; a ``.key`` file with the public DNSKEY, a ``.private`` file +with the private keydata and a ``.ds`` file with the DS record of the DNSKEY +record. + +.. **ldns-keygen** can also be used to create symmetric keys (for TSIG) by +.. selecting the appropriate algorithm: hmac-md5.sig-alg.reg.int, hmac-sha1, +.. hmac-sha224, hmac-sha256, hmac-sha384 or hmac-sha512. In that case no DS record +.. will be created and no .ds file. + +ldns-keygen prints the basename for the key files: ``K++`` + +Options +------- + +.. option:: -a + + Create a key with this algorithm. Specifying 'list' here gives a list of + supported algorithms. Several alias names are also accepted (from older + versions and other software), the list gives names from the RFC. Also the + plain algorithm number is accepted. + +.. option:: -b + + Use this many bits for the key length. + +.. option:: -k + + When given, generate a key signing key. This just sets the flag field to + 257 instead of 256 in the DNSKEY RR in the .key file. + +.. option:: -r + + Make ldns-keygen use this file to seed the random generator with. This + will default to /dev/random. + +.. option:: -s + + ldns-keygen will create symbolic links named ``.private`` to the new + generated private key, ``.key`` to the public DNSKEY and ``.ds`` to the + file containing DS record data. + +.. option:: -f + + Force symlinks to be overwritten if they exist. + +.. option:: -v + + Show the version and exit diff --git a/doc/manual/source/man/ldns-notify.rst b/doc/manual/source/man/ldns-notify.rst new file mode 100644 index 0000000..e9cfb81 --- /dev/null +++ b/doc/manual/source/man/ldns-notify.rst @@ -0,0 +1,60 @@ +ldns-notify +=============== + +Synopsis +-------- + +:program:`ldns-notify` ``[OPTIONS]`` ``-z `` ``...`` + +Description +----------- + +**ldns-notify** sends a NOTIFY packet to the specified name servers. A name +server can be specified as a domain name or IP address. + +This tells them that an updated zone is available at the primaries. It can +perform TSIG signatures, and it can add a SOA serial number of the updated +zone. If a server already has that serial number it will disregard the message. + +Options +------- + +.. option:: -z + + The zone that is updated. + +.. ..option:: -I
+.. +.. Source IP to send the message from. + +.. option:: -s + + Append a SOA record indicating the serial number of the updated zone. + +.. option:: -p + + Destination port to send the UDP packet to. Defaults to 53. + +.. option:: -y + + A base64 TSIG key and optional algorithm to use for the NOTIFY message. + The algorithm defaults to hmac-sha512. + +.. option:: -d + + Print verbose debug information. The query that is sent and the query + that is received. + +.. option:: -r + + Specify the maximum number of retries before notify gives up trying to + send the UDP packet. + +.. option:: -h + + Print the help text and exit. + +.. option:: -v + + Print the version and exit. + diff --git a/doc/manual/source/man/ldns-nsec3-hash.rst b/doc/manual/source/man/ldns-nsec3-hash.rst new file mode 100644 index 0000000..edfff6d --- /dev/null +++ b/doc/manual/source/man/ldns-nsec3-hash.rst @@ -0,0 +1,30 @@ +ldns-nsec3-hash +=============== + +Synopsis +-------- + +:program:`ldns-nsec3-hash` ``[OPTIONS]`` ```` + +Description +----------- + +**ldns-nsec3-hash** is used to print out the NSEC3 hash for the given domain name. + +Options +------- + +.. option:: -a + + Use the given algorithm number for the hash calculation. Defaults to + 1 (SHA-1). + +.. option:: -s + + Use the given salt for the hash calculation. The salt value should be + in hexadecimal format. Defaults to an empty salt. + +.. option:: -t + + Use the given number of additional iterations for the hash + calculation. Defaults to 1. diff --git a/doc/manual/source/man/ldns-signzone.rst b/doc/manual/source/man/ldns-signzone.rst new file mode 100644 index 0000000..a501a15 --- /dev/null +++ b/doc/manual/source/man/ldns-signzone.rst @@ -0,0 +1,109 @@ +ldns-signzone +=============== + +Synopsis +-------- + +:program:`ldns-signzone` ``[OPTIONS]`` ```` ``...`` + +Description +----------- + +**ldns-signzone** signs the zone with the given key(s). + +Keys must be specified by their base name (usually ``K++``), +i.e. WITHOUT the ``.private`` or ``.key`` extension. Both ``.private`` and +``.key`` files are required. + +Arguments +--------- + +.. option:: + + The zonefile to sign. + +.. option:: ... + + The keys to sign the zonefile with. + +Options +------- + +.. option:: -b + + Add comments on DNSSEC records. Without this option only DNSKEY RRs + will have their key tag annotated in the comment. + +.. option:: -d + + Do not add used keys to the resulting zonefile. + +.. option:: -e + + Set the expiration date of signatures to this date (see + :ref:`ldns-signzone-dates`). Defaults to 4 weeks from now. + +.. option:: -f + + Write signed zone to file. Use ``-f -`` to output to stdout. Defaults to + ``.signed``. + +.. option:: -i + + Set the inception date of signatures to this date (see + :ref:`ldns-signzone-dates`). Defaults to now. + +.. option:: -o + + Set the origin for the zone (only necessary for zonefiles with + relative names and no $ORIGIN). + +.. option:: -u + + Set SOA serial to the number of seconds since Jan 1st 1970. + +.. option:: -n + + Use NSEC3 instead of NSEC. If specified, you can use extra options (see + :ref:`ldns-signzone-nsec3-options`). + +.. option:: -h + + Print the help text. + +.. option:: -v + + Print the version and exit. + + +.. _ldns-signzone-nsec3-options: + +NSEC3 options +-------------------------------- + +The following options can be used with ``-n`` to override the default NSEC3 +settings used. + +.. option:: -a + + Specify the hashing algorithm. Defaults to SHA-1. + +.. option:: -t + + Set the number of extra hash iterations. Defaults to 1. + +.. option:: -s + + Specify the salt as a hex string. Defaults to ``-``, meaning empty salt. + +.. option:: -p + + Set the opt-out flag on all NSEC3 RRs. + +.. _ldns-signzone-dates: + +DATES +----- + +A date can be a UNIX timestamp as seconds since the Epoch (1970-01-01 +00:00 UTC), or of the form ````. diff --git a/doc/manual/source/man/ldns-update.rst b/doc/manual/source/man/ldns-update.rst new file mode 100644 index 0000000..9d25ced --- /dev/null +++ b/doc/manual/source/man/ldns-update.rst @@ -0,0 +1,46 @@ +ldns-update +=============== + +Synopsis +-------- + +:program:`ldns-update` ```` ``[ZONE]`` ```` +``[ ]`` + +Description +----------- + +**ldns-update** sends a dynamic update packet to update an IP (or delete all +existing IPs) for a domain name. + +Options +------- + +.. option:: + + The domain name to update the IP address of + +.. option:: + + The zone to send the update to (if omitted, derived from SOA record) + +.. option:: + + The IP to update the domain with (``none`` to remove any existing IPs) + +.. option:: + + TSIG key name + +.. option:: + + TSIG algorithm (e.g. "hmac-sha256") + +.. option:: + + Base64 encoded TSIG key data. + +.. option:: -h, --help + + Print the help text (short summary with ``-h``, long help with + ``--help``).