ns: sign zone file
This commit is contained in:
parent
a527ed6218
commit
037cf4f698
@ -1,6 +1,6 @@
|
|||||||
FROM ubuntu:22.04
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y dnsutils unbound nsd iputils-ping tshark vim
|
apt-get install -y dnsutils unbound nsd iputils-ping tshark vim ldnsutils
|
||||||
|
|
||||||
COPY ./files/etc/unbound/unbound.conf /etc/unbound/unbound.conf
|
COPY ./files/etc/unbound/unbound.conf /etc/unbound/unbound.conf
|
||||||
|
@ -19,6 +19,7 @@ impl Client {
|
|||||||
pub fn dig(
|
pub fn dig(
|
||||||
&self,
|
&self,
|
||||||
recurse: Recurse,
|
recurse: Recurse,
|
||||||
|
dnssec: Dnssec,
|
||||||
server: Ipv4Addr,
|
server: Ipv4Addr,
|
||||||
record_type: RecordType,
|
record_type: RecordType,
|
||||||
fqdn: &FQDN<'_>,
|
fqdn: &FQDN<'_>,
|
||||||
@ -26,6 +27,7 @@ impl Client {
|
|||||||
let output = self.inner.stdout(&[
|
let output = self.inner.stdout(&[
|
||||||
"dig",
|
"dig",
|
||||||
recurse.as_str(),
|
recurse.as_str(),
|
||||||
|
dnssec.as_str(),
|
||||||
&format!("@{server}"),
|
&format!("@{server}"),
|
||||||
record_type.as_str(),
|
record_type.as_str(),
|
||||||
fqdn.as_str(),
|
fqdn.as_str(),
|
||||||
@ -35,6 +37,21 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum Dnssec {
|
||||||
|
Yes,
|
||||||
|
No,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dnssec {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Yes => "+dnssec",
|
||||||
|
Self::No => "+nodnssec",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum Recurse {
|
pub enum Recurse {
|
||||||
Yes,
|
Yes,
|
||||||
@ -44,8 +61,8 @@ pub enum Recurse {
|
|||||||
impl Recurse {
|
impl Recurse {
|
||||||
fn as_str(&self) -> &'static str {
|
fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Recurse::Yes => "+recurse",
|
Self::Yes => "+recurse",
|
||||||
Recurse::No => "+norecurse",
|
Self::No => "+norecurse",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,12 +154,12 @@ impl TryFrom<process::Output> for Output {
|
|||||||
|
|
||||||
fn try_from(output: process::Output) -> Result<Self> {
|
fn try_from(output: process::Output) -> Result<Self> {
|
||||||
let mut stderr = String::from_utf8(output.stderr)?;
|
let mut stderr = String::from_utf8(output.stderr)?;
|
||||||
while stderr.ends_with('\n') {
|
while stderr.ends_with(|c| matches!(c, '\n' | '\r')) {
|
||||||
stderr.pop();
|
stderr.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut stdout = String::from_utf8(output.stdout)?;
|
let mut stdout = String::from_utf8(output.stdout)?;
|
||||||
while stdout.ends_with('\n') {
|
while stdout.ends_with(|c| matches!(c, '\n' | '\r')) {
|
||||||
stdout.pop();
|
stdout.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
|
use core::array;
|
||||||
|
use core::str::FromStr;
|
||||||
use core::sync::atomic::{self, AtomicUsize};
|
use core::sync::atomic::{self, AtomicUsize};
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::process::Child;
|
use std::process::Child;
|
||||||
|
|
||||||
use crate::container::Container;
|
use crate::container::Container;
|
||||||
use crate::zone_file::{self, SoaSettings, ZoneFile};
|
use crate::zone_file::{self, SoaSettings, ZoneFile};
|
||||||
use crate::{Result, FQDN};
|
use crate::{Error, Result, FQDN};
|
||||||
|
|
||||||
pub struct NameServer<'a, State> {
|
pub struct NameServer<'a, State> {
|
||||||
container: Container,
|
container: Container,
|
||||||
zone_file: ZoneFile<'a>,
|
zone_file: ZoneFile<'a>,
|
||||||
_state: State,
|
state: State,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> NameServer<'a, Stopped> {
|
impl<'a> NameServer<'a, Stopped> {
|
||||||
@ -45,7 +47,7 @@ impl<'a> NameServer<'a, Stopped> {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
container: Container::run()?,
|
container: Container::run()?,
|
||||||
zone_file,
|
zone_file,
|
||||||
_state: Stopped,
|
state: Stopped,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,12 +68,66 @@ impl<'a> NameServer<'a, Stopped> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Freezes and signs the name server's zone file
|
||||||
|
pub fn sign(self) -> Result<NameServer<'a, Signed>> {
|
||||||
|
// TODO do we want to make these settings configurable?
|
||||||
|
const ZSK_BITS: usize = 1024;
|
||||||
|
const KSK_BITS: usize = 2048;
|
||||||
|
const ALGORITHM: &str = "RSASHA1-NSEC3-SHA1";
|
||||||
|
|
||||||
|
let Self {
|
||||||
|
container,
|
||||||
|
zone_file,
|
||||||
|
state: _,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
container.status_ok(&["mkdir", "-p", ZONES_DIR])?;
|
||||||
|
container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?;
|
||||||
|
|
||||||
|
let zone = &zone_file.origin;
|
||||||
|
|
||||||
|
let zsk_keygen =
|
||||||
|
format!("cd {ZONES_DIR} && ldns-keygen -a {ALGORITHM} -b {ZSK_BITS} {zone}");
|
||||||
|
let zsk_filename = container.stdout(&["sh", "-c", &zsk_keygen])?;
|
||||||
|
let zsk_path = format!("{ZONES_DIR}/{zsk_filename}.key");
|
||||||
|
let zsk: Key = container.stdout(&["cat", &zsk_path])?.parse()?;
|
||||||
|
|
||||||
|
let ksk_keygen =
|
||||||
|
format!("cd {ZONES_DIR} && ldns-keygen -k -a {ALGORITHM} -b {KSK_BITS} {zone}");
|
||||||
|
let ksk_filename = container.stdout(&["sh", "-c", &ksk_keygen])?;
|
||||||
|
let ksk_path = format!("{ZONES_DIR}/{ksk_filename}.key");
|
||||||
|
let ksk: Key = container.stdout(&["cat", &ksk_path])?.parse()?;
|
||||||
|
|
||||||
|
// -n = use NSEC3 instead of NSEC
|
||||||
|
// -p = set the opt-out flag on all nsec3 rrs
|
||||||
|
let signzone = format!(
|
||||||
|
"cd {ZONES_DIR} && ldns-signzone -n -p {ZONE_FILENAME} {zsk_filename} {ksk_filename}"
|
||||||
|
);
|
||||||
|
container.status_ok(&["sh", "-c", &signzone])?;
|
||||||
|
|
||||||
|
// we have an in-memory representation of the zone file so we just delete the on-disk version
|
||||||
|
let zone_file_path = zone_file_path();
|
||||||
|
container.status_ok(&["mv", &format!("{zone_file_path}.signed"), &zone_file_path])?;
|
||||||
|
|
||||||
|
let signed_zone_file = container.stdout(&["cat", &zone_file_path])?;
|
||||||
|
|
||||||
|
Ok(NameServer {
|
||||||
|
container,
|
||||||
|
zone_file,
|
||||||
|
state: Signed {
|
||||||
|
zsk,
|
||||||
|
ksk,
|
||||||
|
signed_zone_file,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Moves the server to the "Start" state where it can answer client queries
|
/// Moves the server to the "Start" state where it can answer client queries
|
||||||
pub fn start(self) -> Result<NameServer<'a, Running>> {
|
pub fn start(self) -> Result<NameServer<'a, Running>> {
|
||||||
let Self {
|
let Self {
|
||||||
container,
|
container,
|
||||||
zone_file,
|
zone_file,
|
||||||
_state: _,
|
state: _,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
// for PID file
|
// for PID file
|
||||||
@ -79,24 +135,123 @@ impl<'a> NameServer<'a, Stopped> {
|
|||||||
|
|
||||||
container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?;
|
container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?;
|
||||||
|
|
||||||
container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?;
|
container.status_ok(&["mkdir", "-p", ZONES_DIR])?;
|
||||||
container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?;
|
container.cp(&zone_file_path(), &zone_file.to_string())?;
|
||||||
|
|
||||||
let child = container.spawn(&["nsd", "-d"])?;
|
let child = container.spawn(&["nsd", "-d"])?;
|
||||||
|
|
||||||
Ok(NameServer {
|
Ok(NameServer {
|
||||||
container,
|
container,
|
||||||
zone_file,
|
zone_file,
|
||||||
_state: Running { child },
|
state: Running { child },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Key {
|
||||||
|
pub bits: u16,
|
||||||
|
pub encoded: String,
|
||||||
|
pub id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Key {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self> {
|
||||||
|
let (before, after) = input.split_once(';').ok_or("comment was not found")?;
|
||||||
|
let mut columns = before.split_whitespace();
|
||||||
|
|
||||||
|
let [Some(_zone), Some(class), Some(record_type), Some(_flags), Some(_protocol), Some(_algorithm), Some(encoded), None] =
|
||||||
|
array::from_fn(|_| columns.next())
|
||||||
|
else {
|
||||||
|
return Err("expected 7 columns".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
if record_type != "DNSKEY" {
|
||||||
|
return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if class != "IN" {
|
||||||
|
return Err(format!("unknown class: {class}").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// {id = 24975 (zsk), size = 1024b}
|
||||||
|
let error = "invalid comment syntax";
|
||||||
|
let (id_expr, size_expr) = after.split_once(',').ok_or(error)?;
|
||||||
|
|
||||||
|
// {id = 24975 (zsk)
|
||||||
|
let (id_lhs, id_rhs) = id_expr.split_once('=').ok_or(error)?;
|
||||||
|
if id_lhs.trim() != "{id" {
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 24975 (zsk)
|
||||||
|
let (id, _key_type) = id_rhs.trim().split_once(' ').ok_or(error)?;
|
||||||
|
|
||||||
|
// size = 1024b}
|
||||||
|
let (size_lhs, size_rhs) = size_expr.split_once('=').ok_or(error)?;
|
||||||
|
if size_lhs.trim() != "size" {
|
||||||
|
return Err(error.into());
|
||||||
|
}
|
||||||
|
let bits = size_rhs.trim().strip_suffix("b}").ok_or(error)?.parse()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
bits,
|
||||||
|
encoded: encoded.to_string(),
|
||||||
|
id: id.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZONES_DIR: &str = "/etc/nsd/zones";
|
||||||
|
const ZONE_FILENAME: &str = "main.zone";
|
||||||
|
|
||||||
|
fn zone_file_path() -> String {
|
||||||
|
format!("{ZONES_DIR}/{ZONE_FILENAME}")
|
||||||
|
}
|
||||||
|
|
||||||
fn ns_count() -> usize {
|
fn ns_count() -> usize {
|
||||||
static COUNT: AtomicUsize = AtomicUsize::new(0);
|
static COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||||
COUNT.fetch_add(1, atomic::Ordering::Relaxed)
|
COUNT.fetch_add(1, atomic::Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> NameServer<'a, Signed> {
|
||||||
|
/// Moves the server to the "Start" state where it can answer client queries
|
||||||
|
pub fn start(self) -> Result<NameServer<'a, Running>> {
|
||||||
|
let Self {
|
||||||
|
container,
|
||||||
|
zone_file,
|
||||||
|
state: _,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
// for PID file
|
||||||
|
container.status_ok(&["mkdir", "-p", "/run/nsd/"])?;
|
||||||
|
|
||||||
|
container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?;
|
||||||
|
|
||||||
|
let child = container.spawn(&["nsd", "-d"])?;
|
||||||
|
|
||||||
|
Ok(NameServer {
|
||||||
|
container,
|
||||||
|
zone_file,
|
||||||
|
state: Running { child },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_signing_key(&self) -> &Key {
|
||||||
|
&self.state.ksk
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zone_signing_key(&self) -> &Key {
|
||||||
|
&self.state.zsk
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signed_zone_file(&self) -> &str {
|
||||||
|
&self.state.signed_zone_file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, S> NameServer<'a, S> {
|
impl<'a, S> NameServer<'a, S> {
|
||||||
pub fn ipv4_addr(&self) -> Ipv4Addr {
|
pub fn ipv4_addr(&self) -> Ipv4Addr {
|
||||||
self.container.ipv4_addr()
|
self.container.ipv4_addr()
|
||||||
@ -117,6 +272,12 @@ impl<'a, S> NameServer<'a, S> {
|
|||||||
|
|
||||||
pub struct Stopped;
|
pub struct Stopped;
|
||||||
|
|
||||||
|
pub struct Signed {
|
||||||
|
zsk: Key,
|
||||||
|
ksk: Key,
|
||||||
|
signed_zone_file: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Running {
|
pub struct Running {
|
||||||
child: Child,
|
child: Child,
|
||||||
}
|
}
|
||||||
@ -144,7 +305,7 @@ fn nsd_conf(fqdn: &FQDN) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::client::{Client, Recurse};
|
use crate::client::{Client, Dnssec, Recurse};
|
||||||
use crate::record::RecordType;
|
use crate::record::RecordType;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -155,7 +316,13 @@ mod tests {
|
|||||||
let ip_addr = tld_ns.ipv4_addr();
|
let ip_addr = tld_ns.ipv4_addr();
|
||||||
|
|
||||||
let client = Client::new()?;
|
let client = Client::new()?;
|
||||||
let output = client.dig(Recurse::No, ip_addr, RecordType::SOA, &FQDN::COM)?;
|
let output = client.dig(
|
||||||
|
Recurse::No,
|
||||||
|
Dnssec::No,
|
||||||
|
ip_addr,
|
||||||
|
RecordType::SOA,
|
||||||
|
&FQDN::COM,
|
||||||
|
)?;
|
||||||
|
|
||||||
assert!(output.status.is_noerror());
|
assert!(output.status.is_noerror());
|
||||||
|
|
||||||
@ -178,10 +345,56 @@ mod tests {
|
|||||||
let ipv4_addr = root_ns.ipv4_addr();
|
let ipv4_addr = root_ns.ipv4_addr();
|
||||||
|
|
||||||
let client = Client::new()?;
|
let client = Client::new()?;
|
||||||
let output = client.dig(Recurse::No, ipv4_addr, RecordType::NS, &FQDN::COM)?;
|
let output = client.dig(
|
||||||
|
Recurse::No,
|
||||||
|
Dnssec::No,
|
||||||
|
ipv4_addr,
|
||||||
|
RecordType::NS,
|
||||||
|
&FQDN::COM,
|
||||||
|
)?;
|
||||||
|
|
||||||
assert!(output.status.is_noerror());
|
assert!(output.status.is_noerror());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "FIXME need to parse RRSIG record in dig's output"]
|
||||||
|
fn signed() -> Result<()> {
|
||||||
|
let tld_ns = NameServer::new(FQDN::ROOT)?.sign()?;
|
||||||
|
|
||||||
|
eprintln!("KSK: {:?}", tld_ns.key_signing_key());
|
||||||
|
eprintln!("ZSK: {:?}", tld_ns.zone_signing_key());
|
||||||
|
eprintln!("root.zone.signed:\n{}", tld_ns.signed_zone_file());
|
||||||
|
|
||||||
|
let tld_ns = tld_ns.start()?;
|
||||||
|
|
||||||
|
let ipv4_addr = tld_ns.ipv4_addr();
|
||||||
|
|
||||||
|
let client = Client::new()?;
|
||||||
|
let output = client.dig(
|
||||||
|
Recurse::No,
|
||||||
|
Dnssec::Yes,
|
||||||
|
ipv4_addr,
|
||||||
|
RecordType::SOA,
|
||||||
|
&FQDN::ROOT,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert!(output.status.is_noerror());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_parse_ldns_keygen_output() -> Result<()> {
|
||||||
|
let input = "example.com. IN DNSKEY 256 3 7 AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb ;{id = 24975 (zsk), size = 1024b}";
|
||||||
|
|
||||||
|
let key: Key = input.parse()?;
|
||||||
|
|
||||||
|
assert_eq!(1024, key.bits);
|
||||||
|
let expected = "AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb";
|
||||||
|
assert_eq!(expected, key.encoded);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ impl Drop for RecursiveResolver {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{Client, Recurse},
|
client::{Client, Dnssec, Recurse},
|
||||||
name_server::NameServer,
|
name_server::NameServer,
|
||||||
record::RecordType,
|
record::RecordType,
|
||||||
FQDN,
|
FQDN,
|
||||||
@ -85,7 +85,13 @@ mod tests {
|
|||||||
let resolver_ip_addr = resolver.ipv4_addr();
|
let resolver_ip_addr = resolver.ipv4_addr();
|
||||||
|
|
||||||
let client = Client::new()?;
|
let client = Client::new()?;
|
||||||
let output = client.dig(Recurse::Yes, resolver_ip_addr, RecordType::A, &needle)?;
|
let output = client.dig(
|
||||||
|
Recurse::Yes,
|
||||||
|
Dnssec::No,
|
||||||
|
resolver_ip_addr,
|
||||||
|
RecordType::A,
|
||||||
|
&needle,
|
||||||
|
)?;
|
||||||
|
|
||||||
assert!(output.status.is_noerror());
|
assert!(output.status.is_noerror());
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user