#[derive(Debug)]
pub struct DNSQueryEntry {
- pub name: Vec<u8>,
+ pub name: DNSName,
pub rrtype: u16,
pub rrclass: u16,
}
#[derive(Debug, PartialEq, Eq)]
pub struct DNSRDataSOA {
/// Primary name server for this zone
- pub mname: Vec<u8>,
+ pub mname: DNSName,
/// Authority's mailbox
- pub rname: Vec<u8>,
+ pub rname: DNSName,
/// Serial version number
pub serial: u32,
/// Refresh interval (seconds)
/// Port
pub port: u16,
/// Target
- pub target: Vec<u8>,
+ pub target: DNSName,
+}
+
+bitflags! {
+ #[derive(Default)]
+ pub struct DNSNameFlags: u8 {
+ const INFINITE_LOOP = 0b0000_0001;
+ const TRUNCATED = 0b0000_0010;
+ const LABEL_LIMIT = 0b0000_0100;
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DNSName {
+ pub value: Vec<u8>,
+ pub flags: DNSNameFlags,
}
/// Represents RData of various formats
A(Vec<u8>),
AAAA(Vec<u8>),
// RData is a domain name
- CNAME(Vec<u8>),
- PTR(Vec<u8>),
- MX(Vec<u8>),
- NS(Vec<u8>),
+ CNAME(DNSName),
+ PTR(DNSName),
+ MX(DNSName),
+ NS(DNSName),
// RData is text
TXT(Vec<u8>),
NULL(Vec<u8>),
#[derive(Debug, PartialEq, Eq)]
pub struct DNSAnswerEntry {
- pub name: Vec<u8>,
+ pub name: DNSName,
pub rrtype: u16,
pub rrclass: u16,
pub ttl: u32,
tx.tx_data.set_event(event as u8);
}
- fn parse_request(&mut self, input: &[u8], is_tcp: bool, frame: Option<Frame>, flow: *const core::Flow,) -> bool {
+ fn parse_request(
+ &mut self, input: &[u8], is_tcp: bool, frame: Option<Frame>, flow: *const core::Flow,
+ ) -> bool {
match dns_parse_request(input) {
Ok(mut tx) => {
self.tx_id += 1;
self.parse_response(input, false, frame, flow)
}
- fn parse_response(&mut self, input: &[u8], is_tcp: bool, frame: Option<Frame>, flow: *const core::Flow) -> bool {
+ fn parse_response(
+ &mut self, input: &[u8], is_tcp: bool, frame: Option<Frame>, flow: *const core::Flow,
+ ) -> bool {
match dns_parse_response(input) {
Ok(mut tx) => {
self.tx_id += 1;
if let Some(queries) = queries {
if let Some(query) = queries.get(index) {
- if !query.name.is_empty() {
- *buf = query.name.as_ptr();
- *len = query.name.len() as u32;
+ if !query.name.value.is_empty() {
+ *buf = query.name.value.as_ptr();
+ *len = query.name.value.len() as u32;
return true;
}
}
if let Some(answers) = answers {
if let Some(answer) = answers.get(index) {
- if !answer.name.is_empty() {
- *buf = answer.name.as_ptr();
- *len = answer.name.len() as u32;
+ if !answer.name.value.is_empty() {
+ *buf = answer.name.value.as_ptr();
+ *len = answer.name.value.len() as u32;
return true;
}
}
fn dns_log_soa(soa: &DNSRDataSOA) -> Result<JsonBuilder, JsonError> {
let mut js = JsonBuilder::try_new_object()?;
- js.set_string_from_bytes("mname", &soa.mname)?;
- js.set_string_from_bytes("rname", &soa.rname)?;
+ js.set_string_from_bytes("mname", &soa.mname.value)?;
+ js.set_string_from_bytes("rname", &soa.rname.value)?;
js.set_uint("serial", soa.serial as u64)?;
js.set_uint("refresh", soa.refresh as u64)?;
js.set_uint("retry", soa.retry as u64)?;
js.set_uint("priority", srv.priority as u64)?;
js.set_uint("weight", srv.weight as u64)?;
js.set_uint("port", srv.port as u64)?;
- js.set_string_from_bytes("name", &srv.target)?;
+ js.set_string_from_bytes("name", &srv.target.value)?;
js.close()?;
return Ok(js);
fn dns_log_json_answer_detail(answer: &DNSAnswerEntry) -> Result<JsonBuilder, JsonError> {
let mut jsa = JsonBuilder::try_new_object()?;
- jsa.set_string_from_bytes("rrname", &answer.name)?;
+ jsa.set_string_from_bytes("rrname", &answer.name.value)?;
jsa.set_string("rrtype", &dns_rrtype_string(answer.rrtype))?;
jsa.set_uint("ttl", answer.ttl as u64)?;
DNSRData::A(addr) | DNSRData::AAAA(addr) => {
jsa.set_string("rdata", &dns_print_addr(addr))?;
}
- DNSRData::CNAME(bytes)
- | DNSRData::MX(bytes)
- | DNSRData::NS(bytes)
- | DNSRData::TXT(bytes)
- | DNSRData::NULL(bytes)
- | DNSRData::PTR(bytes) => {
+ DNSRData::CNAME(name) | DNSRData::MX(name) | DNSRData::NS(name) | DNSRData::PTR(name) => {
+ jsa.set_string_from_bytes("rdata", &name.value)?;
+ }
+ DNSRData::TXT(bytes) | DNSRData::NULL(bytes) => {
jsa.set_string_from_bytes("rdata", bytes)?;
}
DNSRData::SOA(soa) => {
js.set_uint("opcode", opcode as u64)?;
if let Some(query) = response.queries.first() {
- js.set_string_from_bytes("rrname", &query.name)?;
+ js.set_string_from_bytes("rrname", &query.name.value)?;
js.set_string("rrtype", &dns_rrtype_string(query.rrtype))?;
}
js.set_string("rcode", &dns_rcode_string(header.flags))?;
a.append_string(&dns_print_addr(addr))?;
}
}
- DNSRData::CNAME(bytes)
- | DNSRData::MX(bytes)
- | DNSRData::NS(bytes)
- | DNSRData::TXT(bytes)
- | DNSRData::NULL(bytes)
- | DNSRData::PTR(bytes) => {
+ DNSRData::CNAME(name)
+ | DNSRData::MX(name)
+ | DNSRData::NS(name)
+ | DNSRData::PTR(name) => {
+ if !answer_types.contains_key(&type_string) {
+ answer_types
+ .insert(type_string.to_string(), JsonBuilder::try_new_array()?);
+ }
+ if let Some(a) = answer_types.get_mut(&type_string) {
+ a.append_string_from_bytes(&name.value)?;
+ }
+ }
+ DNSRData::TXT(bytes) | DNSRData::NULL(bytes) => {
if !answer_types.contains_key(&type_string) {
answer_types
.insert(type_string.to_string(), JsonBuilder::try_new_array()?);
a.append_string(&dns_print_addr(addr))?;
}
}
- DNSRData::CNAME(bytes)
- | DNSRData::MX(bytes)
- | DNSRData::NS(bytes)
- | DNSRData::TXT(bytes)
- | DNSRData::NULL(bytes)
- | DNSRData::PTR(bytes) => {
+ DNSRData::CNAME(name)
+ | DNSRData::MX(name)
+ | DNSRData::NS(name)
+ | DNSRData::PTR(name) => {
+ if !answer_types.contains_key(&type_string) {
+ answer_types
+ .insert(type_string.to_string(), JsonBuilder::try_new_array()?);
+ }
+ if let Some(a) = answer_types.get_mut(&type_string) {
+ a.append_string_from_bytes(&name.value)?;
+ }
+ }
+ DNSRData::TXT(bytes) | DNSRData::NULL(bytes) => {
if !answer_types.contains_key(&type_string) {
answer_types
.insert(type_string.to_string(), JsonBuilder::try_new_array()?);
if dns_log_rrtype_enabled(query.rrtype, flags) {
jb.set_string("type", "query")?;
jb.set_uint("id", request.header.tx_id as u64)?;
- jb.set_string_from_bytes("rrname", &query.name)?;
+ jb.set_string_from_bytes("rrname", &query.name.value)?;
jb.set_string("rrtype", &dns_rrtype_string(query.rrtype))?;
jb.set_uint("tx_id", tx.id - 1)?;
if request.header.flags & 0x0040 != 0 {
for query in &message.queries {
if dns_log_rrtype_enabled(query.rrtype, flags) {
jb.start_object()?
- .set_string_from_bytes("rrname", &query.name)?
- .set_string("rrtype", &dns_rrtype_string(query.rrtype))?
- .close()?;
+ .set_string_from_bytes("rrname", &query.name.value)?
+ .set_string("rrtype", &dns_rrtype_string(query.rrtype))?;
+ jb.close()?;
}
}
jb.close()?;
// Should be unreachable...
return false;
};
-
+
for query in &message.queries {
if dns_log_rrtype_enabled(query.rrtype, flags) {
return true;
use nom7::number::streaming::{be_u16, be_u32, be_u8};
use nom7::{error_position, Err, IResult};
+// Set a maximum assembled hostname length of 1025, this value was
+// chosen as its what DNSMasq uses, a popular DNS server, even if most
+// tooling limits names to 256 chars without special options.
+static MAX_NAME_LEN: usize = 1025;
+
/// Parse a DNS name.
///
+/// Names are parsed with the following restrictions:
+///
+/// - Only 255 segments will be processed, if more the parser may
+/// error out. This is also our safeguard against an infinite loop. If
+/// a pointer had been followed a truncated name will be
+/// returned. However if pointer has been processed we error out as we
+/// don't know where the next data point starts without more
+/// iterations.
+///
+/// - The maximum name parsed in representation format is MAX_NAME_LEN
+/// characters. Once larger, the truncated name will be returned with
+/// a flag specifying the name was truncated. Note that parsing
+/// continues if no pointer has been used as we still need to find the
+/// start of the next protocol unit.
+///
+/// As some error in parsing the name are recoverable, a DNSName
+/// object is returned with flags signifying a recoverable
+/// error. These errors include:
+///
+/// - infinite loop: as we know the end of the name in the input
+/// stream, we can return what we've parsed with the remain data.
+///
+/// - maximum number of segments/labels parsed
+///
+/// - truncation of name when too long
+///
/// Parameters:
/// start: the start of the name
/// message: the complete message that start is a part of with the DNS header
-fn dns_parse_name<'b>(start: &'b [u8], message: &'b [u8]) -> IResult<&'b [u8], Vec<u8>> {
+fn dns_parse_name<'b>(start: &'b [u8], message: &'b [u8]) -> IResult<&'b [u8], DNSName> {
let mut pos = start;
let mut pivot = start;
let mut name: Vec<u8> = Vec::with_capacity(32);
let mut count = 0;
+ let mut flags = DNSNameFlags::default();
loop {
if pos.is_empty() {
break;
} else if len & 0b1100_0000 == 0 {
let (rem, label) = length_data(be_u8)(pos)?;
- if !name.is_empty() {
- name.push(b'.');
+ if !flags.contains(DNSNameFlags::TRUNCATED) {
+ if !name.is_empty() {
+ name.push(b'.');
+ }
+ name.extend(label);
}
- name.extend(label);
pos = rem;
} else if len & 0b1100_0000 == 0b1100_0000 {
let (rem, leader) = be_u16(pos)?;
if offset > message.len() {
return Err(Err::Error(error_position!(pos, ErrorKind::OctDigit)));
}
+
+ if &message[offset..] == pos {
+ // Self reference, immedate infinite loop.
+ flags.insert(DNSNameFlags::INFINITE_LOOP);
+
+ // If we have followed a pointer, we can just break as
+ // we've already found the end of the input. But if we
+ // have not followed a pointer yet return a parse
+ // error.
+ if pivot != start {
+ break;
+ }
+ return Err(Err::Error(error_position!(pos, ErrorKind::OctDigit)));
+ }
+
pos = &message[offset..];
if pivot == start {
pivot = rem;
// Return error if we've looped a certain number of times.
count += 1;
+
if count > 255 {
+ flags.insert(DNSNameFlags::LABEL_LIMIT);
+
+ // Our segment limit has been reached, if we have hit a
+ // pointer we can just return the truncated name. If we
+ // have not hit a pointer, we need to bail with an error.
+ if pivot != start {
+ flags.insert(DNSNameFlags::TRUNCATED);
+ break;
+ }
return Err(Err::Error(error_position!(pos, ErrorKind::OctDigit)));
}
+
+ if name.len() > MAX_NAME_LEN {
+ name.truncate(MAX_NAME_LEN);
+ flags.insert(DNSNameFlags::TRUNCATED);
+
+ // If we have pivoted due to a pointer we know where the
+ // end of the data is, so we can break early. Otherwise
+ // we'll keep parsing in hopes to find the end of the name
+ // so parsing can continue.
+ if pivot != start {
+ break;
+ }
+ }
}
// If we followed a pointer we return the position after the first
// pointer followed. Is there a better way to see if these slices
// diverged from each other? A straight up comparison would
// actually check the contents.
- if pivot.len() != start.len() {
- return Ok((pivot, name));
+ if pivot != start {
+ Ok((pivot, DNSName { value: name, flags }))
+ } else {
+ Ok((pos, DNSName { value: name, flags }))
}
- return Ok((pos, name));
}
/// Parse answer entries.
let mut input = slice;
struct Answer<'a> {
- name: Vec<u8>,
+ name: DNSName,
rrtype: u16,
rrclass: u16,
ttl: u32,
];
let expected_remainder: &[u8] = &[0x00, 0x01, 0x00];
let (remainder, name) = dns_parse_name(buf, buf).unwrap();
- assert_eq!("client-cf.dropbox.com".as_bytes(), &name[..]);
+ assert_eq!("client-cf.dropbox.com".as_bytes(), &name.value[..]);
assert_eq!(remainder, expected_remainder);
}
let res1 = dns_parse_name(start1, message);
assert_eq!(
res1,
- Ok((&start1[22..], "www.suricata-ids.org".as_bytes().to_vec()))
+ Ok((
+ &start1[22..],
+ DNSName {
+ value: "www.suricata-ids.org".as_bytes().to_vec(),
+ flags: DNSNameFlags::default(),
+ }
+ ))
);
// The second name starts at offset 80, but is just a pointer
let res2 = dns_parse_name(start2, message);
assert_eq!(
res2,
- Ok((&start2[2..], "www.suricata-ids.org".as_bytes().to_vec()))
+ Ok((
+ &start2[2..],
+ DNSName {
+ value: "www.suricata-ids.org".as_bytes().to_vec(),
+ flags: DNSNameFlags::default()
+ }
+ ))
);
// The third name starts at offset 94, but is a pointer to a
let res3 = dns_parse_name(start3, message);
assert_eq!(
res3,
- Ok((&start3[2..], "suricata-ids.org".as_bytes().to_vec()))
+ Ok((
+ &start3[2..],
+ DNSName {
+ value: "suricata-ids.org".as_bytes().to_vec(),
+ flags: DNSNameFlags::default()
+ }
+ ))
);
// The fourth name starts at offset 110, but is a pointer to a
let res4 = dns_parse_name(start4, message);
assert_eq!(
res4,
- Ok((&start4[2..], "suricata-ids.org".as_bytes().to_vec()))
+ Ok((
+ &start4[2..],
+ DNSName {
+ value: "suricata-ids.org".as_bytes().to_vec(),
+ flags: DNSNameFlags::default()
+ }
+ ))
);
}
let res = dns_parse_name(start, message);
assert_eq!(
res,
- Ok((&start[2..], "block.g1.dropbox.com".as_bytes().to_vec()))
+ Ok((
+ &start[2..],
+ DNSName {
+ value: "block.g1.dropbox.com".as_bytes().to_vec(),
+ flags: DNSNameFlags::default()
+ }
+ ))
);
}
assert_eq!(request.queries.len(), 1);
let query = &request.queries[0];
- assert_eq!(query.name, "www.suricata-ids.org".as_bytes().to_vec());
+ assert_eq!(query.name.value, "www.suricata-ids.org".as_bytes().to_vec());
assert_eq!(query.rrtype, 1);
assert_eq!(query.rrclass, 1);
assert_eq!(
additional,
&DNSAnswerEntry {
- name: vec![],
+ name: DNSName {
+ value: vec![],
+ flags: DNSNameFlags::default()
+ },
rrtype: DNS_RECORD_TYPE_OPT,
rrclass: 0x1000, // for OPT this is UDP payload size
ttl: 0, // for OPT this is extended RCODE and flags
assert_eq!(request.queries.len(), 1);
let query = &request.queries[0];
- assert_eq!(query.name, "www.suricata-ids.org".as_bytes().to_vec());
+ assert_eq!(query.name.value, "www.suricata-ids.org".as_bytes().to_vec());
assert_eq!(query.rrtype, 1);
assert_eq!(query.rrclass, 1);
assert_eq!(
additional,
&DNSAnswerEntry {
- name: vec![],
+ name: DNSName {
+ value: vec![],
+ flags: DNSNameFlags::default()
+ },
rrtype: DNS_RECORD_TYPE_OPT,
rrclass: 0x1000, // for OPT this is requestor's UDP payload size
ttl: 0, // for OPT this is extended RCODE and flags
assert_eq!(response.answers.len(), 3);
let answer1 = &response.answers[0];
- assert_eq!(answer1.name, "www.suricata-ids.org".as_bytes().to_vec());
+ assert_eq!(
+ answer1.name.value,
+ "www.suricata-ids.org".as_bytes().to_vec()
+ );
assert_eq!(answer1.rrtype, 5);
assert_eq!(answer1.rrclass, 1);
assert_eq!(answer1.ttl, 3544);
assert_eq!(
answer1.data,
- DNSRData::CNAME("suricata-ids.org".as_bytes().to_vec())
+ DNSRData::CNAME(DNSName {
+ value: "suricata-ids.org".as_bytes().to_vec(),
+ flags: DNSNameFlags::default()
+ })
);
let answer2 = &response.answers[1];
assert_eq!(
answer2,
&DNSAnswerEntry {
- name: "suricata-ids.org".as_bytes().to_vec(),
+ name: DNSName {
+ value: "suricata-ids.org".as_bytes().to_vec(),
+ flags: DNSNameFlags::default()
+ },
rrtype: 1,
rrclass: 1,
ttl: 244,
assert_eq!(
answer3,
&DNSAnswerEntry {
- name: "suricata-ids.org".as_bytes().to_vec(),
+ name: DNSName {
+ value: "suricata-ids.org".as_bytes().to_vec(),
+ flags: DNSNameFlags::default()
+ },
rrtype: 1,
rrclass: 1,
ttl: 244,
assert_eq!(response.authorities.len(), 1);
let authority = &response.authorities[0];
- assert_eq!(authority.name, "oisf.net".as_bytes().to_vec());
+ assert_eq!(authority.name.value, "oisf.net".as_bytes().to_vec());
assert_eq!(authority.rrtype, 6);
assert_eq!(authority.rrclass, 1);
assert_eq!(authority.ttl, 899);
assert_eq!(
authority.data,
DNSRData::SOA(DNSRDataSOA {
- mname: "ns-110.awsdns-13.com".as_bytes().to_vec(),
- rname: "awsdns-hostmaster.amazon.com".as_bytes().to_vec(),
+ mname: DNSName {
+ value: "ns-110.awsdns-13.com".as_bytes().to_vec(),
+ flags: DNSNameFlags::default()
+ },
+ rname: DNSName {
+ value: "awsdns-hostmaster.amazon.com".as_bytes().to_vec(),
+ flags: DNSNameFlags::default()
+ },
serial: 1,
refresh: 7200,
retry: 900,
assert_eq!(
additional,
&DNSAnswerEntry {
- name: vec![],
+ name: DNSName {
+ value: vec![],
+ flags: DNSNameFlags::default()
+ },
rrtype: DNS_RECORD_TYPE_OPT,
rrclass: 0x0200, // for OPT this is UDP payload size
ttl: 0, // for OPT this is extended RCODE and flags
assert_eq!(response.queries.len(), 1);
let query = &response.queries[0];
- assert_eq!(query.name, "vaaaakardli.pirate.sea".as_bytes().to_vec());
+ assert_eq!(
+ query.name.value,
+ "vaaaakardli.pirate.sea".as_bytes().to_vec()
+ );
assert_eq!(query.rrtype, DNS_RECORD_TYPE_NULL);
assert_eq!(query.rrclass, 1);
assert_eq!(response.answers.len(), 1);
let answer = &response.answers[0];
- assert_eq!(answer.name, "vaaaakardli.pirate.sea".as_bytes().to_vec());
+ assert_eq!(
+ answer.name.value,
+ "vaaaakardli.pirate.sea".as_bytes().to_vec()
+ );
assert_eq!(answer.rrtype, DNS_RECORD_TYPE_NULL);
assert_eq!(answer.rrclass, 1);
assert_eq!(answer.ttl, 0);
assert_eq!(srv.weight, 1);
assert_eq!(srv.port, 5060);
assert_eq!(
- srv.target,
+ srv.target.value,
"sip-anycast-2.voice.google.com".as_bytes().to_vec()
);
} else {
assert_eq!(srv.weight, 1);
assert_eq!(srv.port, 5060);
assert_eq!(
- srv.target,
+ srv.target.value,
"sip-anycast-1.voice.google.com".as_bytes().to_vec()
);
} else {
panic!("Expected DNSRData::SRV");
}
}
+
+ #[test]
+ fn test_dns_parse_name_truncated() {
+ // Generate a non-compressed hostname over our maximum of 1024.
+ let mut buf: Vec<u8> = vec![];
+ for _ in 0..17 {
+ buf.push(0b0011_1111);
+ for _ in 0..63 {
+ buf.push(b'a');
+ }
+ }
+
+ let (rem, name) = dns_parse_name(&buf, &buf).unwrap();
+ assert_eq!(name.value.len(), MAX_NAME_LEN);
+ assert!(name.flags.contains(DNSNameFlags::TRUNCATED));
+ assert!(rem.is_empty());
+ }
+
+ #[test]
+ fn test_dns_parse_name_truncated_max_segments_no_pointer() {
+ let mut buf: Vec<u8> = vec![];
+ for _ in 0..256 {
+ buf.push(0b0000_0001);
+ buf.push(b'a');
+ }
+
+ // This should fail as we've hit the segment limit without a
+ // pointer, we'd need to keep parsing more segments to figure
+ // out where the next data point lies.
+ assert!(dns_parse_name(&buf, &buf).is_err());
+ }
+
+ #[test]
+ fn test_dns_parse_name_truncated_max_segments_with_pointer() {
+ let mut buf: Vec<u8> = vec![];
+
+ // "a" at the beginning of the buffer.
+ buf.push(0b0000_0001);
+ buf.push(b'a');
+
+ // Followed by a pointer back to the beginning.
+ buf.push(0b1100_0000);
+ buf.push(0b0000_0000);
+
+ // The start of the name, which is pointer to the beginning of
+ // the buffer.
+ buf.push(0b1100_0000);
+ buf.push(0b000_0000);
+
+ let (_rem, name) = dns_parse_name(&buf[4..], &buf).unwrap();
+ assert_eq!(name.value.len(), 255);
+ assert!(name.flags.contains(DNSNameFlags::TRUNCATED));
+ }
+
+ #[test]
+ fn test_dns_parse_name_self_reference() {
+ let mut buf = vec![];
+ buf.push(0b1100_0000);
+ buf.push(0b0000_0000);
+ assert!(dns_parse_name(&buf, &buf).is_err());
+ }
}