nsec record creation

This commit is contained in:
Benjamin Fry 2016-05-21 00:53:11 -07:00
parent d4f79c9007
commit 18eb2dc707
5 changed files with 265 additions and 183 deletions

View File

@ -8,6 +8,7 @@ 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
### Fixed
- Added loop on TCP accept requests

View File

@ -13,14 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::cmp::Ordering;
use chrono::offset::utc::UTC;
use ::authority::{UpdateResult, ZoneType, RRSet};
use ::op::{UpdateMessage, ResponseCode};
use ::rr::{DNSClass, Name, RData, Record, RecordType};
use ::rr::rdata::SIG;
use ::rr::rdata::{NSEC, SIG};
use ::rr::dnssec::Signer;
/// Accessor key for RRSets in the Authority.
@ -43,11 +44,28 @@ impl RrKey {
}
}
impl PartialOrd for RrKey {
fn partial_cmp(&self, other: &RrKey) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RrKey {
fn cmp(&self, other: &Self) -> Ordering {
let order = self.name.cmp(&other.name);
if order == Ordering::Equal {
self.record_type.cmp(&other.record_type)
} else {
order
}
}
}
/// Authority is the storage method for all resource records
pub struct Authority {
origin: Name,
class: DNSClass,
records: HashMap<RrKey, RRSet>,
records: BTreeMap<RrKey, RRSet>,
zone_type: ZoneType,
allow_update: bool,
// Private key mapped to the Record of the DNSKey
@ -76,7 +94,7 @@ impl Authority {
/// # Return value
///
/// The new `Authority`.
pub fn new(origin: Name, records: HashMap<RrKey, RRSet>, zone_type: ZoneType, allow_update: bool) -> Authority {
pub fn new(origin: Name, records: BTreeMap<RrKey, RRSet>, zone_type: ZoneType, allow_update: bool) -> Authority {
Authority{ origin: origin, class: DNSClass::IN, records: records, zone_type: zone_type,
allow_update: allow_update, secure_keys: Vec::new() }
}
@ -588,6 +606,9 @@ impl Authority {
// update the serial...
if updated {
// TODO: only call nsec_zone after adds/deletes
// needs to be called before incrementing the soa serial, to make sur IXFR works properly
self.nsec_zone();
// need to resign any records at the current serial number and bump the number.
// first bump the serial number on the SOA, so that it is resigned with the new serial.
@ -724,13 +745,63 @@ impl Authority {
if result.is_empty() { None } else { Some(result) }
}
/// Creates all nsec records needed for the zone, replaces any existing records.
fn nsec_zone(&mut self) {
// only create nsec records for secure zones
if self.secure_keys.is_empty() { return }
debug!("generating nsec records: {}", self.origin);
// first remove all existing nsec records
let delete_keys: Vec<RrKey> = self.records.keys()
.filter(|k| k.record_type == RecordType::NSEC)
.cloned()
.collect();
for key in delete_keys {
self.records.remove(&key);
}
// 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 serial = self.get_serial();
let mut records: Vec<Record> = vec![];
{
let mut nsec_info: Option<(&Name, Vec<RecordType>)> = None;
for key in self.records.keys() {
match nsec_info {
None => nsec_info = Some((&key.name, vec![key.record_type])),
Some((name, ref mut vec)) if name == &key.name => { vec.push(key.record_type) },
Some((name, vec)) => {
// names aren't equal, create the NSEC record
let mut record = Record::with(name.clone(), RecordType::NSEC, ttl);
let rdata = NSEC::new(key.name.clone(), vec);
record.rdata(RData::NSEC(rdata));
records.push(record);
// new record...
nsec_info = Some((&key.name, vec![key.record_type]))
},
}
}
// the last record
if let Some((name, vec)) = nsec_info {
// names aren't equal, create the NSEC record
let mut record = Record::with(name.clone(), RecordType::NSEC, ttl);
let rdata = NSEC::new(self.get_origin().clone(), vec);
record.rdata(RData::NSEC(rdata));
records.push(record);
}
}
// insert all the nsec records
for record in records {
self.upsert(record, serial);
}
}
/// Signs any records in the zone that have serial numbers greater than or equal to `serial`
///
/// # Arguments
///
/// * `serial` - The serial to which the serial number of an `RRSet` should be compared. If the
/// record set serial is greater than or equal to the `serial`, then it will be
/// resigned.
pub fn sign_zone(&mut self) {
debug!("signing zone: {}", self.origin);
let now = UTC::now().timestamp() as u32;
@ -794,7 +865,7 @@ impl Authority {
#[cfg(test)]
pub mod authority_tests {
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::net::{Ipv4Addr,Ipv6Addr};
use ::authority::ZoneType;
@ -805,7 +876,7 @@ pub mod authority_tests {
pub fn create_example() -> Authority {
let origin: Name = Name::parse("example.com.", None,).unwrap();
let mut records: Authority = Authority::new(origin.clone(), HashMap::new(), ZoneType::Master, false);
let mut records: Authority = Authority::new(origin.clone(), BTreeMap::new(), ZoneType::Master, false);
// example.com. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082403 7200 3600 1209600 3600
records.upsert(Record::new().name(origin.clone()).ttl(3600).rr_type(RecordType::SOA).dns_class(DNSClass::IN).rdata(RData::SOA(SOA::new(Name::parse("sns.dns.icann.org.", None).unwrap(), Name::parse("noc.dns.icann.org.", None).unwrap(), 2015082403, 7200, 3600, 1209600, 3600 ))).clone(), 0);

View File

@ -122,47 +122,54 @@ impl Catalog {
self.authorities.insert(name, RwLock::new(authority));
}
/*
* RFC 2136 DNS Update April 1997
*
* 3.1 - Process Zone Section
*
* 3.1.1. The Zone Section is checked to see that there is exactly one
* RR therein and that the RR's ZTYPE is SOA, else signal FORMERR to the
* requestor. Next, the ZNAME and ZCLASS are checked to see if the zone
* so named is one of this server's authority zones, else signal NOTAUTH
* to the requestor. If the server is a zone slave, the request will be
* forwarded toward the primary master.
*
* 3.1.2 - Pseudocode For Zone Section Processing
*
* if (zcount != 1 || ztype != SOA)
* return (FORMERR)
* if (zone_type(zname, zclass) == SLAVE)
* return forward()
* if (zone_type(zname, zclass) == MASTER)
* return update()
* return (NOTAUTH)
*
* Sections 3.2 through 3.8 describe the primary master's behaviour,
* whereas Section 6 describes a forwarder's behaviour.
*
* 3.8 - Response
*
* At the end of UPDATE processing, a response code will be known. A
* response message is generated by copying the ID and Opcode fields
* from the request, and either copying the ZOCOUNT, PRCOUNT, UPCOUNT,
* and ADCOUNT fields and associated sections, or placing zeros (0) in
* the these "count" fields and not including any part of the original
* update. The QR bit is set to one (1), and the response is sent back
* to the requestor. If the requestor used UDP, then the response will
* be sent to the requestor's source UDP port. If the requestor used
* TCP, then the response will be sent back on the requestor's open TCP
* connection.
*/
/// Update the zone given the Update request.
///
/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997
///
/// ```text
/// 3.1 - Process Zone Section
///
/// 3.1.1. The Zone Section is checked to see that there is exactly one
/// RR therein and that the RR's ZTYPE is SOA, else signal FORMERR to the
/// requestor. Next, the ZNAME and ZCLASS are checked to see if the zone
/// so named is one of this server's authority zones, else signal NOTAUTH
/// to the requestor. If the server is a zone slave, the request will be
/// forwarded toward the primary master.
///
/// 3.1.2 - Pseudocode For Zone Section Processing
///
/// if (zcount != 1 || ztype != SOA)
/// return (FORMERR)
/// if (zone_type(zname, zclass) == SLAVE)
/// return forward()
/// if (zone_type(zname, zclass) == MASTER)
/// return update()
/// return (NOTAUTH)
///
/// Sections 3.2 through 3.8 describe the primary master's behaviour,
/// whereas Section 6 describes a forwarder's behaviour.
///
/// 3.8 - Response
///
/// At the end of UPDATE processing, a response code will be known. A
/// response message is generated by copying the ID and Opcode fields
/// from the request, and either copying the ZOCOUNT, PRCOUNT, UPCOUNT,
/// and ADCOUNT fields and associated sections, or placing zeros (0) in
/// the these "count" fields and not including any part of the original
/// update. The QR bit is set to one (1), and the response is sent back
/// to the requestor. If the requestor used UDP, then the response will
/// be sent to the requestor's source UDP port. If the requestor used
/// TCP, then the response will be sent back on the requestor's open TCP
/// connection.
/// ```
///
/// The "request" should be an update formatted message.
/// The response will be in the alternate, all 0's format described in RFC 2136 section 3.8
/// as this is more efficient.
///
/// # Arguments
///
/// * `request` - an update message
pub fn update(&self, request: &Message) -> Message {
let mut response: Message = Message::new();
response.id(request.get_id());
@ -349,7 +356,7 @@ mod catalog_tests {
pub fn create_test() -> Authority {
let origin: Name = Name::parse("test.com.", None).unwrap();
let mut records: Authority = Authority::new(origin.clone(), HashMap::new(), ZoneType::Master, false);
let mut records: Authority = Authority::new(origin.clone(), BTreeMap::new(), ZoneType::Master, false);
records.upsert(Record::new().name(origin.clone()).ttl(3600).rr_type(RecordType::SOA).dns_class(DNSClass::IN).rdata(RData::SOA(SOA::new(Name::parse("sns.dns.icann.org.", None).unwrap(), Name::parse("noc.dns.icann.org.", None).unwrap(), 2015082403, 7200, 3600, 1209600, 3600 ))).clone(), 0);
records.upsert(Record::new().name(origin.clone()).ttl(86400).rr_type(RecordType::NS).dns_class(DNSClass::IN).rdata(RData::NS(Name::parse("a.iana-servers.net.", None).unwrap()) ).clone(), 0);

View File

@ -355,37 +355,39 @@ impl PartialOrd<Name> for Name {
}
impl Ord for Name {
// RFC 4034 DNSSEC Resource Records March 2005
//
// 6.1. Canonical DNS Name Order
//
// For the purposes of DNS security, owner names are ordered by treating
// individual labels as unsigned left-justified octet strings. The
// absence of a octet sorts before a zero value octet, and uppercase
// US-ASCII letters are treated as if they were lowercase US-ASCII
// letters.
//
// To compute the canonical ordering of a set of DNS names, start by
// sorting the names according to their most significant (rightmost)
// labels. For names in which the most significant label is identical,
// continue sorting according to their next most significant label, and
// so forth.
//
// For example, the following names are sorted in canonical DNS name
// order. The most significant label is "example". At this level,
// "example" sorts first, followed by names ending in "a.example", then
// by names ending "z.example". The names within each level are sorted
// in the same way.
//
// example
// a.example
// yljkjljk.a.example
// Z.a.example
// zABC.a.EXAMPLE
// z.example
// \001.z.example
// *.z.example
// \200.z.example
/// RFC 4034 DNSSEC Resource Records March 2005
///
/// ```text
/// 6.1. Canonical DNS Name Order
///
/// For the purposes of DNS security, owner names are ordered by treating
/// individual labels as unsigned left-justified octet strings. The
/// absence of a octet sorts before a zero value octet, and uppercase
/// US-ASCII letters are treated as if they were lowercase US-ASCII
/// letters.
///
/// To compute the canonical ordering of a set of DNS names, start by
/// sorting the names according to their most significant (rightmost)
/// labels. For names in which the most significant label is identical,
/// continue sorting according to their next most significant label, and
/// so forth.
///
/// For example, the following names are sorted in canonical DNS name
/// order. The most significant label is "example". At this level,
/// "example" sorts first, followed by names ending in "a.example", then
/// by names ending "z.example". The names within each level are sorted
/// in the same way.
///
/// example
/// a.example
/// yljkjljk.a.example
/// Z.a.example
/// zABC.a.EXAMPLE
/// z.example
/// \001.z.example
/// *.z.example
/// \200.z.example
/// ```
fn cmp(&self, other: &Self) -> Ordering {
if self.labels.is_empty() && other.labels.is_empty() { return Ordering::Equal }

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::io::Read;
use std::fs::File;
@ -23,104 +23,106 @@ use ::authority::{Authority, RrKey, ZoneType, RRSet};
use super::master_lex::{Lexer, Token};
// 5. MASTER FILES
//
// Master files are text files that contain RRs in text form. Since the
// contents of a zone can be expressed in the form of a list of RRs a
// master file is most often used to define a zone, though it can be used
// to list a cache's contents. Hence, this section first discusses the
// format of RRs in a master file, and then the special considerations when
// a master file is used to create a zone in some name server.
//
// 5.1. Format
//
// The format of these files is a sequence of entries. Entries are
// predominantly line-oriented, though parentheses can be used to continue
// a list of items across a line boundary, and text literals can contain
// CRLF within the text. Any combination of tabs and spaces act as a
// delimiter between the separate items that make up an entry. The end of
// any line in the master file can end with a comment. The comment starts
// with a ";" (semicolon).
//
// The following entries are defined:
//
// <blank>[<comment>]
//
// $ORIGIN <domain-name> [<comment>]
//
// $INCLUDE <file-name> [<domain-name>] [<comment>]
//
// <domain-name><rr> [<comment>]
//
// <blank><rr> [<comment>]
//
// Blank lines, with or without comments, are allowed anywhere in the file.
//
// Two control entries are defined: $ORIGIN and $INCLUDE. $ORIGIN is
// followed by a domain name, and resets the current origin for relative
// domain names to the stated name. $INCLUDE inserts the named file into
// the current file, and may optionally specify a domain name that sets the
// relative domain name origin for the included file. $INCLUDE may also
// have a comment. Note that a $INCLUDE entry never changes the relative
// origin of the parent file, regardless of changes to the relative origin
// made within the included file.
//
// The last two forms represent RRs. If an entry for an RR begins with a
// blank, then the RR is assumed to be owned by the last stated owner. If
// an RR entry begins with a <domain-name>, then the owner name is reset.
//
// <rr> contents take one of the following forms:
//
// [<TTL>] [<class>] <type> <RDATA>
//
// [<class>] [<TTL>] <type> <RDATA>
//
// The RR begins with optional TTL and class fields, followed by a type and
// RDATA field appropriate to the type and class. Class and type use the
// standard mnemonics, TTL is a decimal integer. Omitted class and TTL
// values are default to the last explicitly stated values. Since type and
// class mnemonics are disjoint, the parse is unique. (Note that this
// order is different from the order used in examples and the order used in
// the actual RRs; the given order allows easier parsing and defaulting.)
//
// <domain-name>s make up a large share of the data in the master file.
// The labels in the domain name are expressed as character strings and
// separated by dots. Quoting conventions allow arbitrary characters to be
// stored in domain names. Domain names that end in a dot are called
// absolute, and are taken as complete. Domain names which do not end in a
// dot are called relative; the actual domain name is the concatenation of
// the relative part with an origin specified in a $ORIGIN, $INCLUDE, or as
// an argument to the master file loading routine. A relative name is an
// error when no origin is available.
//
// <character-string> is expressed in one or two ways: as a contiguous set
// of characters without interior spaces, or as a string beginning with a "
// and ending with a ". Inside a " delimited string any character can
// occur, except for a " itself, which must be quoted using \ (back slash).
//
// Because these files are text files several special encodings are
// necessary to allow arbitrary data to be loaded. In particular:
//
// of the root.
//
// @ A free standing @ is used to denote the current origin.
//
// \X where X is any character other than a digit (0-9), is
// used to quote that character so that its special meaning
// does not apply. For example, "\." can be used to place
// a dot character in a label.
//
// \DDD where each D is a digit is the octet corresponding to
// the decimal number described by DDD. The resulting
// octet is assumed to be text and is not checked for
// special meaning.
//
// ( ) Parentheses are used to group data that crosses a line
// boundary. In effect, line terminations are not
// recognized within parentheses.
//
// ; Semicolon is used to start a comment; the remainder of
// the line is ignored.
/// ```text
/// 5. MASTER FILES
///
/// Master files are text files that contain RRs in text form. Since the
/// contents of a zone can be expressed in the form of a list of RRs a
/// master file is most often used to define a zone, though it can be used
/// to list a cache's contents. Hence, this section first discusses the
/// format of RRs in a master file, and then the special considerations when
/// a master file is used to create a zone in some name server.
///
/// 5.1. Format
///
/// The format of these files is a sequence of entries. Entries are
/// predominantly line-oriented, though parentheses can be used to continue
/// a list of items across a line boundary, and text literals can contain
/// CRLF within the text. Any combination of tabs and spaces act as a
/// delimiter between the separate items that make up an entry. The end of
/// any line in the master file can end with a comment. The comment starts
/// with a ";" (semicolon).
///
/// The following entries are defined:
///
/// <blank>[<comment>]
///
/// $ORIGIN <domain-name> [<comment>]
///
/// $INCLUDE <file-name> [<domain-name>] [<comment>]
///
/// <domain-name><rr> [<comment>]
///
/// <blank><rr> [<comment>]
///
/// Blank lines, with or without comments, are allowed anywhere in the file.
///
/// Two control entries are defined: $ORIGIN and $INCLUDE. $ORIGIN is
/// followed by a domain name, and resets the current origin for relative
/// domain names to the stated name. $INCLUDE inserts the named file into
/// the current file, and may optionally specify a domain name that sets the
/// relative domain name origin for the included file. $INCLUDE may also
/// have a comment. Note that a $INCLUDE entry never changes the relative
/// origin of the parent file, regardless of changes to the relative origin
/// made within the included file.
///
/// The last two forms represent RRs. If an entry for an RR begins with a
/// blank, then the RR is assumed to be owned by the last stated owner. If
/// an RR entry begins with a <domain-name>, then the owner name is reset.
///
/// <rr> contents take one of the following forms:
///
/// [<TTL>] [<class>] <type> <RDATA>
///
/// [<class>] [<TTL>] <type> <RDATA>
///
/// The RR begins with optional TTL and class fields, followed by a type and
/// RDATA field appropriate to the type and class. Class and type use the
/// standard mnemonics, TTL is a decimal integer. Omitted class and TTL
/// values are default to the last explicitly stated values. Since type and
/// class mnemonics are disjoint, the parse is unique. (Note that this
/// order is different from the order used in examples and the order used in
/// the actual RRs; the given order allows easier parsing and defaulting.)
///
/// <domain-name>s make up a large share of the data in the master file.
/// The labels in the domain name are expressed as character strings and
/// separated by dots. Quoting conventions allow arbitrary characters to be
/// stored in domain names. Domain names that end in a dot are called
/// absolute, and are taken as complete. Domain names which do not end in a
/// dot are called relative; the actual domain name is the concatenation of
/// the relative part with an origin specified in a $ORIGIN, $INCLUDE, or as
/// an argument to the master file loading routine. A relative name is an
/// error when no origin is available.
///
/// <character-string> is expressed in one or two ways: as a contiguous set
/// of characters without interior spaces, or as a string beginning with a "
/// and ending with a ". Inside a " delimited string any character can
/// occur, except for a " itself, which must be quoted using \ (back slash).
///
/// Because these files are text files several special encodings are
/// necessary to allow arbitrary data to be loaded. In particular:
///
/// of the root.
///
/// @ A free standing @ is used to denote the current origin.
///
/// \X where X is any character other than a digit (0-9), is
/// used to quote that character so that its special meaning
/// does not apply. For example, "\." can be used to place
/// a dot character in a label.
///
/// \DDD where each D is a digit is the octet corresponding to
/// the decimal number described by DDD. The resulting
/// octet is assumed to be text and is not checked for
/// special meaning.
///
/// ( ) Parentheses are used to group data that crosses a line
/// boundary. In effect, line terminations are not
/// recognized within parentheses.
///
/// ; Semicolon is used to start a comment; the remainder of
/// the line is ignored.
/// ```
pub struct Parser;
impl Parser {
@ -141,7 +143,7 @@ impl Parser {
pub fn parse(&mut self, lexer: Lexer, origin: Option<Name>, zone_type: ZoneType, allow_update: bool) -> ParseResult<Authority> {
let mut lexer = lexer;
let mut records: HashMap<RrKey, RRSet> = HashMap::new();
let mut records: BTreeMap<RrKey, RRSet> = BTreeMap::new();
let mut origin: Option<Name> = origin;
let mut current_name: Option<Name> = None;
@ -303,7 +305,6 @@ impl Parser {
//
// build the Authority and return.
records.shrink_to_fit(); // this shouldn't change once stored (replacement instead)
Ok(Authority::new(try!(origin.ok_or(ParseError::OriginIsUndefined)), records, zone_type, allow_update))
}