From 503bc29fcfb16c392a4b803eb338f43b929f0cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BChler?= Date: Fri, 29 Dec 2017 14:07:58 +0100 Subject: [PATCH] step --- .../src/common_types/name/name_text_parser.rs | 2 +- lib/dnsbox-base/src/common_types/types.rs | 4 +- lib/dnsbox-base/src/records/powerdns_tests.rs | 65 +++++- lib/dnsbox-base/src/records/registry.rs | 8 +- lib/dnsbox-base/src/records/structs.rs | 42 ++-- lib/dnsbox-base/src/records/weird_structs.rs | 188 +++++++++++++++++- lib/dnsbox-base/src/ser/rrdata.rs | 2 +- 7 files changed, 280 insertions(+), 31 deletions(-) diff --git a/lib/dnsbox-base/src/common_types/name/name_text_parser.rs b/lib/dnsbox-base/src/common_types/name/name_text_parser.rs index 9e52d02..2061394 100644 --- a/lib/dnsbox-base/src/common_types/name/name_text_parser.rs +++ b/lib/dnsbox-base/src/common_types/name/name_text_parser.rs @@ -40,7 +40,7 @@ impl DnsName { label.push(raw[pos+1]); } } else { - ensure!(!quoted::is_ascii_whitespace(raw[pos+1]), "whitespace must be encoded as \\{:03} in: {:?}", raw[pos+1], name); + ensure!(!quoted::is_ascii_whitespace(raw[pos]), "whitespace must be encoded as \\{:03} in: {:?}", raw[pos], name); label.push(raw[pos]); } pos += 1; diff --git a/lib/dnsbox-base/src/common_types/types.rs b/lib/dnsbox-base/src/common_types/types.rs index c1514df..d22827f 100644 --- a/lib/dnsbox-base/src/common_types/types.rs +++ b/lib/dnsbox-base/src/common_types/types.rs @@ -296,9 +296,9 @@ pub enum KnownType { /// Host Identity Protocol HIP = 0x0037, // RFC 8005 /// NINFO - NINFO = 0x0038, // Jim Reid + NINFO = 0x0038, // Jim Reid: https://tools.ietf.org/html/draft-reid-dnsext-zs-01 /// RKEY - RKEY = 0x0039, // Jim Reid + RKEY = 0x0039, // Jim Reid: https://tools.ietf.org/html/draft-reid-dnsext-rkey-00 /// Trust Anchor LINK TALINK = 0x003a, // Wouter Wijngaards /// Child DS diff --git a/lib/dnsbox-base/src/records/powerdns_tests.rs b/lib/dnsbox-base/src/records/powerdns_tests.rs index 1654a89..3a4c502 100644 --- a/lib/dnsbox-base/src/records/powerdns_tests.rs +++ b/lib/dnsbox-base/src/records/powerdns_tests.rs @@ -87,18 +87,31 @@ fn check(q: Type, text_input: &'static str, canonic: Option<&'static str>, raw: context.set_record_type(q); context.set_last_ttl(3600); - let d_zone = text::parse_with(text_input, |data| { + let d_zone: Box = text::parse_with(text_input, |data| { registry::parse_rr_data(&context, data) }).unwrap(); - let d_wire_packet = deserialize_with(fake_packet(q, raw), DnsPacket::deserialize).unwrap(); - let d_wire = &d_wire_packet.answer[0].data; - let d_zone_text = d_zone.text().unwrap(); + // make sure we actually know the type and the text representation + // uses the known representation (and not the generic one) + assert_eq!(Some(d_zone_text.0.as_ref()), d_zone.rr_type().known_name()); + // ... and the text representation matches the canonic format + assert_eq!(&d_zone_text.1, canonic); + // (pdns tests compares this to the input_text sometimes, because + // they often (TXT, DNS names) use the master file representation + // internally instead of normalizing it. This is bad for testing, + // because they do normalize some parts...) + + // pdns tests deserialize `zone_as_wire` (from below) here, but we + // make sure it's the same anyway + let d_wire_packet = deserialize_with(fake_packet(q, raw), DnsPacket::deserialize).unwrap(); + let d_wire: &Box = &d_wire_packet.answer[0].data; + let d_wire_text = d_wire.text().unwrap(); + // pdns tests compare d_wire_text and canonic, but d_zone_text + // already matches canonic assert_eq!(d_zone_text, d_wire_text, "data parsed from zone doesn't match data from wire"); - assert_eq!(&d_zone_text.1, canonic); let zone_as_wire = serialized_answer(d_zone).unwrap(); assert_eq!(zone_as_wire, raw); @@ -786,3 +799,45 @@ fn test_TYPE65226() { assert_eq!(d1, d2); assert_eq!(d1.text().unwrap(), ("TYPE65226".into(), "\\# 3 414243".into())); } + +fn check_invalid_zone(q: Type, text_input: &str) { + let mut context = text::DnsTextContext::new(); + context.set_zone_class(classes::IN); + context.set_origin(DnsName::new_root()); + context.set_record_type(q); + context.set_last_ttl(3600); + + text::parse_with(text_input, |data| { + registry::parse_rr_data(&context, data) + }).unwrap_err(); +} + +fn check_invalid_wire(q: Type, raw: &'static [u8]) { + deserialize_with(fake_packet(q, raw), DnsPacket::deserialize).unwrap_err(); +} + +#[test] +fn test_invalid_data_checks() { + check_invalid_zone(types::A, "932.521.256.42"); // hollywood IP + check_invalid_zone(types::A, "932.521"); // truncated hollywood IP + check_invalid_zone(types::A, "10.0"); // truncated IP + check_invalid_zone(types::A, "10.0.0.1."); // trailing dot + check_invalid_zone(types::A, "10.0.0."); // trailing dot + check_invalid_zone(types::A, ".0.0.1"); // empty octet + check_invalid_zone(types::A, "10..0.1"); // empty octet + check_invalid_wire(types::A, b"\xca\xec\x00"); // truncated wire value + check_invalid_zone(types::A, "127.0.0.1 evil data"); // trailing garbage + check_invalid_zone(types::AAAA, "23:00"); // time when this test was written + check_invalid_zone(types::AAAA, "23:00::15::43"); // double compression + check_invalid_zone(types::AAAA, "2a23:00::15::"); // ditto + check_invalid_wire(types::AAAA, b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff"); // truncated wire value +// empty label, must be broken + check_invalid_zone(types::CNAME, "name..example.com."); +// overly large label (64), must be broken + check_invalid_zone(types::CNAME, "1234567890123456789012345678901234567890123456789012345678901234.example.com."); +// local overly large name (256), must be broken + check_invalid_zone(types::CNAME, "123456789012345678901234567890123456789012345678901234567890123.123456789012345678901234567890123456789012345678901234567890123.123456789012345678901234567890123456789012345678901234567890123.12345678901234567890123456789012345678901234567890123.rec.test."); +// non-local overly large name (256), must be broken + check_invalid_zone(types::CNAME, "123456789012345678901234567890123456789012345678901234567890123.123456789012345678901234567890123456789012345678901234567890123.123456789012345678901234567890123456789012345678901234567890123.12345678901234567890123456789012345678901234567890123456789012."); + check_invalid_zone(types::SOA, "ns.rec.test hostmaster.test.rec 20130512010 3600 3600 604800 120"); // too long serial +} diff --git a/lib/dnsbox-base/src/records/registry.rs b/lib/dnsbox-base/src/records/registry.rs index 7bd670a..4152a18 100644 --- a/lib/dnsbox-base/src/records/registry.rs +++ b/lib/dnsbox-base/src/records/registry.rs @@ -168,11 +168,11 @@ impl Registry { r.register_known::(); r.register_known::(); r.register_unknown("HIP" , types::HIP); - r.register_unknown("NINFO" , types::NINFO); - r.register_unknown("RKEY" , types::RKEY); + r.register_known::(); + r.register_known::(); r.register_unknown("TALINK" , types::TALINK); - r.register_unknown("CDS" , types::CDS); - r.register_unknown("CDNSKEY" , types::CDNSKEY); + r.register_known::(); + r.register_known::(); r.register_known::(); r.register_unknown("CSYNC" , types::CSYNC); r.register_known::(); diff --git a/lib/dnsbox-base/src/records/structs.rs b/lib/dnsbox-base/src/records/structs.rs index 819c840..9648cc4 100644 --- a/lib/dnsbox-base/src/records/structs.rs +++ b/lib/dnsbox-base/src/records/structs.rs @@ -399,25 +399,43 @@ pub struct SMIMEA { // #[RRClass(?)] // pub struct HIP; -// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] -// #[RRClass(?)] -// pub struct NINFO; +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +#[RRClass(ANY)] +pub struct NINFO { + text: LongText, +} -// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] -// #[RRClass(?)] -// pub struct RKEY; + +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +#[RRClass(ANY)] +pub struct RKEY { + flags: u16, + protocol: u8, + algorithm: u8, + public_key: Base64RemainingBlob, +} // #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] // #[RRClass(?)] // pub struct TALINK; -// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] -// #[RRClass(?)] -// pub struct CDS; +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +#[RRClass(ANY)] +pub struct CDS { + key_tag: u16, + algorithm: u8, + digest_type: u8, + digest: HexRemainingBlob, +} -// #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] -// #[RRClass(?)] -// pub struct CDNSKEY; +#[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] +#[RRClass(ANY)] +pub struct CDNSKEY { + flags: u16, + protocol: u8, + algorithm: u8, + public_key: Base64RemainingBlob, +} #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] #[RRClass(ANY)] diff --git a/lib/dnsbox-base/src/records/weird_structs.rs b/lib/dnsbox-base/src/records/weird_structs.rs index 9ffc5a6..4408afa 100644 --- a/lib/dnsbox-base/src/records/weird_structs.rs +++ b/lib/dnsbox-base/src/records/weird_structs.rs @@ -3,7 +3,7 @@ use errors::*; use common_types::*; use failure::ResultExt; use ser::packet::{DnsPacketData, DnsPacketWriteContext, remaining_bytes}; -use ser::text::{DnsTextData, DnsTextFormatter, DnsTextContext}; +use ser::text::{DnsTextData, DnsTextFormatter, DnsTextContext, next_field}; use std::fmt; use std::io::Read; use std::net::{Ipv4Addr, Ipv6Addr}; @@ -53,13 +53,189 @@ impl DnsPacketData for LOC { } impl DnsTextData for LOC { - fn dns_parse(_context: &DnsTextContext, _data: &mut &str) -> Result { - unimplemented!() + fn dns_parse(_context: &DnsTextContext, data: &mut &str) -> Result { + let degrees_latitude = next_field(data)?.parse::()?; + ensure!(degrees_latitude <= 90, "degrees latitude out of range: {}", degrees_latitude); + let mut minutes_latitude = 0; + let mut seconds_latitude = 0.0; + let mut field = next_field(data)?; + if field != "N" && field != "n" && field != "S" && field != "s" { + minutes_latitude = field.parse::()?; + ensure!(minutes_latitude < 60, "minutes latitude out of range: {}", minutes_latitude); + field = next_field(data)?; + if field != "N" && field != "n" && field != "S" && field != "s" { + seconds_latitude = field.parse::()?; + ensure!(seconds_latitude >= 0.0 && seconds_latitude < 60.0, "seconds latitude out of range: {}", seconds_latitude); + field = next_field(data)?; + } + } + let latitude_off = (3600_000 * degrees_latitude as u32) + (60_000 * minutes_latitude as u32) + (1_000.0 * seconds_latitude).round() as u32; + ensure!(latitude_off <= 3600_000 * 180, "latitude out of range"); + let latitude = match field { + "N"|"n" => 0x8000_0000 + latitude_off, + "S"|"s" => 0x8000_0000 - latitude_off, + _ => bail!("invalid latitude orientation [NS]: {}", field), + }; + + let degrees_longitude = next_field(data)?.parse::()?; + ensure!(degrees_longitude <= 180, "degrees longitude out of range: {}", degrees_longitude); + let mut minutes_longitude = 0; + let mut seconds_longitude = 0.0; + let mut field = next_field(data)?; + if field != "E" && field != "e" && field != "W" && field != "w" { + minutes_longitude = field.parse::()?; + ensure!(minutes_longitude < 60, "minutes longitude out of range: {}", minutes_longitude); + field = next_field(data)?; + if field != "E" && field != "e" && field != "W" && field != "w" { + seconds_longitude = field.parse::()?; + ensure!(seconds_longitude >= 0.0 && seconds_longitude < 60.0, "seconds longitude out of range: {}", seconds_longitude); + field = next_field(data)?; + } + } + let longitude_off = (3600_000 * degrees_longitude as u32) + (60_000 * minutes_longitude as u32) + (1_000.0 * seconds_longitude).round() as u32; + ensure!(longitude_off <= 3600_000 * 180, "longitude out of range"); + let longitude = match field { + "E"|"e" => 0x8000_0000 + longitude_off, + "W"|"w" => 0x8000_0000 - longitude_off, + _ => bail!("invalid longitude orientation [EW]: {}", field), + }; + + fn trim_unit_m(s: &str) -> &str { + if s.ends_with('m') { &s[..s.len()-1] } else { s } + } + + fn parse_precision(s: &str) -> Result { + let s = trim_unit_m(s); + let mut m = 0; + let mut e = 0; + let mut dec_point = None; + for &b in s.as_bytes() { + if b == b'.' { + ensure!(dec_point.is_none(), "invalid precision (double decimal point): {:?}", s); + dec_point = Some(0); + continue; + } + ensure!(b >= b'0' && b <= b'9', "invalid precision (invalid character): {:?}", s); + if let Some(ref mut dp) = dec_point { + if *dp == 2 { continue; } // ignore following digits + *dp += 1; + } + let d = b - b'0'; + if 0 == m { + m = d; + } else { + e += 1; + ensure!(e <= 9, "invalid precision (overflow): {:?}", s); + } + } + e += 2 - dec_point.unwrap_or(0); + ensure!(e <= 9, "invalid precision (overflow): {:?}", s); + Ok(m << 4 | e) + } + + let altitude = match next_field(data) { + Ok(field) => { + let f_altitude = trim_unit_m(field).parse::()?; + let altitude = (f_altitude * 100.0 + 10000000.0).round() as i64; + ensure!(altitude > 0 && (altitude as u32) as i64 == altitude, "altitude out of range"); + altitude as u32 + }, + // standard requires the field, but the example parser doesn't.. + Err(_) => 10000000, // 0m + }; + + let size = match next_field(data) { + Ok(field) => parse_precision(field)?, + Err(_) => 0x12, // => 1e2 cm = 1m + }; + + let horizontal_precision = match next_field(data) { + Ok(field) => parse_precision(field)?, + Err(_) => 0x16, // 1e6 cm = 10km */ + }; + + let vertical_precision = match next_field(data) { + Ok(field) => parse_precision(field)?, + Err(_) => 0x13, // 1e3 cm = 10m */ + }; + + Ok(LOC::Version0(LOC0{ + size, + horizontal_precision, + vertical_precision, + latitude, + longitude, + altitude, + })) } - fn dns_format(&self, _f: &mut DnsTextFormatter) -> fmt::Result { - // always prefer binary representation - Err(fmt::Error) + fn dns_format(&self, f: &mut DnsTextFormatter) -> fmt::Result { + let this = match *self { + LOC::Version0(ref t) => t, + _ => return Err(fmt::Error), + }; + const MAX_LAT_OFFSET: u32 = 3600_000 * 180; + const MAX_LON_OFFSET: u32 = 3600_000 * 180; + const LATLON_MID: u32 = 0x8000_0000; + if this.latitude < LATLON_MID - MAX_LAT_OFFSET || this.latitude > LATLON_MID + MAX_LAT_OFFSET { + return Err(fmt::Error); + } + if this.longitude < LATLON_MID - MAX_LON_OFFSET || this.longitude > LATLON_MID + MAX_LON_OFFSET { + return Err(fmt::Error); + } + + fn is_invalid_prec(v: u8) -> bool { + // "leading-digit" << 4 | "exponent(base 10)" + // if the leading digit is 0, the exponent must be 0 too. + (v > 0x00 && v < 0x10) || (v >> 4) > 9 || (v & 0xf) > 9 + } + if is_invalid_prec(this.size) || is_invalid_prec(this.horizontal_precision) || is_invalid_prec(this.vertical_precision) { + return Err(fmt::Error); + } + + fn putlatlon2(v: u32, f: &mut DnsTextFormatter) -> fmt::Result { + let msecs = v % 1000; + let fullsecs = v / 1000; + let secs = fullsecs % 60; + let fullmins = fullsecs / 60; + let mins = fullmins % 60; + let deg = fullmins / 60; + write!(f, "{} {} {}.{:03}", deg, mins, secs, msecs) + } + + fn putlatlon(v: u32, f: &mut DnsTextFormatter, pos: char, neg: char) -> fmt::Result { + if v >= LATLON_MID { + putlatlon2(v - LATLON_MID, f)?; + write!(f, "{}", pos) + } else { + putlatlon2(LATLON_MID - v, f)?; + write!(f, "{}", neg) + } + } + + putlatlon(this.latitude, f, 'N', 'S')?; + putlatlon(this.longitude, f, 'E', 'W')?; + + write!(f, "{:.2}m", (this.altitude as f64 - 10000000.0) / 100.0)?; + + fn put_prec(v: u8, f: &mut DnsTextFormatter) -> fmt::Result { + let m = v >> 4; + debug_assert!(m < 10); + let e = v & 0xf; + if e >= 2 { + write!(f, "{:0