Persistence (#18)

* initial SQLite integration

* iterable Journal

* journal recovery validated

* validation of journaling adds and deletes

* journal recovery in named

* updated journal documentation
This commit is contained in:
Benjamin Fry 2016-06-16 23:53:29 -07:00 committed by GitHub
parent 32f0bff0a3
commit 85d71ca83e
26 changed files with 957 additions and 214 deletions

View File

@ -2,6 +2,17 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## unreleased
### Added
- Added recovery from journal to named startup
- SQLite journal for dynamic update persistence
### Changed
- Removed many of the unwraps in named binary
### Fixed
- TXT record case sensitivity
## 0.6.0 2016-06-01
### Added
- Documentation on all modules, and many standard RFC types

41
Cargo.lock generated
View File

@ -10,7 +10,9 @@ dependencies = [
"mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)",
"rusqlite 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -49,6 +51,11 @@ name = "bitflags"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bitflags"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bytes"
version = "0.3.0"
@ -132,11 +139,33 @@ dependencies = [
"pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "libsqlite3-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "linked-hash-map"
version = "0.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "log"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lru-cache"
version = "0.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"linked-hash-map 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "memchr"
version = "0.1.11"
@ -276,6 +305,18 @@ name = "regex-syntax"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rusqlite"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
"libsqlite3-sys 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lru-cache 0.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rustc-serialize"
version = "0.3.19"

View File

@ -76,4 +76,6 @@ mio = "^0.5.1"
openssl = "^0.7.8"
openssl-sys = "^0.7.8"
rustc-serialize = "^0.3.18"
rusqlite = "^0.7.3"
time = "^0.1.35"
toml = "^0.1.28"

View File

@ -1,4 +1,4 @@
// Copyright 2015-2016 Benjamin Fry
// Copyright 2015-2016 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or

View File

@ -19,7 +19,8 @@ use std::cmp::Ordering;
use chrono::offset::utc::UTC;
use openssl::crypto::pkey::Role;
use ::authority::{UpdateResult, ZoneType, RRSet};
use ::authority::{Journal, RRSet, UpdateResult, ZoneType};
use ::error::{PersistenceError, PersistenceResult};
use ::op::{Message, UpdateMessage, ResponseCode, Query};
use ::rr::{DNSClass, Name, RData, Record, RecordType};
use ::rr::rdata::{NSEC, SIG};
@ -62,10 +63,14 @@ impl Ord for RrKey {
}
}
/// Authority is the storage method for all resource records
/// Authority is responsible for storing the resource records for a particular zone.
///
/// Authorities default to DNSClass IN. The ZoneType specifies if this should be treated as the
/// start of authority for the zone, is a slave, or a cached zone.
pub struct Authority {
origin: Name,
class: DNSClass,
journal: Option<Journal>,
records: BTreeMap<RrKey, RRSet>,
zone_type: ZoneType,
allow_update: bool,
@ -77,10 +82,6 @@ pub struct Authority {
secure_keys: Vec<Signer>,
}
/// Authority is responsible for storing the resource records for a particular zone.
///
/// Authorities default to DNSClass IN. The ZoneType specifies if this should be treated as the
/// start of authority for the zone, is a slave, or a cached zone.
impl Authority {
/// Creates a new Authority.
///
@ -88,7 +89,7 @@ impl Authority {
///
/// * `origin` - The zone `Name` being created, this should match that of the `RecordType::SOA`
/// record.
/// * `records` - The `HashMap` of the initial set of records in the zone.
/// * `records` - The map of the initial set of records in the zone.
/// * `zone_type` - The type of zone, i.e. is this authoritative?
/// * `allow_update` - If true, then this zone accepts dynamic updates.
///
@ -96,10 +97,15 @@ impl Authority {
///
/// The new `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,
Authority{ origin: origin, class: DNSClass::IN, journal: None, records: records, zone_type: zone_type,
allow_update: allow_update, secure_keys: Vec::new() }
}
/// By adding a secure key, this will implicitly enable dnssec for the zone.
///
/// # Arguments
///
/// * `signer` - Signer with associated private key
pub fn add_secure_key(&mut self, signer: Signer) {
// also add the key to the zone
let zone_ttl = self.get_minimum_ttl();
@ -111,6 +117,68 @@ impl Authority {
self.secure_keys.push(signer);
}
/// Recovers the zone from a Journal, returns an error on failure to recover the zone.
///
/// # Arguments
///
/// * `journal` - the journal from which to load the persisted zone.
pub fn recover_with_journal(&mut self, journal: &Journal) -> PersistenceResult<()> {
assert!(self.records.is_empty(), "records should be empty during a recovery");
info!("recovering from journal");
for record in journal.iter() {
// AXFR is special, it is used to mark the dump of a full zone.
// when recovering, if an AXFR is encountered, we should remove all the records in the
// authority.
if record.get_rr_type() == RecordType::AXFR {
self.records.clear();
} else {
match self.update_records(&[record], false) {
Err(error) => return Err(PersistenceError::RecoveryError(error_loc!(), format!("error recovering from journal: {:?}", error))),
_ => (),
}
}
}
// zone signing was off during load, now sign the zone.
self.sign_zone();
Ok(())
}
/// Persist the state of the current zone to the journal, does nothing if there is no associated
/// Journal.
///
/// Returns an error if there was an issue writing to the persistence layer.
pub fn persist_to_journal(&self) -> PersistenceResult<()> {
if let Some(journal) = self.journal.as_ref() {
let serial = self.get_serial();
info!("persisting zone to journal at SOA.serial: {}", serial);
// TODO: THIS NEEDS TO BE IN A TRANSACTION!!!
try!(journal.insert_record(serial, Record::new().rr_type(RecordType::AXFR)));
for rr_set in self.records.values() {
// TODO: should we preserve rr_sets or not?
for record in rr_set.iter() {
try!(journal.insert_record(serial, record));
}
}
// TODO: COMMIT THE TRANSACTION!!!
}
Ok(())
}
pub fn journal(&mut self, journal: Journal) {
self.journal = Some(journal);
}
pub fn get_journal(&self) -> Option<&Journal> {
self.journal.as_ref()
}
#[cfg(test)]
pub fn set_allow_update(&mut self, allow_update: bool) {
self.allow_update = allow_update;
@ -129,9 +197,13 @@ impl Authority {
self.zone_type
}
pub fn get_records(&self) -> &BTreeMap<RrKey, RRSet> {
&self.records
}
/// Returns the SOA of the authority.
///
/// *Note* This will only return the SOA, if this is fullfilling a request, a standard lookup
/// *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
@ -529,10 +601,24 @@ impl Authority {
/// NONE rrset rr Delete an RR from an RRset
/// zone rrset rr Add to an RRset
/// ```
fn update_records(&mut self, records: &[Record]) -> UpdateResult<bool> {
///
/// # Arguments
///
/// * `records` - set of record instructions for update following above rules
/// * `auto_sign` - if true, the zone will auto_sign (assuming there are signers present), this
/// should be disabled during recovery.
fn update_records(&mut self, records: &[Record], auto_sign: bool) -> UpdateResult<bool> {
let mut updated = false;
let serial: u32 = self.get_serial();
// the persistence act as a write-ahead log. The WAL will also be used for recovery of a zone
// subsequent to a failure of the server.
if let Some(ref journal) = self.journal {
if let Err(error) = journal.insert_records(serial, records) {
error!("could not persist update records: {}", error);
return Err(ResponseCode::ServFail);
}
}
// 3.4.2.7 - Pseudocode For Update Section Processing
//
@ -655,7 +741,7 @@ impl Authority {
}
// update the serial...
if updated {
if auto_sign && updated {
self.secure_zone();
}
@ -746,7 +832,7 @@ impl Authority {
try!(self.verify_prerequisites(update.get_pre_requisites()));
try!(self.pre_scan(update.get_updates()));
self.update_records(update.get_updates())
self.update_records(update.get_updates(), true)
}
/// Using the specified query, perform a lookup against this zone.
@ -1232,12 +1318,12 @@ pub mod authority_tests {
//
// zone rrset rr Add to an RRset
let add_record = &[Record::new().name(new_name.clone()).ttl(86400).rr_type(RecordType::A).dns_class(DNSClass::IN).rdata(RData::A(Ipv4Addr::new(93,184,216,24))).clone()];
assert!(authority.update_records(add_record).expect("update failed"));
assert!(authority.update_records(add_record, true).expect("update failed"));
assert_eq!(authority.lookup(&new_name, RecordType::ANY, false), add_record.iter().collect::<Vec<&Record>>());
assert_eq!(serial + 1, authority.get_serial());
let add_www_record = &[Record::new().name(www_name.clone()).ttl(86400).rr_type(RecordType::A).dns_class(DNSClass::IN).rdata(RData::A(Ipv4Addr::new(10,0,0,1))).clone()];
assert!(authority.update_records(add_www_record).expect("update failed"));
assert!(authority.update_records(add_www_record, true).expect("update failed"));
assert_eq!(serial + 2, authority.get_serial());
{
@ -1253,7 +1339,7 @@ pub mod authority_tests {
//
// NONE rrset rr Delete an RR from an RRset
let del_record = &[Record::new().name(new_name.clone()).ttl(86400).rr_type(RecordType::A).dns_class(DNSClass::NONE).rdata(RData::A(Ipv4Addr::new(93,184,216,24))).clone()];
assert!(authority.update_records(del_record).expect("update failed"));
assert!(authority.update_records(del_record, true).expect("update failed"));
assert_eq!(serial + 3, authority.get_serial());
{
println!("after delete of specific record: {:?}", authority.lookup(&new_name, RecordType::ANY, false));
@ -1262,7 +1348,7 @@ pub mod authority_tests {
// remove one from www
let del_record = &[Record::new().name(www_name.clone()).ttl(86400).rr_type(RecordType::A).dns_class(DNSClass::NONE).rdata(RData::A(Ipv4Addr::new(10,0,0,1))).clone()];
assert!(authority.update_records(del_record).expect("update failed"));
assert!(authority.update_records(del_record, true).expect("update failed"));
assert_eq!(serial + 4, authority.get_serial());
{
let mut www_rrset = authority.lookup(&www_name, RecordType::ANY, false);
@ -1274,7 +1360,7 @@ pub mod authority_tests {
//
// ANY rrset empty Delete an RRset
let del_record = &[Record::new().name(www_name.clone()).ttl(86400).rr_type(RecordType::A).dns_class(DNSClass::ANY).rdata(RData::NULL(NULL::new())).clone()];
assert!(authority.update_records(del_record).expect("update failed"));
assert!(authority.update_records(del_record, true).expect("update failed"));
assert_eq!(serial + 5, authority.get_serial());
let mut removed_a_vec: Vec<_> = vec![
Record::new().name(www_name.clone()).ttl(86400).rr_type(RecordType::TXT).dns_class(DNSClass::IN).rdata(RData::TXT(TXT::new(vec!["v=spf1 -all".to_string()]))).clone(),
@ -1293,7 +1379,7 @@ pub mod authority_tests {
// ANY ANY empty Delete all RRsets from a name
println!("deleting all records");
let del_record = &[Record::new().name(www_name.clone()).ttl(86400).rr_type(RecordType::ANY).dns_class(DNSClass::ANY).rdata(RData::NULL(NULL::new())).clone()];
assert!(authority.update_records(del_record).expect("update failed"));
assert!(authority.update_records(del_record, true).expect("update failed"));
assert!(authority.lookup(&www_name, RecordType::ANY, false).is_empty());
assert_eq!(serial + 6, authority.get_serial());
}
@ -1334,4 +1420,88 @@ pub mod authority_tests {
assert!(record.get_name() < &name);
}
}
#[test]
fn test_journal() {
use std::net::Ipv4Addr;
use rusqlite::Connection;
use ::authority::Journal;
// test that this message can be inserted
let conn = Connection::open_in_memory().expect("could not create in memory DB");
let mut journal = Journal::new(conn).unwrap();
journal.schema_up().unwrap();
let mut authority = create_example();
authority.journal(journal);
authority.persist_to_journal().unwrap();
let new_name = Name::new().label("new").label("example").label("com");
let delete_name = Name::new().label("www").label("example").label("com");
let new_record = Record::new().name(new_name.clone()).rdata(RData::A(Ipv4Addr::new(10,11,12,13))).clone();
let delete_record = Record::new().name(delete_name.clone()).rdata(RData::A(Ipv4Addr::new(93,184,216,34))).dns_class(DNSClass::NONE).clone();
authority.update_records(&[new_record.clone(), delete_record], true).unwrap();
// assert that the correct set of records is there.
let new_rrset: Vec<&Record> = authority.lookup(&new_name, RecordType::A, false);
assert!(new_rrset.iter().all(|r| *r == &new_record));
let delete_rrset: Vec<&Record> = authority.lookup(&delete_name, RecordType::A, false);
assert!(delete_rrset.is_empty());
// that record should have been recorded... let's reload the journal and see if we get it.
let mut recovered_authority = Authority::new(authority.get_origin().clone(),
BTreeMap::new(),
ZoneType::Master,
false);
recovered_authority.recover_with_journal(authority.get_journal().expect("journal not Some")).expect("recovery");
// assert that the correct set of records is there.
let new_rrset: Vec<&Record> = recovered_authority.lookup(&new_name, RecordType::A, false);
assert!(new_rrset.iter().all(|r| *r == &new_record));
let delete_rrset: Vec<&Record> = authority.lookup(&delete_name, RecordType::A, false);
assert!(delete_rrset.is_empty());
}
#[test]
fn test_recovery() {
use rusqlite::Connection;
use ::authority::Journal;
// test that this message can be inserted
let conn = Connection::open_in_memory().expect("could not create in memory DB");
let mut journal = Journal::new(conn).unwrap();
journal.schema_up().unwrap();
let mut authority = create_example();
authority.journal(journal);
authority.persist_to_journal().unwrap();
let journal = authority.get_journal().unwrap();
let mut recovered_authority = Authority::new(authority.get_origin().clone(),
BTreeMap::new(),
ZoneType::Master,
false);
recovered_authority.recover_with_journal(journal).expect("recovery");
assert_eq!(recovered_authority.get_records().len(), authority.get_records().len());
assert_eq!(recovered_authority.get_soa(), authority.get_soa());
assert!(recovered_authority.get_records().iter().all(|(rr_key, rr_set)| {
let other_rr_set = authority.get_records().get(rr_key).expect(&format!("key doesn't exist: {:?}", rr_key));
rr_set.iter().zip(other_rr_set.iter()).all(|(record, other_record)| {
record.get_ttl() == other_record.get_ttl() &&
record.get_rdata() == other_record.get_rdata()
})
}));
assert!(authority.get_records().iter().all(|(rr_key, rr_set)| {
let other_rr_set = recovered_authority.get_records().get(rr_key).expect(&format!("key doesn't exist: {:?}", rr_key));
rr_set.iter().zip(other_rr_set.iter()).all(|(record, other_record)| {
record.get_ttl() == other_record.get_ttl() &&
record.get_rdata() == other_record.get_rdata()
})
}));
}
}

View File

@ -25,12 +25,14 @@ pub enum ZoneType { Master, Slave, Hint, Forward }
mod authority;
mod catalog;
mod persistence;
mod rr_set;
pub use self::authority::Authority;
pub use self::authority::RrKey;
pub use self::catalog::Catalog;
pub use self::rr_set::RRSet;
pub use self::persistence::Journal;
#[cfg(test)]
pub use self::authority::authority_tests;

View File

@ -0,0 +1,348 @@
// Copyright 2015-2016 Benjamin Fry <benjaminfry -@- me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use std::iter::Iterator;
use std::path::Path;
use time;
use rusqlite;
use rusqlite::Connection;
use rusqlite::SqliteError;
use ::error::PersistenceError;
use ::error::PersistenceResult;
use ::rr::Record;
use ::serialize::binary::{BinDecoder, BinEncoder, BinSerializable};
const CURRENT_VERSION: i64 = 1;
/// The Journal is the audit log of all changes to a zone after initial creation.
pub struct Journal {
conn: Connection,
version: i64,
}
impl Journal {
pub fn new(conn: Connection) -> PersistenceResult<Journal> {
let version = Self::select_schema_version(&conn);
Ok(Journal { conn: conn, version: try!(version) })
}
pub fn from_file(journal_file: &Path) -> PersistenceResult<Journal> {
Self::new(try_rethrow!(PersistenceError::SqliteError, Connection::open(journal_file)))
}
/// gets the current schema version of the journal
pub fn get_schema_version(&self) -> i64 {
self.version
}
/// this returns an iterator from the beginning of time, to be used to recreate an authority
pub fn iter<'j>(&'j self) -> JournalIter<'j> {
JournalIter::new(self)
}
/// Inserts a record, this is an append only operation.
///
/// Records should never be posthumously modified. The message will be serialized into the.
/// the first message serialized to the journal, should be a single AXFR of the entire zone,
/// this will be used as a starting point to reconstruct the zone.
///
/// # Argument
///
/// * `record` - will be serialized into the journal
pub fn insert_record(&self, soa_serial: u32, record: &Record) -> PersistenceResult<()> {
assert!(self.version == CURRENT_VERSION, "schema version mismatch, schema_up() resolves this");
let mut serial_record: Vec<u8> = Vec::with_capacity(512);
{
let mut encoder = BinEncoder::new(&mut serial_record);
try_rethrow!(PersistenceError::EncodeError, record.emit(&mut encoder));
}
let timestamp = time::get_time();
let client_id: i64 = 0; // TODO: we need better id information about the client, like pub_key
let soa_serial: i64 = soa_serial as i64;
let count = try_rethrow!(PersistenceError::SqliteError,
self.conn.execute("INSERT
INTO records (client_id, soa_serial, timestamp, record)
VALUES ($1, $2, $3, $4)",
&[&client_id, &soa_serial, &timestamp, &serial_record]));
//
if count != 1 {
return Err(PersistenceError::WrongInsertCount{loc: error_loc!(), got: count, expect: 1});
};
Ok(())
}
/// Inserts a set of records into the Journal, a convenience method for insert_record
pub fn insert_records(&self, soa_serial: u32, records: &[Record]) -> PersistenceResult<()> {
// TODO: NEED TRANSACTION HERE
for record in records {
try!(self.insert_record(soa_serial, record));
}
Ok(())
}
/// Selects a record from the given row_id.
///
/// This allows for the entire set of records to be iterated through, by starting at 0, and
/// incrementing each subsequent row.
///
/// # Arguments
///
/// * `row_id` - the row_id can either be exact, or start at 0 to get the earliest row in the
/// list.
pub fn select_record(&self, row_id: i64) -> PersistenceResult<Option<(i64, Record)>> {
assert!(self.version == CURRENT_VERSION, "schema version mismatch, schema_up() resolves this");
let mut stmt = try_rethrow!(PersistenceError::SqliteError,
self.conn.prepare("SELECT _rowid_, record
FROM records
WHERE _rowid_ >= $1
LIMIT 1"));
let record_opt: Option<Result<(i64, Record), SqliteError>> = try_rethrow!(PersistenceError::SqliteError,
stmt.query_and_then(&[&row_id],
|row| -> Result<(i64, Record), SqliteError> {
let row_id: i64 = try!(row.get_checked(0));
let record_bytes: Vec<u8> = try!(row.get_checked(1));
let mut decoder = BinDecoder::new(&record_bytes);
// todo add location to this...
match Record::read(&mut decoder) {
Ok(record) => Ok((row_id, record)),
Err(decode_error) => Err(rusqlite::Error::FromSqlConversionFailure(Box::new(decode_error))),
}
})).next();
//
match record_opt {
Some(Ok((row_id, record))) => Ok(Some((row_id, record))),
Some(Err(err)) => Err(PersistenceError::SqliteError(error_loc!(), err)),
None => Ok(None),
}
}
/// selects the current schema version of the journal DB, returns -1 if there is no schema
///
///
/// # Arguments
///
/// * `conn` - db connection to use
fn select_schema_version(conn: &Connection) -> PersistenceResult<i64> {
// first see if our schema is there
let mut stmt = try_rethrow!(PersistenceError::SqliteError,
conn.prepare("SELECT name
FROM sqlite_master
WHERE type='table'
AND name='tdns_schema'"));
let tdns_schema_opt: Option<Result<String, _>> = try_rethrow!(PersistenceError::SqliteError,
stmt.query_map(&[],
|row| row.get(0)))
.next();
let tdns_schema = match tdns_schema_opt {
Some(Ok(string)) => string,
Some(Err(err)) => return Err(PersistenceError::SqliteError(error_loc!(), err)),
None => return Ok(-1),
};
assert_eq!(&tdns_schema, "tdns_schema");
let version: i64 = try_rethrow!(PersistenceError::SqliteError,
conn.query_row_safe("SELECT version
FROM tdns_schema",
&[],
|row| row.get(0)));
Ok(version)
}
/// update the schema version
fn update_schema_version(&self, new_version: i64) -> PersistenceResult<()> {
// validate the versions of all the schemas...
assert!(new_version <= CURRENT_VERSION);
let count = try_rethrow!(PersistenceError::SqliteError,
self.conn.execute("UPDATE tdns_schema SET version = $1", &[&new_version]));
//
assert_eq!(count, 1);
Ok(())
}
/// initilizes the schema for the Journal
pub fn schema_up(&mut self) -> PersistenceResult<i64> {
while self.version < CURRENT_VERSION {
match self.version + 1 {
0 => self.version = try!(self.init_up()),
1 => self.version = try!(self.records_up()),
_ => panic!("incorrect version somewhere"),
}
try!(self.update_schema_version(self.version));
}
Ok(self.version)
}
/// initial schema, include the tdns_schema table for tracking the Journal version
fn init_up(&self) -> PersistenceResult<i64> {
let count = try_rethrow!(PersistenceError::SqliteError,
self.conn.execute("CREATE TABLE tdns_schema (
version INTEGER NOT NULL
)", &[]));
//
assert_eq!(count, 0);
let count = try_rethrow!(PersistenceError::SqliteError,
self.conn.execute("INSERT INTO tdns_schema (version)
VALUES (0)", &[]));
//
assert_eq!(count, 1);
Ok(0)
}
/// adds the records table, this is the main and single table for the history of changes to an
/// authority. Each record is expected to be in the format of an update record
fn records_up(&self) -> PersistenceResult<i64> {
// we'll be using rowid for our primary key, basically: `rowid INTEGER PRIMARY KEY ASC`
let count = try_rethrow!(PersistenceError::SqliteError,
self.conn.execute("CREATE TABLE records (
client_id INTEGER NOT NULL,
soa_serial INTEGER NOT NULL,
timestamp TEXT NOT NULL,
record BLOB NOT NULL
)", &[]));
//
assert_eq!(count, 1);
Ok(1)
}
}
pub struct JournalIter<'j> {
current_row_id: i64,
journal: &'j Journal,
}
impl<'j> JournalIter<'j> {
fn new(journal: &'j Journal) -> Self {
JournalIter { current_row_id: 0, journal: journal }
}
}
impl<'j> Iterator for JournalIter<'j> {
type Item = Record;
fn next(&mut self) -> Option<Self::Item> {
let next: PersistenceResult<Option<(i64, Record)>> = self.journal.select_record(self.current_row_id + 1);
match next {
Ok(Some((row_id, record))) => {
self.current_row_id = row_id;
Some(record)
},
Ok(None) => {
None
},
Err(err) => {
error!("persistence error while iterating over journal: {}", err);
None
}
}
}
}
#[test]
fn test_new_journal() {
let conn = Connection::open_in_memory().expect("could not create in memory DB");
assert_eq!(Journal::new(conn).expect("new Journal").get_schema_version(), -1);
}
#[test]
fn test_init_journal() {
let conn = Connection::open_in_memory().expect("could not create in memory DB");
let mut journal = Journal::new(conn).unwrap();
let version = journal.schema_up().unwrap();
assert_eq!(version, CURRENT_VERSION);
assert_eq!(Journal::select_schema_version(&journal.conn).unwrap(), CURRENT_VERSION);
}
#[cfg(test)]
fn create_test_journal() -> (Record, Journal) {
use std::net::Ipv4Addr;
use std::str::FromStr;
use ::rr::{Name, RData, Record, RecordType};
let www = Name::with_labels(vec!["www".to_string(),"example".to_string(), "com".to_string()]);
let mut record = Record::new();
record.name(www);
record.rr_type(RecordType::A);
record.rdata(RData::A(Ipv4Addr::from_str("127.0.0.1").unwrap()));
// test that this message can be inserted
let conn = Connection::open_in_memory().expect("could not create in memory DB");
let mut journal = Journal::new(conn).unwrap();
journal.schema_up().unwrap();
// insert the message
journal.insert_record(0, &record).unwrap();
// insert another...
record.rdata(RData::A(Ipv4Addr::from_str("127.0.1.1").unwrap()));
journal.insert_record(0, &record).unwrap();
(record, journal)
}
#[test]
fn test_insert_and_select_record() {
use std::net::Ipv4Addr;
use std::str::FromStr;
use rr::RData;
let (mut record, journal) = create_test_journal();
// select the record
let (row_id, journal_record) = journal.select_record(0).expect("persistence error").expect("none");
record.rdata(RData::A(Ipv4Addr::from_str("127.0.0.1").unwrap()));
assert_eq!(journal_record, record);
// test another
let (row_id, journal_record) = journal.select_record(row_id + 1).expect("persistence error").expect("none");
record.rdata(RData::A(Ipv4Addr::from_str("127.0.1.1").unwrap()));
assert_eq!(journal_record, record);
// check that we get nothing for id over row_id
let option_none = journal.select_record(row_id + 1).expect("persistence error");
assert!(option_none.is_none());
}
#[test]
fn test_iterator() {
use std::net::Ipv4Addr;
use std::str::FromStr;
use rr::RData;
let (mut record, journal) = create_test_journal();
let mut iter = journal.iter();
assert_eq!(record.rdata(RData::A(Ipv4Addr::from_str("127.0.0.1").unwrap())), &iter.next().unwrap());
assert_eq!(record.rdata(RData::A(Ipv4Addr::from_str("127.0.1.1").unwrap())), &iter.next().unwrap());
assert_eq!(None, iter.next());
}

View File

@ -1,23 +1,15 @@
/*
* Copyright (C) 2015-2016 Benjamin Fry <benjaminfry@me.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Copyright 2015-2016 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use std::slice::Iter;
use ::rr::{Name, Record, RecordType, RData};
/// Set of resource records associated to a name and type
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub struct RRSet {
name: Name,
record_type: RecordType,
@ -78,6 +70,11 @@ impl RRSet {
}
}
/// Returns an iterator over the records in the set
pub fn iter<'s>(&'s self) -> Iter<'s, Record> {
self.records.iter()
}
/// # Return value
///
/// True if there are no records in this set

View File

@ -90,6 +90,7 @@ pub struct ZoneConfig {
zone_type: ZoneType,
file: String,
allow_update: Option<bool>,
enable_dnssec: Option<bool>,
}
impl ZoneConfig {
@ -97,7 +98,8 @@ impl ZoneConfig {
pub fn get_zone(&self) -> ParseResult<Name> { Name::parse(&self.zone, Some(&Name::new())) }
pub fn get_zone_type(&self) -> ZoneType { self.zone_type }
pub fn get_file(&self) -> PathBuf { PathBuf::from(&self.file) }
pub fn get_allow_udpate(&self) -> bool { self.allow_update.unwrap_or(false) }
pub fn is_update_allowed(&self) -> bool { self.allow_update.unwrap_or(false) }
pub fn is_dnssec_enabled(&self) -> bool { self.enable_dnssec.unwrap_or(false) }
}
#[cfg(test)]

View File

@ -1,20 +1,20 @@
#
# This is an example configuration file for the TRust-DNS named server.
#
# The format is in TOML: https://github.com/toml-lang/toml which was chosen
# as the configuration format for TRust-DNS. While Trust-DNS is inteaded to
# be a drop-in replacement for BIND9, it will not support the named.conf files
# directly. At some point, there will be a binary tool for converting the
# BIND9 configuration files over to TRust-DNS TOML.
#
# Many of these options are available as both command line options and
# configuration options in these files. In that case, the command line option
# will take presidence.
#
# Comments with two hash marks, ##, document the config parameter
# Comments with one hash mark, #, is an example line and should be the default
#
# The root options are similar to the options in 'options { .. }' in named.conf
##
## This is an example configuration file for the TRust-DNS named server.
##
## The format is in TOML: https://github.com/toml-lang/toml which was chosen
## as the configuration format for TRust-DNS. While Trust-DNS is inteaded to
## be a drop-in replacement for BIND9, it will not support the named.conf files
## directly. At some point, there will be a binary tool for converting the
## BIND9 configuration files over to TRust-DNS TOML.
##
## Many of these options are available as both command line options and
## configuration options in these files. In that case, the command line option
## will take presidence.
##
## Comments with two hash marks, ##, document the config parameter
## Comments with one hash mark, #, is an example line and should be the default
##
## The root options are similar to the options in 'options { .. }' in named.conf
## listen_addrs: address on which to listen for incoming connections
## this can be a list of ipv4 or ipv6 addresses
@ -34,13 +34,8 @@
## Default zones, these should be present on all nameservers, except in rare
## configuration cases
[[zones]]
## zone: this is the ORIGIN of the zone, aka the base name, '.' is implied on the end
zone = "localhost"
## zone_type: Master, Slave, Hint, Forward
zone_type = "Master"
## file: this is relative to the directory above
file = "default/localhost.zone"
[[zones]]
@ -64,6 +59,20 @@ zone_type = "Master"
file = "default/0.zone"
# [[zones]]
## zone: this is the ORIGIN of the zone, aka the base name, '.' is implied on the end
# zone = "example.com"
## zone_type: Master, Slave, Hint, Forward
# zone_type = "Master"
## file: this is relative to the directory above
# file = "example.com.zone"
## if false, updates will not be allowed, default false
# allow_update = false
## if true, looks to see if a chained pem file exists at $file.pem
## these keys will also be registered as authorities for update,
## meaning that SIG(0) updates can be established by initially using these
## keys.
# enable_dnssec = false

View File

@ -38,11 +38,11 @@ fn test_read_config() {
assert_eq!(config.get_log_level(), LogLevel::Info);
assert_eq!(config.get_directory(), Path::new("/var/named"));
assert_eq!(config.get_zones(), [
ZoneConfig { zone: "localhost".into(), zone_type: ZoneType::Master, file: "default/localhost.zone".into(), allow_update: None },
ZoneConfig { zone: "0.0.127.in-addr.arpa".into(), zone_type: ZoneType::Master, file: "default/127.0.0.1.zone".into(), allow_update: None },
ZoneConfig { zone: "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa".into(), zone_type: ZoneType::Master, file: "default/ipv6_1.zone".into(), allow_update: None },
ZoneConfig { zone: "255.in-addr.arpa".into(), zone_type: ZoneType::Master, file: "default/255.zone".into(), allow_update: None },
ZoneConfig { zone: "0.in-addr.arpa".into(), zone_type: ZoneType::Master, file: "default/0.zone".into(), allow_update: None }
ZoneConfig { zone: "localhost".into(), zone_type: ZoneType::Master, file: "default/localhost.zone".into(), allow_update: None, enable_dnssec: None },
ZoneConfig { zone: "0.0.127.in-addr.arpa".into(), zone_type: ZoneType::Master, file: "default/127.0.0.1.zone".into(), allow_update: None, enable_dnssec: None },
ZoneConfig { zone: "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa".into(), zone_type: ZoneType::Master, file: "default/ipv6_1.zone".into(), allow_update: None, enable_dnssec: None },
ZoneConfig { zone: "255.in-addr.arpa".into(), zone_type: ZoneType::Master, file: "default/255.zone".into(), allow_update: None, enable_dnssec: None },
ZoneConfig { zone: "0.in-addr.arpa".into(), zone_type: ZoneType::Master, file: "default/0.zone".into(), allow_update: None, enable_dnssec: None }
]);
}

View File

@ -23,6 +23,7 @@ mod encode_error;
mod client_error;
mod lexer_error;
mod parse_error;
mod persistence_error;
mod config_error;
pub use self::base_error::ErrorLoc;
@ -31,6 +32,7 @@ pub use self::encode_error::EncodeError;
pub use self::client_error::ClientError;
pub use self::lexer_error::LexerError;
pub use self::parse_error::ParseError;
pub use self::persistence_error::PersistenceError;
pub use self::config_error::ConfigError;
pub type DecodeResult<T> = Result<T, DecodeError>;
@ -38,4 +40,5 @@ pub type EncodeResult = Result<(), EncodeError>;
pub type ClientResult<T> = Result<T, ClientError>;
pub type LexerResult<T> = Result<T, LexerError>;
pub type ParseResult<T> = Result<T, ParseError>;
pub type PersistenceResult<T> = Result<T, PersistenceError>;
pub type ConfigResult<T> = Result<T, ConfigError>;

View File

@ -0,0 +1,57 @@
// Copyright 2015-2016 Benjamin Fry
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use std::error::Error;
use std::fmt;
use rusqlite;
pub enum PersistenceError {
DecodeError(super::ErrorLoc, super::DecodeError),
EncodeError(super::ErrorLoc, super::EncodeError),
SqliteError(super::ErrorLoc, rusqlite::Error),
WrongInsertCount{ loc: super::ErrorLoc, got: i32, expect: i32 },
RecoveryError(super::ErrorLoc, String),
}
impl fmt::Debug for PersistenceError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self, f)
}
}
impl fmt::Display for PersistenceError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
PersistenceError::DecodeError(ref err_loc, ref err) => write!(f, "{}:{}", err_loc, err),
PersistenceError::EncodeError(ref err_loc, ref err) => write!(f, "{}:{}", err_loc, err),
PersistenceError::SqliteError(ref err_loc, ref err) => write!(f, "{}: {}", err_loc, err),
PersistenceError::WrongInsertCount{ref loc, got, expect } => write!(f, "{}: got {}, expected: {}", loc, got, expect),
PersistenceError::RecoveryError(ref err_loc, ref msg) => write!(f, "{}: error recovering: {}", err_loc, msg),
}
}
}
impl Error for PersistenceError {
fn description(&self) -> &str {
match *self {
PersistenceError::DecodeError(_, ref err) => err.description(),
PersistenceError::EncodeError(_, ref err) => err.description(),
PersistenceError::SqliteError(_, ref err) => err.description(),
PersistenceError::WrongInsertCount{ .. } => "an unexpected number of records were inserted",
PersistenceError::RecoveryError( .. ) => "error recovering from journal",
}
}
fn cause(&self) -> Option<&Error> {
match *self {
PersistenceError::DecodeError(_, ref err) => Some(err),
PersistenceError::EncodeError(_, ref err) => Some(err),
PersistenceError::SqliteError(_, ref err) => Some(err),
_ => None,
}
}
}

View File

@ -32,7 +32,9 @@ extern crate data_encoding;
extern crate mio;
extern crate openssl;
extern crate openssl_sys;
extern crate rusqlite;
extern crate rustc_serialize;
extern crate time;
extern crate toml;

View File

@ -13,6 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//! The `named` binary for running a DNS server
//!
//! ```text
//! Usage: named [options]
//! named (-h | --help | --version)
//!
//! Options:
//! -q, --quiet Disable INFO messages, WARN and ERROR will remain
//! -d, --debug Turn on DEBUG messages (default is only INFO)
//! -h, --help Show this message
//! -v, --version Show the version of trust-dns
//! -c FILE, --config=FILE Path to configuration file, default is /etc/named.toml
//! -z DIR, --zonedir=DIR Path to the root directory for all zone files, see also config toml
//! -p PORT, --port=PORT Override the listening port
//! ```
extern crate trust_dns;
extern crate rustc_serialize;
extern crate docopt;
@ -21,6 +38,7 @@ extern crate mio;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::collections::BTreeMap;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::net::ToSocketAddrs;
@ -31,8 +49,8 @@ use docopt::Docopt;
use trust_dns::logger;
use trust_dns::version;
use trust_dns::authority::{Catalog, Authority};
use trust_dns::config::Config;
use trust_dns::authority::{Authority, Catalog, Journal};
use trust_dns::config::{Config, ZoneConfig};
use trust_dns::serialize::txt::Parser;
use trust_dns::rr::Name;
use trust_dns::server::Server;
@ -64,9 +82,67 @@ struct Args {
pub flag_port: Option<u16>,
}
fn load_zone(zone_dir: &Path, zone: &ZoneConfig) -> Result<Authority, String> {
let zone_name: Name = zone.get_zone().expect("bad zone name");
let zone_path: PathBuf = zone_dir.to_owned().join(zone.get_file());
let journal_path: PathBuf = zone_path.with_extension(".jrnl");
if zone.is_update_allowed() && journal_path.exists() {
info!("recovering zone from journal: {:?}", journal_path);
let journal = match Journal::from_file(&journal_path) {
Ok(j) => j,
Err(e) => return Err(format!("error opening journal: {:?}: {}", journal_path, e)),
};
let mut authority = Authority::new(zone_name.clone(), BTreeMap::new(), zone.get_zone_type(), zone.is_update_allowed());
if let Err(e) = authority.recover_with_journal(&journal) {
return Err(format!("error recovering from journal: {}", e))
}
authority.journal(journal);
info!("recovered zone: {}", zone_name);
Ok(authority)
} else if zone_path.exists() {
info!("loading zone file: {:?}", zone_path);
let zone_file = match File::open(&zone_path) {
Ok(f) => f,
Err(e) => return Err(format!("error opening zone file: {:?}: {}", zone_path, e)),
};
let mut authority: Authority = match Parser::parse_file(zone_file, Some(zone_name.clone()), zone.get_zone_type(), zone.is_update_allowed()) {
Ok(a) => a,
Err(e) => return Err(format!("error reading zone: {:?}: {}", zone_path, e)),
};
// if dynamic update is enabled, enable the journal
if zone.is_update_allowed() {
info!("enabling journal: {:?}", journal_path);
let journal = match Journal::from_file(&journal_path) {
Ok(j) => j,
Err(e) => return Err(format!("error creating journal {:?}: {}", journal_path, e)),
};
authority.journal(journal);
// preserve to the new journal, i.e. we just loaded the zone from disk, start the journal
if let Err(e) = authority.persist_to_journal() {
return Err(format!("error persisting to journal {:?}: {}", journal_path, e))
}
}
info!("loaded zone: {}", zone_name);
Ok(authority)
} else {
Err(format!("no zone file defined at: {:?}", zone_path))
}
}
/// Main method for running the named server.
/// As of this writing, this will panic on any invalid input. At this top level binary is the only
/// part Trust-DNS where panics are allowed.
///
/// `Note`: Tries to avoid panics, in favor of always starting.
pub fn main() {
// read any command line options
let args: Args = Docopt::new(USAGE)
@ -93,15 +169,12 @@ pub fn main() {
let mut catalog: Catalog = Catalog::new();
// configure our server based on the config_path
for zone in config.get_zones() {
let zone_name: Name = zone.get_zone().unwrap();
let zone_path: PathBuf = zone_dir.to_owned().join(zone.get_file());
info!("loading zone file: {:?}", zone_path);
let zone_name = zone.get_zone().expect(&format!("bad zone name in {:?}", config_path));
let zone_file = File::open(zone_path).unwrap();
let authority: Authority = Parser::parse_file(zone_file, Some(zone_name.clone()), zone.get_zone_type(), zone.get_allow_udpate()).unwrap();
info!("loaded zone: {}", zone_name);
catalog.upsert(zone_name, authority);
match load_zone(zone_dir, zone) {
Ok(authority) => catalog.upsert(zone_name, authority),
Err(error) => error!("could not load zone {}: {}", zone_name, error),
}
}
// TODO support all the IPs asked to listen on...

View File

@ -275,7 +275,7 @@ impl BinSerializable<Name> for Name {
}
},
LabelParseState::Label => {
labels.push(Rc::new(try!(decoder.read_character_data())));
labels.push(Rc::new(try!(decoder.read_character_data(true))));
// reset to collect more data
LabelParseState::LabelLengthOrPointer

View File

@ -20,7 +20,7 @@ use ::serialize::binary::*;
use ::error::*;
use ::rr::dnssec::Algorithm;
/// [RFC 4034, DNSSEC Resource Records, March 2005](https://tools.ietf.org/html/rfc4034#section-2)
/// [RFC 4034](https://tools.ietf.org/html/rfc4034#section-2), DNSSEC Resource Records, March 2005
///
/// ```text
/// 2. The DNSKEY Resource Record

View File

@ -53,7 +53,7 @@ use ::rr::dnssec::Algorithm;
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
///
/// ```
/// [RFC 2931, DNS Request and Transaction Signatures, September 2000](https://tools.ietf.org/html/rfc2931)
/// [RFC 2931](https://tools.ietf.org/html/rfc2931), DNS Request and Transaction Signatures, September 2000
///
/// NOTE: 2931 updates SIG0 to clarify certain particulars...
///

View File

@ -61,7 +61,7 @@ pub fn read(decoder: &mut BinDecoder, rdata_length: u16) -> DecodeResult<TXT> {
let mut strings = Vec::with_capacity(1);
while data_len - decoder.len() < rdata_length as usize {
strings.push(try!(decoder.read_character_data()));
strings.push(try!(decoder.read_character_data(false)));
}
Ok(TXT::new(strings))
}
@ -88,7 +88,7 @@ pub fn parse(tokens: &Vec<Token>) -> ParseResult<TXT> {
#[test]
fn test() {
let rdata = TXT::new(vec!["test me some".to_string(), "more please".to_string()]);
let rdata = TXT::new(vec!["Test me some".to_string(), "more please".to_string()]);
let mut bytes = Vec::new();
let mut encoder: BinEncoder = BinEncoder::new(&mut bytes);

View File

@ -31,7 +31,7 @@ use super::rdata::{ DNSKEY, DS, MX, NSEC, NSEC3, NSEC3PARAM, NULL, OPT, SIG, SOA
/// Record data enum variants
///
/// [RFC 1035, DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION, November 1987](https://tools.ietf.org/html/rfc1035)
/// [RFC 1035](https://tools.ietf.org/html/rfc1035), DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION, November 1987
///
/// ```text
/// 3.3. Standard RRs

View File

@ -27,62 +27,40 @@ use super::record_type::RecordType;
use super::dns_class::DNSClass;
use super::domain;
/*
* RFC 1035 Domain Implementation and Specification November 1987
*
* 4.1.3. Resource record format
*
* The answer, authority, and additional sections all share the same
* format: a variable number of resource records, where the number of
* records is specified in the corresponding count field in the header.
* Each resource record has the following format:
* 1 1 1 1 1 1
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* | |
* / /
* / NAME /
* | |
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* | TYPE |
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* | CLASS |
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* | TTL |
* | |
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
* | RDLENGTH |
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
* / RDATA /
* / /
* +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
*
* where:
*
* NAME a domain name to which this resource record pertains.
*
* TYPE two octets containing one of the RR type codes. This
* field specifies the meaning of the data in the RDATA
* field.
*
* CLASS two octets which specify the class of the data in the
* RDATA field.
*
* TTL a 32 bit unsigned integer that specifies the time
* interval (in seconds) that the resource record may be
* cached before it should be discarded. Zero values are
* interpreted to mean that the RR can only be used for the
* transaction in progress, and should not be cached.
*
* RDLENGTH an unsigned 16 bit integer that specifies the length in
* octets of the RDATA field.
*
* RDATA a variable length string of octets that describes the
* resource. The format of this information varies
* according to the TYPE and CLASS of the resource record.
* For example, the if the TYPE is A and the CLASS is IN,
* the RDATA field is a 4 octet ARPA Internet address.
*/
/// Resource records are storage value in DNS, into which all key/value pair data is stored.
///
/// [RFC 1035](https://tools.ietf.org/html/rfc1035), DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION, November 1987
///
/// ```text
/// 4.1.3. Resource record format
///
/// The answer, authority, and additional sections all share the same
/// format: a variable number of resource records, where the number of
/// records is specified in the corresponding count field in the header.
/// Each resource record has the following format:
/// 1 1 1 1 1 1
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | |
/// / /
/// / NAME /
/// | |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | TYPE |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | CLASS |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | TTL |
/// | |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
/// | RDLENGTH |
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/// / RDATA /
/// / /
/// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
///
/// ```
#[derive(Eq, Ord, Debug, Clone)]
pub struct Record {
name_labels: domain::Name,
@ -125,11 +103,41 @@ impl Record {
}
}
/// ```text
/// NAME a domain name to which this resource record pertains.
/// ```
pub fn name(&mut self, name: domain::Name) -> &mut Self { self.name_labels = name; self }
pub fn add_name(&mut self, label: String) -> &mut Self { self.name_labels.add_label(Rc::new(label)); self }
/// ```text
/// TYPE two octets containing one of the RR type codes. This
/// field specifies the meaning of the data in the RDATA
/// field.
/// ```
pub fn rr_type(&mut self, rr_type: RecordType) -> &mut Self { self.rr_type = rr_type; self }
/// ```text
/// CLASS two octets which specify the class of the data in the
/// RDATA field.
/// ```
pub fn dns_class(&mut self, dns_class: DNSClass) -> &mut Self { self.dns_class = dns_class; self }
/// ```text
/// TTL a 32 bit unsigned integer that specifies the time
/// interval (in seconds) that the resource record may be
/// cached before it should be discarded. Zero values are
/// interpreted to mean that the RR can only be used for the
/// transaction in progress, and should not be cached.
/// ```
pub fn ttl(&mut self, ttl: u32) -> &mut Self { self.ttl = ttl; self }
/// ```text
/// RDATA a variable length string of octets that describes the
/// resource. The format of this information varies
/// according to the TYPE and CLASS of the resource record.
/// For example, the if the TYPE is A and the CLASS is IN,
/// the RDATA field is a 4 octet ARPA Internet address.
/// ```
pub fn rdata(&mut self, rdata: RData) -> &mut Self { self.rdata = rdata; self }
pub fn get_name(&self) -> &domain::Name { &self.name_labels }
@ -222,26 +230,29 @@ impl BinSerializable<Record> for Record {
}
}
/// RFC 2136 DNS Update April 1997
///
/// 1.1.1. Two RRs are considered equal if their NAME, CLASS, TYPE,
/// RDLENGTH and RDATA fields are equal. Note that the time-to-live
/// (TTL) field is explicitly excluded from the comparison.
///
/// 1.1.2. The rules for comparison of character strings in names are
/// specified in [RFC1035 2.3.3]. i.e. case insensitive
impl PartialEq for Record {
fn eq(&self, other: &Self) -> bool {
// self == other && // the same pointer
self.name_labels == other.name_labels &&
self.rr_type == other.rr_type &&
self.dns_class == other.dns_class &&
self.rdata == other.rdata
}
/// Equality or records, as defined by
/// [RFC 2136](https://tools.ietf.org/html/rfc2136), DNS Update, April 1997
///
/// ```text
/// 1.1.1. Two RRs are considered equal if their NAME, CLASS, TYPE,
/// RDLENGTH and RDATA fields are equal. Note that the time-to-live
/// (TTL) field is explicitly excluded from the comparison.
///
/// 1.1.2. The rules for comparison of character strings in names are
/// specified in [RFC1035 2.3.3]. i.e. case insensitive
/// ```
fn eq(&self, other: &Self) -> bool {
// self == other && // the same pointer
self.name_labels == other.name_labels &&
self.rr_type == other.rr_type &&
self.dns_class == other.dns_class &&
self.rdata == other.rdata
}
fn ne(&self, other: &Self) -> bool {
!self.eq(other)
}
fn ne(&self, other: &Self) -> bool {
!self.eq(other)
}
}
/// returns the value of the compare if the items are greater or lesser, but coninues on equal
@ -256,32 +267,35 @@ macro_rules! compare_or_equal {
}
impl PartialOrd<Record> for Record {
// RFC 4034 DNSSEC Resource Records March 2005
//
// 6.2. Canonical RR Form
//
// For the purposes of DNS security, the canonical form of an RR is the
// wire format of the RR where:
//
// 1. every domain name in the RR is fully expanded (no DNS name
// compression) and fully qualified;
//
// 2. all uppercase US-ASCII letters in the owner name of the RR are
// replaced by the corresponding lowercase US-ASCII letters;
//
// 3. if the type of the RR is NS, MD, MF, CNAME, SOA, MB, MG, MR, PTR,
// HINFO, MINFO, MX, HINFO, RP, AFSDB, RT, SIG, PX, NXT, NAPTR, KX,
// SRV, DNAME, A6, RRSIG, or NSEC, all uppercase US-ASCII letters in
// the DNS names contained within the RDATA are replaced by the
// corresponding lowercase US-ASCII letters;
//
// 4. if the owner name of the RR is a wildcard name, the owner name is
// in its original unexpanded form, including the "*" label (no
// wildcard substitution); and
//
// 5. the RR's TTL is set to its original value as it appears in the
// originating authoritative zone or the Original TTL field of the
// covering RRSIG RR.
/// Canonical ordering as defined by
/// [RFC 4034](https://tools.ietf.org/html/rfc4034#section-6), DNSSEC Resource Records, March 2005
///
/// ```text
/// 6.2. Canonical RR Form
///
/// For the purposes of DNS security, the canonical form of an RR is the
/// wire format of the RR where:
///
/// 1. every domain name in the RR is fully expanded (no DNS name
/// compression) and fully qualified;
///
/// 2. all uppercase US-ASCII letters in the owner name of the RR are
/// replaced by the corresponding lowercase US-ASCII letters;
///
/// 3. if the type of the RR is NS, MD, MF, CNAME, SOA, MB, MG, MR, PTR,
/// HINFO, MINFO, MX, HINFO, RP, AFSDB, RT, SIG, PX, NXT, NAPTR, KX,
/// SRV, DNAME, A6, RRSIG, or NSEC, all uppercase US-ASCII letters in
/// the DNS names contained within the RDATA are replaced by the
/// corresponding lowercase US-ASCII letters;
///
/// 4. if the owner name of the RR is a wildcard name, the owner name is
/// in its original unexpanded form, including the "*" label (no
/// wildcard substitution); and
///
/// 5. the RR's TTL is set to its original value as it appears in the
/// originating authoritative zone or the Original TTL field of the
/// covering RRSIG RR.
/// ```
fn partial_cmp(&self, other: &Record) -> Option<Ordering> {
// TODO: given that the ordering of Resource Records is dependent on it's binary form and this
// method will be used during insertion sort or similar, we should probably do this

View File

@ -28,7 +28,7 @@ fn get_character_data() -> Vec<(String, Vec<u8>)> {
#[test]
fn read_character_data() {
test_read_data_set(get_character_data(), |mut d| d.read_character_data());
test_read_data_set(get_character_data(), |mut d| d.read_character_data(false));
}
#[test]

View File

@ -73,14 +73,19 @@ impl<'a> BinDecoder<'a> {
/// length (including the length octet).
///
/// the vector should be reversed before calling.
pub fn read_character_data(&mut self) -> DecodeResult<String> {
pub fn read_character_data(&mut self, lowercase: bool) -> DecodeResult<String> {
let length: u8 = try!(self.pop());
// TODO once Drain stabalizes on Vec, this should be replaced...
let label_vec: Vec<u8> = try!(self.read_vec(length as usize));
// translate bytes to string, then lowercase...
Ok(try!(String::from_utf8(label_vec)).to_lowercase())
let data = try!(String::from_utf8(label_vec));
if lowercase {
Ok(data.to_lowercase())
} else {
Ok(data)
}
}
pub fn read_vec(&mut self, len: usize) -> DecodeResult<Vec<u8>> {

View File

@ -141,6 +141,7 @@ impl Parser {
Self::new().parse(lexer, origin, zone_type, allow_update)
}
// TODO: change this function to load into an Authority, using the update_records() method
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: BTreeMap<RrKey, RRSet> = BTreeMap::new();

View File

@ -1,3 +1,10 @@
// Copyright 2015-2016 Benjamin Fry
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use std::iter::Peekable;
use std::str::Chars;
use std::char;

View File

@ -338,29 +338,18 @@ mod server_tests {
use ::udp::UdpClientConnection;
#[cfg(feature = "ftest")]
use ::tcp::TcpClientConnection;
#[cfg(feature = "ftest")]
use mio::tcp::TcpListener;
#[test]
fn test_server_www_udp() {
// use log::LogLevel;
// use ::logger;
// logger::TrustDnsLogger::enable_logging(LogLevel::Debug);
let example = create_example();
let origin = example.get_origin().clone();
let mut catalog: Catalog = Catalog::new();
catalog.upsert(origin.clone(), example);
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127,0,0,1), 0));
let udp_socket = UdpSocket::bound(&addr).unwrap();
let ipaddr = udp_socket.local_addr().unwrap();
println!("udp_socket on port: {}", ipaddr);
let mut server = Server::new(catalog);
server.register_socket(udp_socket);
/*let server_thread = */thread::Builder::new().name("test_server:udp:server".to_string()).spawn(move || server_thread(server)).unwrap();
thread::Builder::new().name("test_server:udp:server".to_string()).spawn(move || server_thread_udp(udp_socket)).unwrap();
let client_conn = UdpClientConnection::new(ipaddr).unwrap();
let client_thread = thread::Builder::new().name("test_server:udp:client".to_string()).spawn(move || client_thread_www(client_conn)).unwrap();
@ -377,26 +366,13 @@ mod server_tests {
fn test_server_www_tcp() {
use mio::tcp::TcpListener;
// use log::LogLevel;
// use ::logger;
// logger::TrustDnsLogger::enable_logging(LogLevel::Debug);
let example = create_example();
let origin = example.get_origin().clone();
let mut catalog: Catalog = Catalog::new();
catalog.upsert(origin.clone(), example);
let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127,0,0,1), 0));
let tcp_listener = TcpListener::bind(&addr).unwrap();
let ipaddr = tcp_listener.local_addr().unwrap();
println!("tcp_listner on port: {}", ipaddr);
let mut server = Server::new(catalog);
server.register_listener(tcp_listener);
/*let server_thread = */thread::Builder::new().name("test_server:tcp:server".to_string()).spawn(move || server_thread(server)).unwrap();
thread::Builder::new().name("test_server:tcp:server".to_string()).spawn(move || server_thread_tcp(tcp_listener)).unwrap();
let client_conn = TcpClientConnection::new(ipaddr).unwrap();
let client_thread = thread::Builder::new().name("test_server:tcp:client".to_string()).spawn(move || client_thread_www(client_conn)).unwrap();
@ -439,7 +415,30 @@ mod server_tests {
assert_eq!(ns.last().unwrap().get_rdata(), &RData::NS(Name::parse("b.iana-servers.net.", None).unwrap()) );
}
fn server_thread(mut server: Server) {
fn new_catalog() -> Catalog {
let example = create_example();
let origin = example.get_origin().clone();
let mut catalog: Catalog = Catalog::new();
catalog.upsert(origin.clone(), example);
catalog
}
fn server_thread_udp(udp_socket: UdpSocket) {
let catalog = new_catalog();
let mut server = Server::new(catalog);
server.register_socket(udp_socket);
server.listen().unwrap();
}
#[cfg(feature = "ftest")]
fn server_thread_tcp(tcp_listener: TcpListener) {
let catalog = new_catalog();
let mut server = Server::new(catalog);
server.register_listener(tcp_listener);
server.listen().unwrap();
}
}