fixed nsec3 hashing and base32hex encoding
This commit is contained in:
parent
79a9a4f2ca
commit
4c62e0349a
@ -5,6 +5,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
## [unreleased]
|
||||
### Added
|
||||
- NSEC3 resolver validation
|
||||
- data-ecoding as a dependency (base32hex)
|
||||
- trust-dns banner on boot of server
|
||||
|
||||
### Changed
|
||||
- Changed the bin.rs to named.rs, more accurate, allow for other binaries
|
||||
|
||||
## 0.5.0 2016-03-22
|
||||
### Added
|
||||
|
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -3,6 +3,7 @@ name = "trust-dns"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"chrono 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"data-encoding 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"docopt 0.6.78 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mio 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@ -44,6 +45,11 @@ dependencies = [
|
||||
"time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "docopt"
|
||||
version = "0.6.78"
|
||||
|
13
Cargo.toml
13
Cargo.toml
@ -40,7 +40,7 @@ path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "named"
|
||||
path = "src/bin.rs"
|
||||
path = "src/named.rs"
|
||||
|
||||
|
||||
[profile.dev]
|
||||
@ -62,11 +62,12 @@ debug-assertions = true
|
||||
codegen-units = 1
|
||||
|
||||
[dependencies]
|
||||
rustc-serialize = "^0.3.16"
|
||||
log = "^0.3.1"
|
||||
docopt = "^0.6.72"
|
||||
mio = "^0.4.4"
|
||||
toml = "^0.1.22"
|
||||
chrono = "^0.2.16"
|
||||
data-encoding = "^1.1.2"
|
||||
docopt = "^0.6.72"
|
||||
log = "^0.3.1"
|
||||
mio = "^0.4.4"
|
||||
openssl = "^0.7.8"
|
||||
openssl-sys = "^0.7.8"
|
||||
rustc-serialize = "^0.3.16"
|
||||
toml = "^0.1.22"
|
||||
|
@ -14,7 +14,9 @@
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc as Rc;
|
||||
|
||||
use data_encoding::base32hex;
|
||||
use openssl::crypto::pkey::Role;
|
||||
|
||||
use ::error::*;
|
||||
@ -103,6 +105,7 @@ impl<C: ClientConnection> Client<C> {
|
||||
},
|
||||
rt @ RecordType::NSEC3 => {
|
||||
try!(self.verify_nsec3(query_name, query_type, query_class,
|
||||
record_response.get_name_servers().iter().filter(|rr| rr.get_rr_type() == RecordType::SOA).next(),
|
||||
record_response.get_name_servers().iter().filter(|rr| rr.get_rr_type() == rt).collect()));
|
||||
validated_nx = true;
|
||||
},
|
||||
@ -325,11 +328,119 @@ impl<C: ClientConnection> Client<C> {
|
||||
Err(ClientError::InvalidNsec)
|
||||
}
|
||||
|
||||
|
||||
// Laurie, et al. Standards Track [Page 22]
|
||||
//
|
||||
// RFC 5155 NSEC3 March 2008
|
||||
//
|
||||
//
|
||||
// 8. Validator Considerations
|
||||
//
|
||||
// 8.1. Responses with Unknown Hash Types
|
||||
//
|
||||
// A validator MUST ignore NSEC3 RRs with unknown hash types. The
|
||||
// practical result of this is that responses containing only such NSEC3
|
||||
// RRs will generally be considered bogus.
|
||||
//
|
||||
// 8.2. Verifying NSEC3 RRs
|
||||
//
|
||||
// A validator MUST ignore NSEC3 RRs with a Flag fields value other than
|
||||
// zero or one.
|
||||
//
|
||||
// A validator MAY treat a response as bogus if the response contains
|
||||
// NSEC3 RRs that contain different values for hash algorithm,
|
||||
// iterations, or salt from each other for that zone.
|
||||
//
|
||||
// 8.3. Closest Encloser Proof
|
||||
//
|
||||
// In order to verify a closest encloser proof, the validator MUST find
|
||||
// the longest name, X, such that
|
||||
//
|
||||
// o X is an ancestor of QNAME that is matched by an NSEC3 RR present
|
||||
// in the response. This is a candidate for the closest encloser,
|
||||
// and
|
||||
//
|
||||
// o The name one label longer than X (but still an ancestor of -- or
|
||||
// equal to -- QNAME) is covered by an NSEC3 RR present in the
|
||||
// response.
|
||||
//
|
||||
// One possible algorithm for verifying this proof is as follows:
|
||||
//
|
||||
// 1. Set SNAME=QNAME. Clear the flag.
|
||||
//
|
||||
// 2. Check whether SNAME exists:
|
||||
//
|
||||
// * If there is no NSEC3 RR in the response that matches SNAME
|
||||
// (i.e., an NSEC3 RR whose owner name is the same as the hash of
|
||||
// SNAME, prepended as a single label to the zone name), clear
|
||||
// the flag.
|
||||
//
|
||||
// * If there is an NSEC3 RR in the response that covers SNAME, set
|
||||
// the flag.
|
||||
//
|
||||
// * If there is a matching NSEC3 RR in the response and the flag
|
||||
// was set, then the proof is complete, and SNAME is the closest
|
||||
// encloser.
|
||||
//
|
||||
// * If there is a matching NSEC3 RR in the response, but the flag
|
||||
// is not set, then the response is bogus.
|
||||
//
|
||||
// 3. Truncate SNAME by one label from the left, go to step 2.
|
||||
//
|
||||
// Once the closest encloser has been discovered, the validator MUST
|
||||
// check that the NSEC3 RR that has the closest encloser as the original
|
||||
// owner name is from the proper zone. The DNAME type bit must not be
|
||||
// set and the NS type bit may only be set if the SOA type bit is set.
|
||||
// If this is not the case, it would be an indication that an attacker
|
||||
// is using them to falsely deny the existence of RRs for which the
|
||||
// server is not authoritative.
|
||||
//
|
||||
// In the following descriptions, the phrase "a closest (provable)
|
||||
// encloser proof for X" means that the algorithm above (or an
|
||||
// equivalent algorithm) proves that X does not exist by proving that an
|
||||
// ancestor of X is its closest encloser.
|
||||
fn verify_nsec3(&self, query_name: &domain::Name, query_type: RecordType,
|
||||
query_class: DNSClass, nsec3: Vec<&Record>) -> ClientResult<()> {
|
||||
query_class: DNSClass, soa: Option<&Record>,
|
||||
nsec3s: Vec<&Record>) -> ClientResult<()> {
|
||||
// the search name is the one to look for
|
||||
let zone_name = try!(soa.ok_or(ClientError::NoSOARecord(query_name.clone()))).get_name();
|
||||
|
||||
unimplemented!();
|
||||
println!("nsec3s: {:?}", nsec3s);
|
||||
|
||||
for nsec3 in nsec3s {
|
||||
// for each nsec3 we search for matching hashed names
|
||||
let mut search_name: domain::Name = query_name.clone();
|
||||
|
||||
// hash the search name
|
||||
if let &RData::NSEC3{hash_algorithm, opt_out, iterations, ref salt, ref type_bit_maps, ..} = nsec3.get_rdata() {
|
||||
println!("nsec3_name: {}", nsec3.get_name());
|
||||
|
||||
// search all the name options
|
||||
while search_name.num_labels() >= zone_name.num_labels() {
|
||||
|
||||
// TODO: cache hashes across nsec3 validations
|
||||
let hash = hash_algorithm.hash(salt, &search_name, iterations);
|
||||
let hash_label = base32hex::encode(&hash).to_lowercase();
|
||||
let hash_name = zone_name.prepend_label(Rc::new(hash_label));
|
||||
|
||||
println!("hash_name: {}", hash_name);
|
||||
|
||||
if &hash_name == nsec3.get_name() {
|
||||
// like nsec, if there is a name that matches, then we have proof that the name does
|
||||
// not exist
|
||||
if &search_name == query_name {
|
||||
if !type_bit_maps.contains(&query_type) { return Ok(()) }
|
||||
}
|
||||
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// need to continue up the chain
|
||||
search_name = search_name.base_name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(ClientError::InvalidNsec3)
|
||||
}
|
||||
|
||||
// send a DNS query to the name_server specified in Client.
|
||||
@ -540,37 +651,19 @@ mod test {
|
||||
assert!(response.get_response_code() == ResponseCode::NXDomain);
|
||||
}
|
||||
|
||||
// // TODO: use this site for verifying nsec3
|
||||
// #[test]
|
||||
// #[cfg(feature = "ftest")]
|
||||
// fn test_secure_query_sdsmt() {
|
||||
// use std::net::*;
|
||||
//
|
||||
// use ::rr::dns_class::DNSClass;
|
||||
// use ::rr::record_type::RecordType;
|
||||
// use ::rr::domain;
|
||||
// use ::rr::record_data::RData;
|
||||
// use ::udp::Client;
|
||||
//
|
||||
// let name = domain::Name::with_labels(vec!["www".to_string(), "sdsmt".to_string(), "edu".to_string()]);
|
||||
// let client = Client::new(("8.8.8.8").parse().unwrap()).unwrap();
|
||||
//
|
||||
// let response = client.secure_query(&name, DNSClass::IN, RecordType::A);
|
||||
// assert!(response.is_ok(), "query failed: {}", response.unwrap_err());
|
||||
//
|
||||
// let response = response.unwrap();
|
||||
//
|
||||
// println!("response records: {:?}", response);
|
||||
//
|
||||
// let record = &response.get_answers()[0];
|
||||
// assert_eq!(record.get_name(), &name);
|
||||
// assert_eq!(record.get_rr_type(), RecordType::A);
|
||||
// assert_eq!(record.get_dns_class(), DNSClass::IN);
|
||||
//
|
||||
// if let &RData::A{ ref address } = record.get_rdata() {
|
||||
// assert_eq!(address, &Ipv4Addr::new(93,184,216,34))
|
||||
// } else {
|
||||
// assert!(false);
|
||||
// }
|
||||
// }
|
||||
// TODO: use this site for verifying nsec3
|
||||
#[test]
|
||||
#[cfg(feature = "ftest")]
|
||||
fn test_secure_query_sdsmt() {
|
||||
let addr: SocketAddr = ("75.75.75.75",53).to_socket_addrs().unwrap().next().unwrap();
|
||||
let conn = TcpClientConnection::new(addr).unwrap();
|
||||
let name = domain::Name::with_labels(vec!["www".to_string(), "sdsmt".to_string(), "edu".to_string()]);
|
||||
let client = Client::new(conn);
|
||||
|
||||
let response = client.secure_query(&name, DNSClass::IN, RecordType::NS);
|
||||
assert!(response.is_ok(), "query failed: {}", response.unwrap_err());
|
||||
|
||||
let response = response.unwrap();
|
||||
assert!(response.get_response_code() == ResponseCode::NXDomain);
|
||||
}
|
||||
}
|
||||
|
@ -14,12 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
extern crate chrono;
|
||||
extern crate data_encoding;
|
||||
#[macro_use] extern crate log;
|
||||
extern crate mio;
|
||||
extern crate openssl;
|
||||
extern crate openssl_sys;
|
||||
extern crate toml;
|
||||
extern crate rustc_serialize;
|
||||
extern crate toml;
|
||||
|
||||
|
||||
pub mod logger;
|
||||
pub mod rr;
|
||||
|
@ -130,9 +130,9 @@ pub fn main() {
|
||||
for tcp_listener in tcp_listeners {
|
||||
info!("listening for TCP on {:?}", tcp_listener);
|
||||
server.register_listener(tcp_listener);
|
||||
|
||||
}
|
||||
|
||||
banner();
|
||||
if let Err(e) = server.listen() {
|
||||
error!("failed to listen: {}", e);
|
||||
}
|
||||
@ -143,3 +143,12 @@ pub fn main() {
|
||||
// we're exiting for some reason...
|
||||
info!("Trust-DNS {} stopping", trust_dns::version());
|
||||
}
|
||||
|
||||
fn banner() {
|
||||
println!(" o o o ");
|
||||
println!(" | | | ");
|
||||
println!(" -o- o-o o o o-o -o- o-o o-O o-o o-o ");
|
||||
println!(" | | | | \\ | | | | | \\ ");
|
||||
println!(" o o o--o o-o o o-o o o o-o ");
|
||||
println!(" ");
|
||||
}
|
@ -13,7 +13,13 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
use std::io::Write;
|
||||
use openssl::crypto::hash;
|
||||
|
||||
use ::error::*;
|
||||
use ::rr::dnssec::DigestType;
|
||||
use ::rr::Name;
|
||||
use ::serialize::binary::{BinEncoder, BinSerializable};
|
||||
|
||||
// RFC 5155 NSEC3 March 2008
|
||||
//
|
||||
@ -62,12 +68,6 @@ use ::error::*;
|
||||
// registry is named "DNSSEC NSEC3PARAM Flags". The initial contents of
|
||||
// this registry are:
|
||||
//
|
||||
//
|
||||
//
|
||||
// Laurie, et al. Standards Track [Page 29]
|
||||
//
|
||||
//
|
||||
//
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | | | | | | | | 0 |
|
||||
@ -106,8 +106,65 @@ impl Nsec3HashAlgorithm {
|
||||
_ => Err(DecodeError::UnknownAlgorithmTypeValue(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Laurie, et al. Standards Track [Page 14]
|
||||
//
|
||||
// RFC 5155 NSEC3 March 2008
|
||||
//
|
||||
// Define H(x) to be the hash of x using the Hash Algorithm selected by
|
||||
// the NSEC3 RR, k to be the number of Iterations, and || to indicate
|
||||
// concatenation. Then define:
|
||||
//
|
||||
// IH(salt, x, 0) = H(x || salt), and
|
||||
//
|
||||
// IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0
|
||||
//
|
||||
// Then the calculated hash of an owner name is
|
||||
//
|
||||
// IH(salt, owner name, iterations),
|
||||
//
|
||||
// where the owner name is in the canonical form, defined as:
|
||||
//
|
||||
// The wire format of the owner name where:
|
||||
//
|
||||
// 1. The owner name is fully expanded (no DNS name compression) and
|
||||
// fully qualified;
|
||||
//
|
||||
// 2. All uppercase US-ASCII letters are replaced by the corresponding
|
||||
// lowercase US-ASCII letters;
|
||||
//
|
||||
// 3. If the owner name is a wildcard name, the owner name is in its
|
||||
// original unexpanded form, including the "*" label (no wildcard
|
||||
// substitution);
|
||||
pub fn hash(&self, salt: &[u8], name: &Name, iterations: u16) -> Vec<u8> {
|
||||
match *self {
|
||||
// if there ever is more than just SHA1 support, this should be a genericized method
|
||||
Nsec3HashAlgorithm::SHA1 => {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
{
|
||||
let mut encoder: BinEncoder = BinEncoder::new(&mut buf);
|
||||
encoder.set_canonical_names(true);
|
||||
name.emit(&mut encoder).expect("could not encode Name");
|
||||
}
|
||||
|
||||
Self::sha1_recursive_hash(salt, buf, iterations)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// until there is another supported algorithm, just hardcoded to this.
|
||||
fn sha1_recursive_hash(salt: &[u8], bytes: Vec<u8>, iterations: u16) -> Vec<u8> {
|
||||
let mut hasher: hash::Hasher = hash::Hasher::new(DigestType::SHA1.to_hash());
|
||||
|
||||
if iterations > 0 {
|
||||
hasher.write_all(&Self::sha1_recursive_hash(salt, bytes, iterations - 1)).expect("hasher failed");
|
||||
} else {
|
||||
hasher.write_all(&bytes).expect("hasher failed");
|
||||
}
|
||||
hasher.write_all(salt).expect("hasher failed");
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Nsec3HashAlgorithm> for u8 {
|
||||
fn from(a: Nsec3HashAlgorithm) -> u8 {
|
||||
@ -116,3 +173,61 @@ impl From<Nsec3HashAlgorithm> for u8 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash() {
|
||||
|
||||
let name = Name::new().label("www").label("example").label("com");
|
||||
let salt: Vec<u8> = vec![1,2,3,4];
|
||||
|
||||
assert_eq!(Nsec3HashAlgorithm::SHA1.hash(&salt, &name, 0).len(), 20);
|
||||
assert_eq!(Nsec3HashAlgorithm::SHA1.hash(&salt, &name, 1).len(), 20);
|
||||
assert_eq!(Nsec3HashAlgorithm::SHA1.hash(&salt, &name, 3).len(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_known_hashes() {
|
||||
// H(example) = 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom
|
||||
assert_eq!(hash_with_base32("example"), "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom");
|
||||
|
||||
// H(a.example) = 35mthgpgcu1qg68fab165klnsnk3dpvl
|
||||
assert_eq!(hash_with_base32("a.example"), "35mthgpgcu1qg68fab165klnsnk3dpvl");
|
||||
|
||||
// H(ai.example) = gjeqe526plbf1g8mklp59enfd789njgi
|
||||
assert_eq!(hash_with_base32("ai.example"), "gjeqe526plbf1g8mklp59enfd789njgi");
|
||||
|
||||
// H(ns1.example) = 2t7b4g4vsa5smi47k61mv5bv1a22bojr
|
||||
assert_eq!(hash_with_base32("ns1.example"), "2t7b4g4vsa5smi47k61mv5bv1a22bojr");
|
||||
|
||||
// H(ns2.example) = q04jkcevqvmu85r014c7dkba38o0ji5r
|
||||
assert_eq!(hash_with_base32("ns2.example"), "q04jkcevqvmu85r014c7dkba38o0ji5r");
|
||||
|
||||
// H(w.example) = k8udemvp1j2f7eg6jebps17vp3n8i58h
|
||||
assert_eq!(hash_with_base32("w.example"), "k8udemvp1j2f7eg6jebps17vp3n8i58h");
|
||||
|
||||
// H(*.w.example) = r53bq7cc2uvmubfu5ocmm6pers9tk9en
|
||||
assert_eq!(hash_with_base32("*.w.example"), "r53bq7cc2uvmubfu5ocmm6pers9tk9en");
|
||||
|
||||
// H(x.w.example) = b4um86eghhds6nea196smvmlo4ors995
|
||||
assert_eq!(hash_with_base32("x.w.example"), "b4um86eghhds6nea196smvmlo4ors995");
|
||||
|
||||
// H(y.w.example) = ji6neoaepv8b5o6k4ev33abha8ht9fgc
|
||||
assert_eq!(hash_with_base32("y.w.example"), "ji6neoaepv8b5o6k4ev33abha8ht9fgc");
|
||||
|
||||
// H(x.y.w.example) = 2vptu5timamqttgl4luu9kg21e0aor3s
|
||||
assert_eq!(hash_with_base32("x.y.w.example"), "2vptu5timamqttgl4luu9kg21e0aor3s");
|
||||
|
||||
// H(xx.example) = t644ebqk9bibcna874givr6joj62mlhv
|
||||
assert_eq!(hash_with_base32("xx.example"), "t644ebqk9bibcna874givr6joj62mlhv");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn hash_with_base32(name: &str) -> String {
|
||||
use data_encoding::base32hex;
|
||||
|
||||
// NSEC3PARAM 1 0 12 aabbccdd
|
||||
let known_name = Name::parse(name, Some(&Name::new())).unwrap();
|
||||
let known_salt = [0xAAu8, 0xBBu8, 0xCCu8, 0xDDu8,];
|
||||
let hash = Nsec3HashAlgorithm::SHA1.hash(&known_salt, &known_name, 12);
|
||||
base32hex::encode(&hash).to_lowercase()
|
||||
}
|
||||
|
@ -44,31 +44,46 @@ impl Name {
|
||||
self.labels.is_empty()
|
||||
}
|
||||
|
||||
// inline builder
|
||||
/// inline builder
|
||||
pub fn label(mut self, label: &'static str) -> Self {
|
||||
// TODO get_mut() on Arc was unstable when this was written
|
||||
let mut new_labels: Vec<Rc<String>> = (*self.labels).clone();
|
||||
new_labels.push(Rc::new(label.into()));
|
||||
self.labels = Rc::new(new_labels);
|
||||
assert!(self.labels.len() < 256);
|
||||
assert!(self.labels.len() < 256); // this should be an error
|
||||
self
|
||||
}
|
||||
|
||||
// for mutating over time
|
||||
/// for mutating over time
|
||||
pub fn with_labels(labels: Vec<String>) -> Self {
|
||||
assert!(labels.len() < 256);
|
||||
assert!(labels.len() < 256); // this should be an error
|
||||
Name { labels: Rc::new(labels.into_iter().map(|s|Rc::new(s)).collect()) }
|
||||
}
|
||||
|
||||
/// prepend the String to the label
|
||||
pub fn prepend_label(&self, label: Rc<String>) -> Self {
|
||||
let mut new_labels: Vec<Rc<String>> = Vec::with_capacity(self.labels.len() + 1);
|
||||
new_labels.push(label);
|
||||
|
||||
for label in &*self.labels {
|
||||
new_labels.push(label.clone());
|
||||
}
|
||||
|
||||
assert!(new_labels.len() < 256); // this should be an error
|
||||
Name{ labels: Rc::new(new_labels) }
|
||||
}
|
||||
|
||||
/// appends the String to this label at the end
|
||||
pub fn add_label(&mut self, label: Rc<String>) -> &mut Self {
|
||||
// TODO get_mut() on Arc was unstable when this was written
|
||||
let mut new_labels: Vec<Rc<String>> = (*self.labels).clone();
|
||||
new_labels.push(label);
|
||||
self.labels = Rc::new(new_labels);
|
||||
assert!(self.labels.len() < 256);
|
||||
assert!(self.labels.len() < 256); // this should be an error
|
||||
self
|
||||
}
|
||||
|
||||
/// appends the other to this name
|
||||
pub fn append(&mut self, other: &Self) -> &mut Self {
|
||||
for rcs in &*other.labels {
|
||||
self.add_label(rcs.clone());
|
||||
@ -394,9 +409,11 @@ enum LabelParseState {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Arc as Rc;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use ::serialize::binary::bin_tests::{test_read_data_set, test_emit_data_set};
|
||||
use ::serialize::binary::*;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
fn get_data() -> Vec<(Name, Vec<u8>)> {
|
||||
vec![
|
||||
@ -468,6 +485,14 @@ mod tests {
|
||||
assert!(zone.base_name().base_name().base_name().is_root());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepend() {
|
||||
let zone = Name::new().label("example").label("com");
|
||||
let www = zone.prepend_label(Rc::new("www".to_string()));
|
||||
|
||||
assert_eq!(www, Name::new().label("www").label("example").label("com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zone_of() {
|
||||
let zone = Name::new().label("example").label("com");
|
||||
|
Loading…
Reference in New Issue
Block a user