From 36ee9e704a59c15c0ac08d8802bea6396d92843c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BChler?= Date: Fri, 29 Dec 2017 22:25:43 +0100 Subject: [PATCH] step --- lib/dnsbox-base/src/common_types/binary.rs | 16 +- lib/dnsbox-base/src/common_types/name/mod.rs | 7 + lib/dnsbox-base/src/packet/mod.rs | 52 +++- lib/dnsbox-base/src/packet/opt.rs | 285 ++++++++++++++++++ lib/dnsbox-base/src/records/powerdns_tests.rs | 61 +++- lib/dnsbox-base/src/records/structs.rs | 2 +- lib/dnsbox-base/src/records/tests.rs | 13 +- lib/dnsbox-base/src/records/unknown.rs | 8 + lib/dnsbox-base/src/ser/rrdata.rs | 7 +- lib/dnsbox-derive/src/rrdata.rs | 4 + 10 files changed, 435 insertions(+), 20 deletions(-) create mode 100644 lib/dnsbox-base/src/packet/opt.rs diff --git a/lib/dnsbox-base/src/common_types/binary.rs b/lib/dnsbox-base/src/common_types/binary.rs index d71d657..d64f9a6 100644 --- a/lib/dnsbox-base/src/common_types/binary.rs +++ b/lib/dnsbox-base/src/common_types/binary.rs @@ -116,11 +116,11 @@ impl DnsTextData for Base64LongBlob { } } -// No length byte (or restriction), just all data to end of record. uses -// base64 encoding for text representation, whitespace allowed, padding -// required. -// -// No following field allowed, i.e. last field in the record. +/// No length byte (or restriction), just all data to end of record. uses +/// base64 encoding for text representation, whitespace allowed, padding +/// required. +/// +/// No following field allowed, i.e. last field in the record. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Base64RemainingBlob(Bytes); @@ -154,11 +154,13 @@ impl DnsTextData for Base64RemainingBlob { } } +/// No length byte (or restriction), just all data to end of record. uses +/// hex encoding for text representation, whitespace allowed. +/// +/// No following field allowed, i.e. last field in the record. #[derive(Clone, PartialEq, Eq, Debug)] pub struct HexRemainingBlob(Bytes); -// No length byte, just all data to end of record. uses hex encoding for -// text representation (spaces are ignored - last field in record). impl DnsPacketData for HexRemainingBlob { fn deserialize(data: &mut Cursor) -> Result { Ok(HexRemainingBlob(remaining_bytes(data))) diff --git a/lib/dnsbox-base/src/common_types/name/mod.rs b/lib/dnsbox-base/src/common_types/name/mod.rs index e3e121f..b4dcdad 100644 --- a/lib/dnsbox-base/src/common_types/name/mod.rs +++ b/lib/dnsbox-base/src/common_types/name/mod.rs @@ -200,6 +200,13 @@ impl DnsPacketData for DnsName { #[derive(Clone)] pub struct DnsCompressedName(pub DnsName); +impl DnsCompressedName { + /// Create new name representing the DNS root (".") + pub fn new_root() -> Self { + DnsCompressedName(DnsName::new_root()) + } +} + impl Deref for DnsCompressedName { type Target = DnsName; diff --git a/lib/dnsbox-base/src/packet/mod.rs b/lib/dnsbox-base/src/packet/mod.rs index 222ffdf..0ad3a95 100644 --- a/lib/dnsbox-base/src/packet/mod.rs +++ b/lib/dnsbox-base/src/packet/mod.rs @@ -1,12 +1,14 @@ use byteorder::ByteOrder; use bytes::{Bytes, Buf, BufMut, BigEndian}; -use common_types::{Type, Class, DnsCompressedName}; +use common_types::{Type, Class, DnsCompressedName, types}; use errors::*; use ser::RRData; use ser::packet::{DnsPacketData, DnsPacketWriteContext}; use std::io::Cursor; use records::registry::deserialize_rr_data; +pub mod opt; + #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum QueryResponse { Query, @@ -164,16 +166,40 @@ pub struct DnsPacket { pub answer: Vec, pub authority: Vec, pub additional: Vec, + pub opt: Option>, } impl DnsPacket { - pub fn to_bytes(&self) -> Result> { + /// also serializes OPT before conversion if present + pub fn to_bytes(&mut self) -> Result> { + if self.opt.is_some() { + // delete other OPTs, so only call it if there is a "new" OPT + self.serialize_opt()?; + } + let mut buf = Vec::new(); let mut ctx = DnsPacketWriteContext::new(); ctx.enable_compression(); self.serialize(&mut ctx, &mut buf)?; Ok(buf) } + + /// overwrites existing OPT records, and serializes a new one (if + /// self.opt is not None) + pub fn serialize_opt(&mut self) -> Result<()> { + // delete all additional OPT records + self.additional.retain(|r| r.data.rr_type() != types::OPT); + + match self.opt.take() { + Some(Err(e)) => bail!("can't serialize broken OPT: {:?}", e), + Some(Ok(opt)) => { + self.additional.push(opt.serialize()?); + }, + None => (), + } + + Ok(()) + } } impl Default for DnsPacket { @@ -185,6 +211,7 @@ impl Default for DnsPacket { answer: Vec::new(), authority: Vec::new(), additional: Vec::new(), + opt: None, } } } @@ -192,14 +219,31 @@ impl Default for DnsPacket { impl DnsPacketData for DnsPacket { fn deserialize(data: &mut Cursor) -> Result { let header = DnsHeader::deserialize(data)?; - Ok(DnsPacket { + let mut p = DnsPacket { id: header.id, flags: header.flags, question: (0..header.qdcount).map(|_| Question::deserialize(data)).collect::>>()?, answer: (0..header.ancount).map(|_| Resource::deserialize(data)).collect::>>()?, authority: (0..header.nscount).map(|_| Resource::deserialize(data)).collect::>>()?, additional: (0..header.arcount).map(|_| Resource::deserialize(data)).collect::>>()?, - }) + opt: None, + }; + + let mut opt_resource_ndx = None; + + for (i, r) in p.additional.iter().enumerate() { + if r.data.rr_type() == types::OPT { + ensure!(opt_resource_ndx.is_none(), "multiple OPT resource records"); + opt_resource_ndx = Some(i); + } + } + + if let Some(ndx) = opt_resource_ndx { + let opt_rr = p.additional.remove(ndx); + p.opt = Some(opt::Opt::deserialize(&opt_rr)?); + } + + Ok(p) } fn serialize(&self, context: &mut DnsPacketWriteContext, packet: &mut Vec) -> Result<()> { diff --git a/lib/dnsbox-base/src/packet/opt.rs b/lib/dnsbox-base/src/packet/opt.rs new file mode 100644 index 0000000..0942be7 --- /dev/null +++ b/lib/dnsbox-base/src/packet/opt.rs @@ -0,0 +1,285 @@ +use byteorder::ByteOrder; +use bytes::{Bytes, Buf, BufMut, BigEndian}; +use common_types::{DnsCompressedName, Class, types}; +use errors::*; +use packet::Resource; +use records::UnknownRecord; +use ser::packet::{DnsPacketData, DnsPacketWriteContext, get_blob, remaining_bytes}; +use std::io::{Cursor, Read}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum OptError { + UnknownVersion, + InvalidData, +} + +/// parsing OPT can fail without having to throw away the packet +/// completely; just fail in some way +/// +/// TODO: define some useful error... +pub type OptResult = ::std::result::Result; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct OptFlag(pub u16); + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct OptFlags(pub u16); + +impl ::std::ops::BitOr for OptFlags { + type Output = bool; + + fn bitor(self, rhs: OptFlag) -> Self::Output { + 0 != self.0 & rhs.0 + } +} + +impl ::std::ops::BitOr for OptFlag { + type Output = bool; + + fn bitor(self, rhs: OptFlags) -> Self::Output { + 0 != self.0 & rhs.0 + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum DnsOption { + /// DNS Name Server Identifier (0x0003) + NSID(Bytes), + // DAU (0x0005) + // DHU (0x0006) + // N3U (0x0007) + // edns-client-subnet (0x0008) + ClientSubnet { + source_prefix_length: u8, + scope_prefix_length: u8, + addr: IpAddr, + }, + // EDNS EXPIRE (0x0009) + // COOKIE (0x000a) + // edns-tcp-keepalive (0x000b) + // Padding (0x000c) + // CHAIN (0x000d) + // edns-key-tag (0x000e) + // DeviceID (26946 = 0x6942) + Unknown { + code: u16, + data: Bytes, + } +} + +impl DnsOption { + fn parse_client_subnet(data: &mut Cursor) -> Result { + let addr_family = u16::deserialize(data)?; + let source_prefix_length = u8::deserialize(data)?; + let scope_prefix_length = u8::deserialize(data)?; + let addr_prefix_len = ((source_prefix_length + 7) / 8) as usize; + ensure!(scope_prefix_length <= source_prefix_length, "scope prefix {} > source prefix {}", scope_prefix_length, source_prefix_length); + let addr = match addr_family { + 1 => { + ensure!(source_prefix_length <= 32, "invalid prefix for IPv4"); + let mut o = [0u8; 4]; + data.read_exact(&mut o[0..addr_prefix_len])?; + if 0 != source_prefix_length % 8 { + let mask = 0xff >> (source_prefix_length % 8); + ensure!(0 == o[addr_prefix_len - 1] & mask, "non-zero padding"); + } + IpAddr::V4(Ipv4Addr::from(o)) + }, + 2 => { + ensure!(source_prefix_length <= 128, "invalid prefix for IPv6"); + let mut o = [0u8; 16]; + data.read_exact(&mut o[0..addr_prefix_len])?; + if 0 != source_prefix_length % 8 { + let mask = 0xff >> (source_prefix_length % 8); + ensure!(0 == o[addr_prefix_len - 1] & mask, "non-zero padding"); + } + IpAddr::V6(Ipv6Addr::from(o)) + }, + _ => { + bail!("unknown address family {}", addr_family); + }, + }; + Ok(DnsOption::ClientSubnet{ + source_prefix_length, + scope_prefix_length, + addr, + }) + } + + fn parse_opt(code: u16, opt_data: Bytes) -> Result { + let mut data = Cursor::new(opt_data); + + let result = (|| Ok(match code { + 0x0003 => DnsOption::NSID(remaining_bytes(&mut data)), + 0x0008 => DnsOption::parse_client_subnet(&mut data)?, + _ => bail!("unknown option {}", code), + }))()?; + + ensure!(!data.has_remaining(), "option data remaining: {} bytes", data.remaining()); + + Ok(result) + } + + fn write_opt_data(&self, _context: &mut DnsPacketWriteContext, packet: &mut Vec) -> Result<()> { + match *self { + DnsOption::NSID(ref id) => { + packet.reserve(id.len()); + packet.put_slice(id); + }, + DnsOption::ClientSubnet{source_prefix_length, scope_prefix_length, ref addr} => { + let addr_prefix_len = ((source_prefix_length + 7) / 8) as usize; + packet.reserve(4 + addr_prefix_len); + packet.put_u16::(match *addr { + IpAddr::V4(_) => 1, + IpAddr::V6(_) => 2, + }); + packet.put_u8(source_prefix_length); + packet.put_u8(scope_prefix_length); + + match *addr { + IpAddr::V4(ref addr) => { + let o = addr.octets(); + packet.put_slice(&o[..addr_prefix_len]); + }, + IpAddr::V6(ref addr) => { + let o = addr.octets(); + packet.put_slice(&o[..addr_prefix_len]); + }, + } + }, + DnsOption::Unknown{ref data, ..} => { + packet.reserve(data.len()); + packet.put_slice(data); + }, + } + Ok(()) + } +} + +impl DnsPacketData for DnsOption { + fn deserialize(data: &mut Cursor) -> Result { + let code = u16::deserialize(data)?; + let opt_len = u16::deserialize(data)? as usize; + + let opt_data = get_blob(data, opt_len)?; + + DnsOption::parse_opt(code, opt_data.clone()).or_else(|_| { + Ok(DnsOption::Unknown{ + code: code, + data: opt_data, + }) + }) + } + + fn serialize(&self, context: &mut DnsPacketWriteContext, packet: &mut Vec) -> Result<()> { + let code: u16 = match *self { + DnsOption::NSID(_) => 0x0003, + DnsOption::ClientSubnet{..} => 0x0008, + DnsOption::Unknown{code, ..} => code, + }; + code.serialize(context, packet)?; + + let opt_len_pos = packet.len(); + packet.reserve(2); + packet.put_u16::(0); // stub + + let opt_start = packet.len(); + self.write_opt_data(context, packet)?; + let opt_end = packet.len(); + let opt_len = opt_end - opt_start; + + ensure!(opt_len < 0x1_0000, "OPTION DATA too big"); + + // now patch length + BigEndian::write_u16(&mut packet[opt_len_pos..][..2], opt_len as u16); + + Ok(()) + } +} + +pub const DNSSEC_OK: OptFlag = OptFlag(0x8000); + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Opt { + // "CLASS" + pub udp_payload_size: u16, + // "TTL" + pub extended_rcode_high: u8, + pub version: u8, + pub flags: OptFlags, + + // RDATA + pub options: Vec, +} + +impl Opt { + fn deserialize_options(ur: &UnknownRecord) -> Result> { + let mut data = Cursor::new(ur.raw().clone()); + let mut options = Vec::new(); + while data.has_remaining() { + options.push(DnsOption::deserialize(&mut data)?); + } + + Ok(options) + } + + pub fn deserialize(r: &Resource) -> Result> { + let udp_payload_size = r.class.0; + let extended_rcode_high = (r.ttl >> 24) as u8; + let version = (r.ttl >> 16) as u8; + let flags = OptFlags(r.ttl as u16); + + if version > 0 { return Ok(Err(OptError::UnknownVersion)); } + + let ur = match r.data.as_any().downcast_ref::() { + Some(ur) => ur, + None => bail!("need to parse OPT from UnknownRecord"), + }; + + let options = match Opt::deserialize_options(ur) { + Ok(options) => options, + Err(_) => return Ok(Err(OptError::InvalidData)), + }; + + Ok(Ok(Opt { + udp_payload_size, + extended_rcode_high, + version, + flags, + options, + })) + } + + pub fn serialize(&self) -> Result { + let mut data = Vec::new(); + let mut ctx = DnsPacketWriteContext::new(); + + for o in &self.options { + o.serialize(&mut ctx, &mut data)?; + } + let ur = UnknownRecord::new(types::OPT, data.into()); + + let ttl = ((self.extended_rcode_high as u32) << 24) + | ((self.version as u32) << 16) + | self.flags.0 as u32; + Ok(Resource{ + name: DnsCompressedName::new_root(), + class: Class(self.udp_payload_size), + ttl: ttl, + data: Box::new(ur) as _, + }) + } +} + +impl Default for Opt { + fn default() -> Self { + Opt { + udp_payload_size: 500, + extended_rcode_high: 0, + version: 0, + flags: OptFlags(0), + options: Vec::new(), + } + } +} diff --git a/lib/dnsbox-base/src/records/powerdns_tests.rs b/lib/dnsbox-base/src/records/powerdns_tests.rs index 3a4c502..d1ff452 100644 --- a/lib/dnsbox-base/src/records/powerdns_tests.rs +++ b/lib/dnsbox-base/src/records/powerdns_tests.rs @@ -4,13 +4,14 @@ use bytes::Bytes; use common_types::{DnsName, DnsCompressedName, Class, Type, types, classes}; use errors::*; use packet::*; -use records::{UnknownRecord, registry}; +use packet::opt::{Opt, DnsOption}; +use records::{UnknownRecord, registry, A}; use ser::{RRData, RRDataText, text}; use ser::packet::{DnsPacketData, deserialize_with}; use std::io::Cursor; fn fake_packet(rrtype: Type, raw: &[u8]) -> Bytes { - let p = DnsPacket{ + let mut p = DnsPacket{ question: vec![ Question { qname: ".".parse().unwrap(), @@ -55,7 +56,7 @@ fn get_first_answer_rdata(packet: Bytes) -> Result { } fn serialized_answer(rrdata: Box) -> Result { - let p = DnsPacket{ + let mut p = DnsPacket{ question: vec![ Question { qname: ".".parse().unwrap(), @@ -841,3 +842,57 @@ fn test_invalid_data_checks() { 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 } + +#[test] +fn test_opt_record_in() { + let p = deserialize_with( + Bytes::from_static(b"\xf0\x01\x01\x00\x00\x01\x00\x01\x00\x00\x00\x01\x03www\x08powerdns\x03com\x00\x00\x01\x00\x01\x03www\x08powerdns\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x10\x00\x04\x7f\x00\x00\x01\x00\x00\x29\x05\x00\x00\x00\x00\x00\x00\x0c\x00\x03\x00\x08powerdns"), + DnsPacket::deserialize + ).unwrap(); + + let opt = p.opt.unwrap().unwrap(); + + assert_eq!(opt.udp_payload_size, 1280); + assert_eq!(opt.options, vec![ + DnsOption::NSID(Bytes::from_static(b"powerdns")), + ]); +} + +#[test] +fn test_opt_record_out() { + let mut p = DnsPacket{ + id: 0xf001, + flags: DnsHeaderFlags { + recursion_desired: true, + .. Default::default() + }, + question: vec![ + Question { + qname: "www.powerdns.com.".parse().unwrap(), + qtype: types::A, + qclass: classes::IN, + } + ], + answer: vec![ + Resource { + name: "www.powerdns.com.".parse().unwrap(), + class: classes::IN, + ttl: 16, + data: Box::new(A { addr: "127.0.0.1".parse().unwrap() }), + } + ], + opt: Some(Ok(Opt { + udp_payload_size: 1280, + options: vec![ + DnsOption::NSID(Bytes::from_static(b"powerdns")), + ], + .. Default::default() + })), + .. Default::default() + }; + + assert_eq!( + &p.to_bytes().unwrap() as &[u8], + b"\xf0\x01\x01\x00\x00\x01\x00\x01\x00\x00\x00\x01\x03www\x08powerdns\x03com\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x10\x00\x04\x7f\x00\x00\x01\x00\x00\x29\x05\x00\x00\x00\x00\x00\x00\x0c\x00\x03\x00\x08powerdns" as &[u8] + ); +} diff --git a/lib/dnsbox-base/src/records/structs.rs b/lib/dnsbox-base/src/records/structs.rs index 9648cc4..edc78c8 100644 --- a/lib/dnsbox-base/src/records/structs.rs +++ b/lib/dnsbox-base/src/records/structs.rs @@ -10,7 +10,7 @@ use std::net::{Ipv4Addr, Ipv6Addr}; #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, DnsPacketData, DnsTextData, RRData)] #[RRClass(IN)] pub struct A { - addr: Ipv4Addr, + pub addr: Ipv4Addr, } #[derive(Clone, PartialEq, Eq, Debug, DnsPacketData, DnsTextData, RRData)] diff --git a/lib/dnsbox-base/src/records/tests.rs b/lib/dnsbox-base/src/records/tests.rs index 1c5a24a..c9de637 100644 --- a/lib/dnsbox-base/src/records/tests.rs +++ b/lib/dnsbox-base/src/records/tests.rs @@ -48,6 +48,13 @@ where let d2: T = rrdata_parse(txt).context("couldn't parse text record")?; ensure!(d1 == d2, "decoded data not equal: {:?} != {:?}", d1, d2); + let d1_text = d1.text().unwrap(); + let d2_text = d2.text().unwrap(); + let canon_text = (T::NAME.to_owned(), canon.into()); + + ensure!(d1_text == canon_text, "re-formatted binary record not equal to canonical representation: {:?} != {:?}", d1_text, canon_text); + ensure!(d2_text == canon_text, "re-formatted text record not equal to canonical representation: {:?} != {:?}", d2_text, canon_text); + Ok(()) } @@ -66,10 +73,10 @@ where T: StaticRRData + fmt::Debug + PartialEq { // at least one "segment" (which could be empty) - check2::(r#" "" "#, b"", r#" "" "#).unwrap_err(); - check2::(r#""#, b"\x00", r#" "" "#).unwrap_err(); + check2::(r#" "" "#, b"", r#""""#).unwrap_err(); + check2::(r#""#, b"\x00", r#""""#).unwrap_err(); // one empty segment - check2::(r#" "" "#, b"\x00", r#" "" "#).unwrap(); + check2::(r#" "" "#, b"\x00", r#""""#).unwrap(); // one segment check::(r#" "foo" "#, b"\x03foo").unwrap(); // two segments diff --git a/lib/dnsbox-base/src/records/unknown.rs b/lib/dnsbox-base/src/records/unknown.rs index 4adcc11..bb5f436 100644 --- a/lib/dnsbox-base/src/records/unknown.rs +++ b/lib/dnsbox-base/src/records/unknown.rs @@ -24,6 +24,10 @@ impl UnknownRecord { } } + pub fn raw(&self) -> &Bytes { + &self.raw + } + pub fn deserialize(rr_type: Type, data: &mut ::std::io::Cursor) -> Result { Ok(UnknownRecord::new(rr_type, remaining_bytes(data))) } @@ -89,4 +93,8 @@ impl RRData for UnknownRecord { fn clone_box(&self) -> Box { Box::new(self.clone()) as _ } + + fn as_any(&self) -> &::std::any::Any { + self as _ + } } diff --git a/lib/dnsbox-base/src/ser/rrdata.rs b/lib/dnsbox-base/src/ser/rrdata.rs index 6941c60..86e21fa 100644 --- a/lib/dnsbox-base/src/ser/rrdata.rs +++ b/lib/dnsbox-base/src/ser/rrdata.rs @@ -1,12 +1,13 @@ use bytes::Bytes; use common_types::{Class, Type, classes}; use errors::*; +use records::UnknownRecord; use ser::packet::{DnsPacketData, DnsPacketWriteContext}; use ser::text::{DnsTextData, DnsTextFormatter, DnsTextContext}; +use std::any::Any; use std::borrow::Cow; use std::fmt; use std::io::Cursor; -use records::UnknownRecord; pub trait RRDataPacket { fn deserialize_rr_data(ttl: u32, rr_class: Class, rr_type: Type, data: &mut Cursor) -> Result @@ -93,8 +94,10 @@ impl RRDataText for T { } } -pub trait RRData: RRDataPacket + RRDataText + fmt::Debug { +pub trait RRData: RRDataPacket + RRDataText + fmt::Debug + 'static { fn clone_box(&self) -> Box; + + fn as_any(&self) -> &Any; } impl Clone for Box { diff --git a/lib/dnsbox-derive/src/rrdata.rs b/lib/dnsbox-derive/src/rrdata.rs index 1f356d6..c519e8b 100644 --- a/lib/dnsbox-derive/src/rrdata.rs +++ b/lib/dnsbox-derive/src/rrdata.rs @@ -76,6 +76,10 @@ pub fn build(ast: &syn::DeriveInput) -> quote::Tokens { fn clone_box(&self) -> Box<::dnsbox_base::ser::RRData> { Box::new(self.clone() as #name) as _ } + + fn as_any(&self) -> &::std::any::Any { + self as _ + } } impl ::dnsbox_base::ser::StaticRRData for #name {