@ -24,16 +24,48 @@ use nom7::multi::{count, length_data, many_m_n};
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 ( ) {
@ -47,10 +79,12 @@ fn dns_parse_name<'b>(start: &'b [u8], message: &'b [u8]) -> IResult<&'b [u8], V
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 ) ? ;
@ -58,6 +92,21 @@ fn dns_parse_name<'b>(start: &'b [u8], message: &'b [u8]) -> IResult<&'b [u8], V
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 ;
@ -68,19 +117,43 @@ fn dns_parse_name<'b>(start: &'b [u8], message: &'b [u8]) -> IResult<&'b [u8], V
// 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.
@ -100,7 +173,7 @@ fn dns_parse_answer<'a>(
let mut input = slice ;
struct Answer < ' a > {
name : Vec < u8 > ,
name : DNSName ,
rrtype : u16 ,
rrclass : u16 ,
ttl : u32 ,
@ -392,7 +465,7 @@ mod tests {
] ;
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 ) ;
}
@ -428,7 +501,13 @@ mod tests {
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
@ -437,7 +516,13 @@ mod tests {
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
@ -446,7 +531,13 @@ mod tests {
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
@ -455,7 +546,13 @@ mod tests {
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 ( )
}
) )
) ;
}
@ -490,7 +587,13 @@ mod tests {
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 ( )
}
) )
) ;
}
@ -528,7 +631,7 @@ mod tests {
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 ) ;
@ -539,7 +642,10 @@ mod tests {
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
@ -586,7 +692,7 @@ mod tests {
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 ) ;
@ -597,7 +703,10 @@ mod tests {
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
@ -661,20 +770,29 @@ mod tests {
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 ,
@ -686,7 +804,10 @@ mod tests {
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 ,
@ -737,15 +858,21 @@ mod tests {
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 ,
@ -761,7 +888,10 @@ mod tests {
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
@ -804,14 +934,20 @@ mod tests {
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 ) ;
@ -893,7 +1029,7 @@ mod tests {
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 {
@ -905,11 +1041,72 @@ mod tests {
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 ( ) ) ;
}
}