added tests for NSEC record creation

This commit is contained in:
Benjamin Fry 2016-05-22 23:58:29 -07:00
parent 721486ea79
commit 8ff4935079
5 changed files with 107 additions and 109 deletions

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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());

View File

@ -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());