added tests for NSEC record creation
This commit is contained in:
parent
721486ea79
commit
8ff4935079
@ -8,12 +8,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Authority zone signing now complete, still need to load/save private keys
|
||||
- DNSKEYs auto inserted for added private keys
|
||||
- New mocked network client tests, to verify zone signing
|
||||
- NSEC record creation for zone
|
||||
- NSEC record creation for zone, with tests
|
||||
|
||||
### Fixed
|
||||
- Added loop on TCP accept requests
|
||||
- Added loop on UDP reads
|
||||
- Upgraded to mio 0.5.1 for some bug fixes
|
||||
- Not returning RRSIGs with SOA records on authoritative answers
|
||||
|
||||
### Changed
|
||||
- Internal representation of record sets now a full data structure
|
||||
|
@ -101,7 +101,7 @@ impl Authority {
|
||||
|
||||
pub fn add_secure_key(&mut self, signer: Signer) {
|
||||
// also add the key to the zone
|
||||
let zone_ttl = self.get_soa(false).map_or(0, |soa| if let &RData::SOA(ref soa_rdata) = soa.get_rdata() { soa_rdata.get_minimum() } else { 0 });
|
||||
let zone_ttl = self.get_minimum_ttl();
|
||||
let dnskey = signer.to_dnskey(self.origin.clone(), zone_ttl);
|
||||
|
||||
// TODO: also generate the CDS and CDNSKEY
|
||||
@ -128,13 +128,28 @@ impl Authority {
|
||||
self.zone_type
|
||||
}
|
||||
|
||||
pub fn get_soa(&self, is_secure: bool) -> Option<&Record> {
|
||||
/// Returns the SOA of the authority.
|
||||
///
|
||||
/// *Note* This will only return the SOA, if this is fullfilling a request, a standard lookup
|
||||
/// should be used, see `get_soa_secure()`, which will optionally return RRSIGs.
|
||||
pub fn get_soa(&self) -> Option<&Record> {
|
||||
// SOA should be origin|SOA
|
||||
self.lookup(&self.origin, RecordType::SOA, is_secure).first().map(|v| *v)
|
||||
self.lookup(&self.origin, RecordType::SOA, false).first().map(|v| *v)
|
||||
}
|
||||
|
||||
/// Returns the SOA record
|
||||
///
|
||||
///
|
||||
pub fn get_soa_secure(&self, is_secure: bool) -> Vec<&Record> {
|
||||
self.lookup(&self.origin, RecordType::SOA, is_secure)
|
||||
}
|
||||
|
||||
pub fn get_minimum_ttl(&self) -> u32 {
|
||||
self.get_soa().map_or(0, |soa| if let &RData::SOA(ref rdata) = soa.get_rdata() { rdata.get_minimum() } else { 0 })
|
||||
}
|
||||
|
||||
fn get_serial(&self) -> u32 {
|
||||
let soa = if let Some(ref soa_record) = self.get_soa(false) {
|
||||
let soa = if let Some(ref soa_record) = self.get_soa() {
|
||||
soa_record.clone()
|
||||
} else {
|
||||
warn!("no soa record found for zone: {}", self.origin);
|
||||
@ -149,7 +164,7 @@ impl Authority {
|
||||
}
|
||||
|
||||
fn increment_soa_serial(&mut self) -> u32 {
|
||||
let mut soa = if let Some(ref mut soa_record) = self.get_soa(false) {
|
||||
let mut soa = if let Some(ref mut soa_record) = self.get_soa() {
|
||||
soa_record.clone()
|
||||
} else {
|
||||
warn!("no soa record found for zone: {}", self.origin);
|
||||
@ -727,7 +742,7 @@ impl Authority {
|
||||
let mut query_result: Vec<_> = self.lookup(query.get_name(), record_type, is_secure);
|
||||
|
||||
if RecordType::AXFR == record_type {
|
||||
if let Some(soa) = self.get_soa(false) {
|
||||
if let Some(soa) = self.get_soa() {
|
||||
let mut xfr: Vec<&Record> = query_result;
|
||||
// TODO: probably make Records Rc or Arc, to remove the clone
|
||||
xfr.insert(0, soa);
|
||||
@ -776,18 +791,21 @@ impl Authority {
|
||||
}
|
||||
};
|
||||
|
||||
if is_secure && result.is_empty() {
|
||||
// get NSEC records
|
||||
result
|
||||
}
|
||||
|
||||
// this search is predicated on the fact that the BTreeMap returns the properly ordered records
|
||||
// if there are none, then
|
||||
self.records.values().filter(|rr_set| rr_set.get_record_type() == RecordType::NSEC)
|
||||
.skip_while(|rr_set| name < rr_set.get_name())
|
||||
.next()
|
||||
.map_or(vec![], |rr_set| rr_set.get_records(is_secure).into_iter().collect())
|
||||
} else {
|
||||
result
|
||||
}
|
||||
/// Return the NSEC records based on the given name
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - given this name (i.e. the lookup name), return the NSEC record that is less than
|
||||
/// this
|
||||
/// * `is_secure` - if true then it will return RRSIG records as well
|
||||
pub fn get_nsec_records(&self, name: &Name, is_secure: bool) -> Vec<&Record> {
|
||||
self.records.values().filter(|rr_set| rr_set.get_record_type() == RecordType::NSEC)
|
||||
.skip_while(|rr_set| name < rr_set.get_name())
|
||||
.next()
|
||||
.map_or(vec![], |rr_set| rr_set.get_records(is_secure).into_iter().collect())
|
||||
}
|
||||
|
||||
/// (Re)generates the nsec records, increments the serial number nad signs the zone
|
||||
@ -821,7 +839,7 @@ impl Authority {
|
||||
}
|
||||
|
||||
// now go through and generate the nsec records
|
||||
let ttl = self.get_soa(false).map_or(0, |soa| if let &RData::SOA(ref rdata) = soa.get_rdata() { rdata.get_minimum() } else { 0 });
|
||||
let ttl = self.get_minimum_ttl();
|
||||
let serial = self.get_serial();
|
||||
let mut records: Vec<Record> = vec![];
|
||||
|
||||
@ -864,9 +882,7 @@ impl Authority {
|
||||
fn sign_zone(&mut self) {
|
||||
debug!("signing zone: {}", self.origin);
|
||||
let now = UTC::now().timestamp() as u32;
|
||||
let zone_ttl = self.get_soa(false)
|
||||
.map_or(0, |soa_rec| if let &RData::SOA(ref soa) = soa_rec.get_rdata() { soa.get_minimum() }
|
||||
else { panic!("SOA has no RDATA: {}", self.origin) } );
|
||||
let zone_ttl = self.get_minimum_ttl();
|
||||
|
||||
for rr_set in self.records.iter_mut().filter_map(|(_, rr_set)| {
|
||||
// do not sign zone DNSKEY's that's the job of the parent zone
|
||||
@ -1044,8 +1060,8 @@ pub mod authority_tests {
|
||||
fn test_authority() {
|
||||
let authority: Authority = create_example();
|
||||
|
||||
assert!(authority.get_soa(false).is_some());
|
||||
assert_eq!(authority.get_soa(false).unwrap().get_dns_class(), DNSClass::IN);
|
||||
assert!(authority.get_soa().is_some());
|
||||
assert_eq!(authority.get_soa().unwrap().get_dns_class(), DNSClass::IN);
|
||||
|
||||
assert!(!authority.lookup(authority.get_origin(), RecordType::NS, false).is_empty());
|
||||
|
||||
@ -1270,4 +1286,16 @@ pub mod authority_tests {
|
||||
} ), "record type not covered: {:?}", record);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_nsec() {
|
||||
let name = Name::new().label("zzz").label("example").label("com");
|
||||
let authority: Authority = create_secure_example();
|
||||
|
||||
let results = authority.get_nsec_records(&name, true);
|
||||
|
||||
for record in results.iter() {
|
||||
assert!(record.get_name() < &name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use ::rr::{Record, Name, RecordType};
|
||||
use ::rr::{Name, RecordType};
|
||||
use ::authority::{Authority, ZoneType};
|
||||
use ::op::*;
|
||||
|
||||
@ -33,6 +33,11 @@ impl Catalog {
|
||||
Catalog{ authorities: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Determine's what needs to happen given the type of request, i.e. Query or Update.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - the requested action to perform.
|
||||
pub fn handle_request(&self, request: &Message) -> Message {
|
||||
info!("id: {} type: {:?} op_code: {:?}", request.get_id(), request.get_message_type(), request.get_op_code());
|
||||
debug!("request: {:?}", request);
|
||||
@ -212,6 +217,11 @@ impl Catalog {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given the requested query, lookup and return any matching results.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - the query message.
|
||||
pub fn lookup(&self, request: &Message) -> Message {
|
||||
let mut response: Message = Message::new();
|
||||
response.id(request.get_id());
|
||||
@ -226,7 +236,7 @@ impl Catalog {
|
||||
debug!("found authority: {:?}", authority.get_origin());
|
||||
let is_dnssec = request.get_edns().map_or(false, |edns|edns.is_dnssec_ok());
|
||||
|
||||
let records = Self::search(&*authority, query, is_dnssec);
|
||||
let records = authority.search(query, is_dnssec);
|
||||
if !records.is_empty() {
|
||||
response.response_code(ResponseCode::NoError);
|
||||
response.authoritative(true);
|
||||
@ -239,13 +249,18 @@ impl Catalog {
|
||||
response.add_all_name_servers(&ns);
|
||||
}
|
||||
} else {
|
||||
if is_dnssec {
|
||||
// get NSEC records
|
||||
response.add_all_name_servers(&authority.get_nsec_records(query.get_name(), is_dnssec));
|
||||
}
|
||||
|
||||
// in the not found case it's standard to return the SOA in the authority section
|
||||
response.response_code(ResponseCode::NXDomain);
|
||||
|
||||
let soa = authority.get_soa(is_dnssec);
|
||||
if soa.is_none() { warn!("there is no SOA record for: {:?}", authority.get_origin()); }
|
||||
let soa = authority.get_soa_secure(is_dnssec);
|
||||
if soa.is_empty() { warn!("there is no SOA record for: {:?}", authority.get_origin()); }
|
||||
else {
|
||||
response.add_name_server(soa.unwrap().clone());
|
||||
response.add_all_name_servers(&soa);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -259,40 +274,7 @@ impl Catalog {
|
||||
response
|
||||
}
|
||||
|
||||
// TODO: move this to Authority
|
||||
pub fn search<'a>(authority: &'a Authority, query: &Query, is_secure: bool) -> Vec<&'a Record> {
|
||||
let record_type: RecordType = query.get_query_type();
|
||||
|
||||
// if this is an AXFR zone transfer, verify that this is either the slave or master
|
||||
// for AXFR the first and last record must be the SOA
|
||||
if RecordType::AXFR == record_type {
|
||||
match authority.get_zone_type() {
|
||||
ZoneType::Master | ZoneType::Slave => (),
|
||||
// TODO Forward?
|
||||
_ => return vec![], // TODO this sould be an error.
|
||||
}
|
||||
}
|
||||
|
||||
// it would be better to stream this back, rather than packaging everything up in an array
|
||||
// though for UDP it would still need to be bundled
|
||||
let mut query_result: Vec<_> = authority.lookup(query.get_name(), record_type, is_secure);
|
||||
|
||||
if RecordType::AXFR == record_type {
|
||||
if let Some(soa) = authority.get_soa(false) {
|
||||
let mut xfr: Vec<&Record> = query_result;
|
||||
// TODO: probably make Records Rc or Arc, to remove the clone
|
||||
xfr.insert(0, soa);
|
||||
xfr.push(soa);
|
||||
|
||||
query_result = xfr;
|
||||
} else {
|
||||
return vec![]; // TODO is this an error?
|
||||
}
|
||||
}
|
||||
|
||||
query_result
|
||||
}
|
||||
|
||||
/// recursively searches the catalog for a matching auhtority.
|
||||
fn find_auth_recurse(&self, name: &Name) -> Option<&RwLock<Authority>> {
|
||||
let authority = self.authorities.get(name);
|
||||
if authority.is_some() { return authority; }
|
||||
@ -320,43 +302,6 @@ mod catalog_tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_catalog_search() {
|
||||
let example = create_example();
|
||||
let origin = example.get_origin().clone();
|
||||
|
||||
let mut query: Query = Query::new();
|
||||
query.name(origin.clone());
|
||||
|
||||
let result = Catalog::search(&example, &query, false);
|
||||
if !result.is_empty() {
|
||||
assert_eq!(result.first().unwrap().get_rr_type(), RecordType::A);
|
||||
assert_eq!(result.first().unwrap().get_dns_class(), DNSClass::IN);
|
||||
assert_eq!(result.first().unwrap().get_rdata(), &RData::A(Ipv4Addr::new(93,184,216,34)));
|
||||
} else {
|
||||
panic!("expected a result");
|
||||
}
|
||||
}
|
||||
|
||||
/// this is a litte more interesting b/c it requires a recursive lookup for the origin
|
||||
#[test]
|
||||
fn test_catalog_search_www() {
|
||||
let example = create_example();
|
||||
let www_name = Name::parse("www.example.com.", None).unwrap();
|
||||
|
||||
let mut query: Query = Query::new();
|
||||
query.name(www_name.clone());
|
||||
|
||||
let result = Catalog::search(&example, &query, false);
|
||||
if !result.is_empty() {
|
||||
assert_eq!(result.first().unwrap().get_rr_type(), RecordType::A);
|
||||
assert_eq!(result.first().unwrap().get_dns_class(), DNSClass::IN);
|
||||
assert_eq!(result.first().unwrap().get_rdata(), &RData::A(Ipv4Addr::new(93,184,216,34)));
|
||||
} else {
|
||||
panic!("expected a result");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_test() -> Authority {
|
||||
let origin: Name = Name::parse("test.com.", None).unwrap();
|
||||
let mut records: Authority = Authority::new(origin.clone(), BTreeMap::new(), ZoneType::Master, false);
|
||||
|
@ -121,7 +121,7 @@ impl<C: ClientConnection> Client<C> {
|
||||
let proof = try!(self.recursive_query_verify(&name, rrset, rrsigs.clone(), rrset_type, query_class));
|
||||
|
||||
// TODO return this, also make a prettier print
|
||||
debug!("proved existance through: {:?}", proof);
|
||||
debug!("proved existance through for {}:{:?}: {:?}", name, rrset_type, proof);
|
||||
}
|
||||
|
||||
// at this point all records are validated, but if there are NSEC records present,
|
||||
@ -180,6 +180,8 @@ impl<C: ClientConnection> Client<C> {
|
||||
for rrsig in rrsigs.iter().filter(|rr| rr.get_name() == name) {
|
||||
// TODO: need to verify inception and experation...
|
||||
if let &RData::SIG(ref sig) = rrsig.get_rdata() {
|
||||
if sig.get_type_covered() != query_type { continue } // wrong RRSIG, probably another record
|
||||
|
||||
// get DNSKEY from signer_name
|
||||
let key_response = try!(self.inner_query(sig.get_signer_name(), query_class, RecordType::DNSKEY, true));
|
||||
let key_rrset: Vec<&Record> = key_response.get_answers().iter().filter(|rr| rr.get_rr_type() == RecordType::DNSKEY).collect();
|
||||
@ -211,7 +213,8 @@ impl<C: ClientConnection> Client<C> {
|
||||
return Ok(proof);
|
||||
}
|
||||
} else {
|
||||
debug!("could not verify: {} with: {}", name, rrsig.get_name());
|
||||
debug!("could not verify: {}:{:?} with: {}:{:?}", name, query_type, rrsig.get_name(),
|
||||
if let &RData::SIG(ref sig) = rrsig.get_rdata() { sig.get_type_covered() } else { RecordType::NULL });
|
||||
}
|
||||
} else {
|
||||
panic!("this should be a DNSKEY")
|
||||
@ -548,7 +551,6 @@ mod test {
|
||||
use ::authority::Catalog;
|
||||
use ::authority::authority_tests::{create_example, create_secure_example};
|
||||
use ::client::{Client, ClientConnection, TestClientConnection};
|
||||
#[cfg(feature = "ftest")]
|
||||
use ::op::ResponseCode;
|
||||
use ::rr::{DNSClass, RecordType, domain, RData};
|
||||
use ::rr::dnssec::TrustAnchor;
|
||||
@ -677,12 +679,35 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nsec_query_example_nonet() {
|
||||
use ::client::client_connection::test::TestClientConnection;
|
||||
|
||||
let authority = create_secure_example();
|
||||
|
||||
let public_key = {
|
||||
let signers = authority.get_secure_keys();
|
||||
signers.first().expect("expected a key in the authority").get_public_key()
|
||||
};
|
||||
|
||||
let mut catalog = Catalog::new();
|
||||
catalog.upsert(authority.get_origin().clone(), authority);
|
||||
|
||||
let mut trust_anchor = TrustAnchor::new();
|
||||
trust_anchor.insert_trust_anchor(public_key);
|
||||
|
||||
let client = Client::with_trust_anchor(TestClientConnection::new(&catalog), trust_anchor);
|
||||
|
||||
test_nsec_query_example(client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "ftest")]
|
||||
fn test_nsec_query_example_udp() {
|
||||
let addr: SocketAddr = ("8.8.8.8",53).to_socket_addrs().unwrap().next().unwrap();
|
||||
let conn = UdpClientConnection::new(addr).unwrap();
|
||||
test_nsec_query_example(conn);
|
||||
let client = Client::new(conn);
|
||||
test_nsec_query_example(client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -690,15 +715,14 @@ mod test {
|
||||
fn test_nsec_query_example_tcp() {
|
||||
let addr: SocketAddr = ("8.8.8.8",53).to_socket_addrs().unwrap().next().unwrap();
|
||||
let conn = TcpClientConnection::new(addr).unwrap();
|
||||
test_nsec_query_example(conn);
|
||||
let client = Client::new(conn);
|
||||
test_nsec_query_example(client);
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "ftest")]
|
||||
fn test_nsec_query_example<C: ClientConnection>(conn: C) {
|
||||
fn test_nsec_query_example<C: ClientConnection>(client: Client<C>) {
|
||||
let name = domain::Name::with_labels(vec!["none".to_string(), "example".to_string(), "com".to_string()]);
|
||||
let client = Client::new(conn);
|
||||
|
||||
let response = client.secure_query(&name, DNSClass::IN, RecordType::A);
|
||||
assert!(response.is_ok(), "query failed: {}", response.unwrap_err());
|
||||
|
@ -46,7 +46,7 @@ VENERA A 10.1.0.52
|
||||
// not validating everything, just one of each...
|
||||
|
||||
// SOA
|
||||
let soa_record = authority.get_soa(false).unwrap();
|
||||
let soa_record = authority.get_soa().unwrap();
|
||||
assert_eq!(RecordType::SOA, soa_record.get_rr_type());
|
||||
assert_eq!(&Name::new().label("isi").label("edu"), soa_record.get_name()); // i.e. the origin or domain
|
||||
assert_eq!(3600000, soa_record.get_ttl());
|
||||
|
Loading…
Reference in New Issue
Block a user