From 025b4cb656104ea8a44ca9e18faedc772f147e6a Mon Sep 17 00:00:00 2001 From: Simon Dugas Date: Wed, 15 Apr 2020 15:20:22 +0000 Subject: [PATCH 1/4] dns: refactor to handle more rdata formats Represent rdata as `DNSRData` enum variants instead of `Vec`. This will allow parsing/logging of more complex formats like SOA. --- rust/src/dns/dns.rs | 36 ++++++++- rust/src/dns/log.rs | 95 +++++++++++------------ rust/src/dns/lua.rs | 39 +++++++--- rust/src/dns/parser.rs | 167 ++++++++++++++++++++++++++++++++--------- 4 files changed, 240 insertions(+), 97 deletions(-) diff --git a/rust/src/dns/dns.rs b/rust/src/dns/dns.rs index fda3a6b8c319..9b1245adb92e 100644 --- a/rust/src/dns/dns.rs +++ b/rust/src/dns/dns.rs @@ -232,13 +232,47 @@ pub struct DNSQueryEntry { pub rrclass: u16, } +#[derive(Debug,PartialEq)] +pub struct DNSRDataSOA { + pub data: Vec, +} + +#[derive(Debug,PartialEq)] +pub struct DNSRDataSSHFP { + /// Algorithm number + pub algo: u8, + /// Fingerprint type + pub fp_type: u8, + /// Fingerprint + pub fingerprint: Vec, +} + +/// Represents RData of various formats +#[derive(Debug,PartialEq)] +pub enum DNSRData { + // RData is an address + A(Vec), + AAAA(Vec), + // RData is a domain name + CNAME(Vec), + PTR(Vec), + MX(Vec), + // RData is text + TXT(Vec), + // RData has several fields + SOA(DNSRDataSOA), + SSHFP(DNSRDataSSHFP), + // RData for remaining types is sometimes ignored + Unknown(Vec), +} + #[derive(Debug,PartialEq)] pub struct DNSAnswerEntry { pub name: Vec, pub rrtype: u16, pub rrclass: u16, pub ttl: u32, - pub data: Vec, + pub data: DNSRData, } #[derive(Debug)] diff --git a/rust/src/dns/log.rs b/rust/src/dns/log.rs index 209ede26b6f8..d2cb6d1fdd0f 100644 --- a/rust/src/dns/log.rs +++ b/rust/src/dns/log.rs @@ -396,24 +396,21 @@ pub fn dns_print_addr(addr: &Vec) -> std::string::String { } /// Log the SSHPF in an DNSAnswerEntry. -fn dns_log_sshfp(answer: &DNSAnswerEntry) -> Result, JsonError> +fn dns_log_sshfp(sshfp: &DNSRDataSSHFP) -> Result { - // Need at least 3 bytes - TODO: log something if we don't? - if answer.data.len() < 3 { - return Ok(None) - } - - let mut sshfp = JsonBuilder::new_object(); + let mut js = JsonBuilder::new_object(); let mut hex = Vec::new(); - for byte in &answer.data[2..] { + for byte in &sshfp.fingerprint { hex.push(format!("{:02x}", byte)); } - sshfp.set_string("fingerprint", &hex.join(":"))?; - sshfp.set_uint("algo", answer.data[0] as u64)?; - sshfp.set_uint("type", answer.data[1] as u64)?; - return Ok(Some(sshfp)); + js.set_string("fingerprint", &hex.join(":"))?; + js.set_uint("algo", sshfp.algo as u64)?; + js.set_uint("type", sshfp.fp_type as u64)?; + + js.close()?; + return Ok(js); } fn dns_log_json_answer_detail(answer: &DNSAnswerEntry) -> Result @@ -424,21 +421,19 @@ fn dns_log_json_answer_detail(answer: &DNSAnswerEntry) -> Result { - jsa.set_string("rdata", &dns_print_addr(&answer.data))?; + match &answer.data { + DNSRData::A(addr) | DNSRData::AAAA(addr) => { + jsa.set_string("rdata", &dns_print_addr(&addr))?; + } + DNSRData::CNAME(bytes) | + DNSRData::MX(bytes) | + DNSRData::TXT(bytes) | + DNSRData::PTR(bytes) => { + jsa.set_string_from_bytes("rdata", &bytes)?; + } + DNSRData::SSHFP(sshfp) => { + jsa.set_object("sshfp", &dns_log_sshfp(&sshfp)?)?; } - DNS_RECORD_TYPE_CNAME | - DNS_RECORD_TYPE_MX | - DNS_RECORD_TYPE_TXT | - DNS_RECORD_TYPE_PTR => { - jsa.set_string_from_bytes("rdata", &answer.data)?; - }, - DNS_RECORD_TYPE_SSHFP => { - if let Some(sshfp) = dns_log_sshfp(answer)? { - jsa.set_object("sshfp", &sshfp)?; - } - }, _ => {} } @@ -488,37 +483,35 @@ fn dns_log_json_answer(js: &mut JsonBuilder, response: &DNSResponse, flags: u64) if flags & LOG_FORMAT_GROUPED != 0 { let type_string = dns_rrtype_string(answer.rrtype); - match answer.rrtype { - DNS_RECORD_TYPE_A | DNS_RECORD_TYPE_AAAA => { + match &answer.data { + DNSRData::A(addr) | DNSRData::AAAA(addr) => { if !answer_types.contains_key(&type_string) { answer_types.insert(type_string.to_string(), JsonBuilder::new_array()); } if let Some(a) = answer_types.get_mut(&type_string) { - a.append_string(&dns_print_addr(&answer.data))?; + a.append_string(&dns_print_addr(&addr))?; } } - DNS_RECORD_TYPE_CNAME | - DNS_RECORD_TYPE_MX | - DNS_RECORD_TYPE_TXT | - DNS_RECORD_TYPE_PTR => { + DNSRData::CNAME(bytes) | + DNSRData::MX(bytes) | + DNSRData::TXT(bytes) | + DNSRData::PTR(bytes) => { if !answer_types.contains_key(&type_string) { answer_types.insert(type_string.to_string(), JsonBuilder::new_array()); } if let Some(a) = answer_types.get_mut(&type_string) { - a.append_string_from_bytes(&answer.data)?; + a.append_string_from_bytes(&bytes)?; } }, - DNS_RECORD_TYPE_SSHFP => { + DNSRData::SSHFP(sshfp) => { if !answer_types.contains_key(&type_string) { answer_types.insert(type_string.to_string(), JsonBuilder::new_array()); } if let Some(a) = answer_types.get_mut(&type_string) { - if let Some(sshfp) = dns_log_sshfp(&answer)? { - a.append_object(&sshfp)?; - } + a.append_object(&dns_log_sshfp(&sshfp)?)?; } }, _ => {} @@ -659,21 +652,19 @@ fn dns_log_json_answer_v1(header: &DNSHeader, answer: &DNSAnswerEntry) js.set_string("rrtype", &dns_rrtype_string(answer.rrtype))?; js.set_uint("ttl", answer.ttl as u64)?; - match answer.rrtype { - DNS_RECORD_TYPE_A | DNS_RECORD_TYPE_AAAA => { - js.set_string("rdata", &dns_print_addr(&answer.data))?; + match &answer.data { + DNSRData::A(addr) | DNSRData::AAAA(addr) => { + js.set_string("rdata", &dns_print_addr(&addr))?; + } + DNSRData::CNAME(bytes) | + DNSRData::MX(bytes) | + DNSRData::TXT(bytes) | + DNSRData::PTR(bytes) => { + js.set_string_from_bytes("rdata", &bytes)?; + } + DNSRData::SSHFP(sshfp) => { + js.set_object("sshfp", &dns_log_sshfp(&sshfp)?)?; } - DNS_RECORD_TYPE_CNAME | - DNS_RECORD_TYPE_MX | - DNS_RECORD_TYPE_TXT | - DNS_RECORD_TYPE_PTR => { - js.set_string_from_bytes("rdata", &answer.data)?; - }, - DNS_RECORD_TYPE_SSHFP => { - if let Some(sshfp) = dns_log_sshfp(&answer)? { - js.set_object("sshfp", &sshfp)?; - } - }, _ => {} } diff --git a/rust/src/dns/lua.rs b/rust/src/dns/lua.rs index f40ea6836ea9..0165fa8e77e8 100644 --- a/rust/src/dns/lua.rs +++ b/rust/src/dns/lua.rs @@ -165,17 +165,38 @@ pub extern "C" fn rs_dns_lua_get_answer_table(clua: &mut CLuaState, lua.pushstring(&String::from_utf8_lossy(&answer.name)); lua.settable(-3); - if answer.data.len() > 0 { - lua.pushstring("addr"); - match answer.rrtype { - DNS_RECORD_TYPE_A | DNS_RECORD_TYPE_AAAA => { - lua.pushstring(&dns_print_addr(&answer.data)); + // All rdata types are pushed to "addr" for backwards compatibility + match answer.data { + DNSRData::A(ref bytes) | DNSRData::AAAA(ref bytes) => { + if bytes.len() > 0 { + lua.pushstring("addr"); + lua.pushstring(&dns_print_addr(&bytes)); + lua.settable(-3); } - _ => { - lua.pushstring(&String::from_utf8_lossy(&answer.data)); + }, + DNSRData::CNAME(ref bytes) | + DNSRData::MX(ref bytes) | + DNSRData::TXT(ref bytes) | + DNSRData::PTR(ref bytes) | + DNSRData::Unknown(ref bytes) => { + if bytes.len() > 0 { + lua.pushstring("addr"); + lua.pushstring(&String::from_utf8_lossy(&bytes)); + lua.settable(-3); } - } - lua.settable(-3); + }, + DNSRData::SOA(ref soa) => { + if soa.data.len() > 0 { + lua.pushstring("addr"); + lua.pushstring(&String::from_utf8_lossy(&soa.data)); + lua.settable(-3); + } + }, + DNSRData::SSHFP(ref sshfp) => { + lua.pushstring("addr"); + lua.pushstring(&String::from_utf8_lossy(&sshfp.fingerprint)); + lua.settable(-3); + }, } lua.settable(-3); } diff --git a/rust/src/dns/parser.rs b/rust/src/dns/parser.rs index 458f16108380..ec900ab0cd29 100644 --- a/rust/src/dns/parser.rs +++ b/rust/src/dns/parser.rs @@ -18,6 +18,7 @@ //! Nom parsers for DNS. use nom::IResult; +use nom::combinator::rest; use nom::error::ErrorKind; use nom::multi::length_data; use nom::number::streaming::{be_u8, be_u16, be_u32}; @@ -180,7 +181,7 @@ fn dns_parse_answer<'a>(slice: &'a [u8], message: &'a [u8], count: usize) 1 } }; - let result: IResult<&'a [u8], Vec>> = + let result: IResult<&'a [u8], Vec> = do_parse!( data, rdata: many_m_n!(1, n, @@ -257,40 +258,99 @@ pub fn dns_parse_query<'a>(input: &'a [u8], ) } +fn dns_parse_rdata_a<'a>(input: &'a [u8]) -> IResult<&'a [u8], DNSRData> { + do_parse!( + input, + data: take!(input.len()) >> + (DNSRData::A(data.to_vec())) + ) +} + +fn dns_parse_rdata_aaaa<'a>(input: &'a [u8]) -> IResult<&'a [u8], DNSRData> { + do_parse!( + input, + data: take!(input.len()) >> + (DNSRData::AAAA(data.to_vec())) + ) +} + +fn dns_parse_rdata_cname<'a>(input: &'a [u8], message: &'a [u8]) + -> IResult<&'a [u8], DNSRData> { + dns_parse_name(input, message).map(|(input, name)| + (input, DNSRData::CNAME(name))) +} + +fn dns_parse_rdata_ptr<'a>(input: &'a [u8], message: &'a [u8]) + -> IResult<&'a [u8], DNSRData> { + dns_parse_name(input, message).map(|(input, name)| + (input, DNSRData::PTR(name))) +} + +fn dns_parse_rdata_soa<'a>(input: &'a [u8], message: &'a [u8]) + -> IResult<&'a [u8], DNSRData> { + dns_parse_name(input, message).map(|(input, name)| + (input, DNSRData::SOA(DNSRDataSOA{data: name}))) +} + +fn dns_parse_rdata_mx<'a>(input: &'a [u8], message: &'a [u8]) + -> IResult<&'a [u8], DNSRData> { + // For MX we skip over the preference field before + // parsing out the name. + do_parse!( + input, + be_u16 >> + name: call!(dns_parse_name, message) >> + (DNSRData::MX(name)) + ) +} + +fn dns_parse_rdata_txt<'a>(input: &'a [u8]) + -> IResult<&'a [u8], DNSRData> { + do_parse!( + input, + len: be_u8 >> + txt: take!(len) >> + (DNSRData::TXT(txt.to_vec())) + ) +} + +fn dns_parse_rdata_sshfp<'a>(input: &'a [u8]) + -> IResult<&'a [u8], DNSRData> { + do_parse!( + input, + algo: be_u8 >> + fp_type: be_u8 >> + fingerprint: call!(rest) >> + (DNSRData::SSHFP(DNSRDataSSHFP{ + algo, + fp_type, + fingerprint: fingerprint.to_vec() + })) + ) +} + +fn dns_parse_rdata_unknown<'a>(input: &'a [u8]) + -> IResult<&'a [u8], DNSRData> { + do_parse!( + input, + data: take!(input.len()) >> + (DNSRData::Unknown(data.to_vec())) + ) +} + pub fn dns_parse_rdata<'a>(input: &'a [u8], message: &'a [u8], rrtype: u16) - -> IResult<&'a [u8], Vec> + -> IResult<&'a [u8], DNSRData> { match rrtype { - DNS_RECORD_TYPE_CNAME | - DNS_RECORD_TYPE_PTR | - DNS_RECORD_TYPE_SOA => { - dns_parse_name(input, message) - }, - DNS_RECORD_TYPE_MX => { - // For MX we we skip over the preference field before - // parsing out the name. - do_parse!( - input, - be_u16 >> - name: call!(dns_parse_name, message) >> - (name) - ) - }, - DNS_RECORD_TYPE_TXT => { - do_parse!( - input, - len: be_u8 >> - txt: take!(len) >> - (txt.to_vec()) - ) - }, - _ => { - do_parse!( - input, - data: take!(input.len()) >> - (data.to_vec()) - ) - } + DNS_RECORD_TYPE_A => dns_parse_rdata_a(input), + DNS_RECORD_TYPE_AAAA => dns_parse_rdata_aaaa(input), + DNS_RECORD_TYPE_CNAME => dns_parse_rdata_cname(input, message), + DNS_RECORD_TYPE_PTR => dns_parse_rdata_ptr(input, message), + DNS_RECORD_TYPE_SOA => dns_parse_rdata_soa(input, message), + DNS_RECORD_TYPE_MX => dns_parse_rdata_mx(input, message), + DNS_RECORD_TYPE_TXT => dns_parse_rdata_txt(input), + DNS_RECORD_TYPE_SSHFP => dns_parse_rdata_sshfp(input), + _ => dns_parse_rdata_unknown(input), } } @@ -512,7 +572,7 @@ mod tests { assert_eq!(answer1.rrclass, 1); assert_eq!(answer1.ttl, 3544); assert_eq!(answer1.data, - "suricata-ids.org".as_bytes().to_vec()); + DNSRData::CNAME("suricata-ids.org".as_bytes().to_vec())); let answer2 = &response.answers[1]; assert_eq!(answer2, &DNSAnswerEntry{ @@ -520,7 +580,7 @@ mod tests { rrtype: 1, rrclass: 1, ttl: 244, - data: [192, 0, 78, 24].to_vec(), + data: DNSRData::A([192, 0, 78, 24].to_vec()), }); let answer3 = &response.answers[2]; @@ -529,7 +589,7 @@ mod tests { rrtype: 1, rrclass: 1, ttl: 244, - data: [192, 0, 78, 25].to_vec(), + data: DNSRData::A([192, 0, 78, 25].to_vec()), }) }, @@ -539,4 +599,41 @@ mod tests { } } + #[test] + fn test_dns_parse_rdata_sshfp() { + // Dummy data since we don't have a pcap sample. + let data: &[u8] = &[ + // algo: DSS + 0x02, + // fp_type: SHA-1 + 0x01, + // fingerprint: 123456789abcdef67890123456789abcdef67890 + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf6, 0x78, 0x90, + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf6, 0x78, 0x90 + ]; + + let res = dns_parse_rdata_sshfp(data); + match res { + Ok((rem, rdata)) => { + + // The data should be fully parsed. + assert_eq!(rem.len(), 0); + + match rdata { + DNSRData::SSHFP(sshfp) => { + assert_eq!(sshfp.algo, 2); + assert_eq!(sshfp.fp_type, 1); + assert_eq!(sshfp.fingerprint, &data[2..]); + }, + _ => { + assert!(false); + } + } + }, + _ => { + assert!(false); + } + } + } + } From 0d208688de56150c17a341d089a7a53fab6f13b5 Mon Sep 17 00:00:00 2001 From: Simon Dugas Date: Fri, 24 Apr 2020 18:03:23 +0000 Subject: [PATCH 2/4] dns: use nom's rest to take all remaining rdata Using nom's `rest` combinator eliminates the need to call the do_parse macro for parsing a single element. --- rust/src/dns/parser.rs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/rust/src/dns/parser.rs b/rust/src/dns/parser.rs index ec900ab0cd29..8fea18457e58 100644 --- a/rust/src/dns/parser.rs +++ b/rust/src/dns/parser.rs @@ -259,19 +259,11 @@ pub fn dns_parse_query<'a>(input: &'a [u8], } fn dns_parse_rdata_a<'a>(input: &'a [u8]) -> IResult<&'a [u8], DNSRData> { - do_parse!( - input, - data: take!(input.len()) >> - (DNSRData::A(data.to_vec())) - ) + rest(input).map(|(input, data)| (input, DNSRData::A(data.to_vec()))) } fn dns_parse_rdata_aaaa<'a>(input: &'a [u8]) -> IResult<&'a [u8], DNSRData> { - do_parse!( - input, - data: take!(input.len()) >> - (DNSRData::AAAA(data.to_vec())) - ) + rest(input).map(|(input, data)| (input, DNSRData::AAAA(data.to_vec()))) } fn dns_parse_rdata_cname<'a>(input: &'a [u8], message: &'a [u8]) @@ -331,11 +323,7 @@ fn dns_parse_rdata_sshfp<'a>(input: &'a [u8]) fn dns_parse_rdata_unknown<'a>(input: &'a [u8]) -> IResult<&'a [u8], DNSRData> { - do_parse!( - input, - data: take!(input.len()) >> - (DNSRData::Unknown(data.to_vec())) - ) + rest(input).map(|(input, data)| (input, DNSRData::Unknown(data.to_vec()))) } pub fn dns_parse_rdata<'a>(input: &'a [u8], message: &'a [u8], rrtype: u16) From c5d81e59f30f9f8f60ceabf487abb9fe20fc52d5 Mon Sep 17 00:00:00 2001 From: Simon Dugas Date: Fri, 24 Apr 2020 17:57:20 +0000 Subject: [PATCH 3/4] dns: parse and log fields for SOA record type Added `dns_parse_rdata_soa` to parse SOA fields into an `DNSRDataSOA` struct. Added logging for answer and authority SOA records in both version 1 & 2, as well as grouped formats. --- rust/src/dns/dns.rs | 15 +++++++- rust/src/dns/log.rs | 33 +++++++++++++++- rust/src/dns/lua.rs | 4 +- rust/src/dns/parser.rs | 86 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 132 insertions(+), 6 deletions(-) diff --git a/rust/src/dns/dns.rs b/rust/src/dns/dns.rs index 9b1245adb92e..992d9f4b2df1 100644 --- a/rust/src/dns/dns.rs +++ b/rust/src/dns/dns.rs @@ -234,7 +234,20 @@ pub struct DNSQueryEntry { #[derive(Debug,PartialEq)] pub struct DNSRDataSOA { - pub data: Vec, + /// Primary name server for this zone + pub mname: Vec, + /// Authority's mailbox + pub rname: Vec, + /// Serial version number + pub serial: u32, + /// Refresh interval (seconds) + pub refresh: u32, + /// Retry interval (seconds) + pub retry: u32, + /// Upper time limit until zone is no longer authoritative (seconds) + pub expire: u32, + /// Minimum ttl for records in this zone (seconds) + pub minimum: u32, } #[derive(Debug,PartialEq)] diff --git a/rust/src/dns/log.rs b/rust/src/dns/log.rs index d2cb6d1fdd0f..c27076be45ec 100644 --- a/rust/src/dns/log.rs +++ b/rust/src/dns/log.rs @@ -395,7 +395,23 @@ pub fn dns_print_addr(addr: &Vec) -> std::string::String { } } -/// Log the SSHPF in an DNSAnswerEntry. +/// Log SOA section fields. +fn dns_log_soa(soa: &DNSRDataSOA) -> Result { + let mut js = JsonBuilder::new_object(); + + js.set_string_from_bytes("mname", &soa.mname)?; + js.set_string_from_bytes("rname", &soa.rname)?; + 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("expire", soa.expire as u64)?; + js.set_uint("minimum", soa.minimum as u64)?; + + js.close()?; + return Ok(js); +} + +/// Log SSHFP section fields. fn dns_log_sshfp(sshfp: &DNSRDataSSHFP) -> Result { let mut js = JsonBuilder::new_object(); @@ -431,6 +447,9 @@ fn dns_log_json_answer_detail(answer: &DNSAnswerEntry) -> Result { jsa.set_string_from_bytes("rdata", &bytes)?; } + DNSRData::SOA(soa) => { + jsa.set_object("soa", &dns_log_soa(&soa)?)?; + } DNSRData::SSHFP(sshfp) => { jsa.set_object("sshfp", &dns_log_sshfp(&sshfp)?)?; } @@ -505,6 +524,15 @@ fn dns_log_json_answer(js: &mut JsonBuilder, response: &DNSResponse, flags: u64) a.append_string_from_bytes(&bytes)?; } }, + DNSRData::SOA(soa) => { + if !answer_types.contains_key(&type_string) { + answer_types.insert(type_string.to_string(), + JsonBuilder::new_array()); + } + if let Some(a) = answer_types.get_mut(&type_string) { + a.append_object(&dns_log_soa(&soa)?)?; + } + }, DNSRData::SSHFP(sshfp) => { if !answer_types.contains_key(&type_string) { answer_types.insert(type_string.to_string(), @@ -662,6 +690,9 @@ fn dns_log_json_answer_v1(header: &DNSHeader, answer: &DNSAnswerEntry) DNSRData::PTR(bytes) => { js.set_string_from_bytes("rdata", &bytes)?; } + DNSRData::SOA(soa) => { + js.set_object("soa", &dns_log_soa(&soa)?)?; + } DNSRData::SSHFP(sshfp) => { js.set_object("sshfp", &dns_log_sshfp(&sshfp)?)?; } diff --git a/rust/src/dns/lua.rs b/rust/src/dns/lua.rs index 0165fa8e77e8..6a7ea2692137 100644 --- a/rust/src/dns/lua.rs +++ b/rust/src/dns/lua.rs @@ -186,9 +186,9 @@ pub extern "C" fn rs_dns_lua_get_answer_table(clua: &mut CLuaState, } }, DNSRData::SOA(ref soa) => { - if soa.data.len() > 0 { + if soa.mname.len() > 0 { lua.pushstring("addr"); - lua.pushstring(&String::from_utf8_lossy(&soa.data)); + lua.pushstring(&String::from_utf8_lossy(&soa.mname)); lua.settable(-3); } }, diff --git a/rust/src/dns/parser.rs b/rust/src/dns/parser.rs index 8fea18457e58..0f7038dda614 100644 --- a/rust/src/dns/parser.rs +++ b/rust/src/dns/parser.rs @@ -280,8 +280,25 @@ fn dns_parse_rdata_ptr<'a>(input: &'a [u8], message: &'a [u8]) fn dns_parse_rdata_soa<'a>(input: &'a [u8], message: &'a [u8]) -> IResult<&'a [u8], DNSRData> { - dns_parse_name(input, message).map(|(input, name)| - (input, DNSRData::SOA(DNSRDataSOA{data: name}))) + do_parse!( + input, + mname: call!(dns_parse_name, message) >> + rname: call!(dns_parse_name, message) >> + serial: be_u32 >> + refresh: be_u32 >> + retry: be_u32 >> + expire: be_u32 >> + minimum: be_u32 >> + (DNSRData::SOA(DNSRDataSOA{ + mname, + rname, + serial, + refresh, + retry, + expire, + minimum, + })) + ) } fn dns_parse_rdata_mx<'a>(input: &'a [u8], message: &'a [u8]) @@ -587,6 +604,71 @@ mod tests { } } + #[test] + fn test_dns_parse_response_nxdomain_soa() { + // DNS response with an SOA authority record from + // dns-udp-nxdomain-soa.pcap. + let pkt: &[u8] = &[ + 0x82, 0x95, 0x81, 0x83, 0x00, 0x01, /* j....... */ + 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x03, 0x64, /* .......d */ + 0x6e, 0x65, 0x04, 0x6f, 0x69, 0x73, 0x66, 0x03, /* ne.oisf. */ + 0x6e, 0x65, 0x74, 0x00, 0x00, 0x01, 0x00, 0x01, /* net..... */ + 0xc0, 0x10, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, /* ........ */ + 0x03, 0x83, 0x00, 0x45, 0x06, 0x6e, 0x73, 0x2d, /* ...E.ns- */ + 0x31, 0x31, 0x30, 0x09, 0x61, 0x77, 0x73, 0x64, /* 110.awsd */ + 0x6e, 0x73, 0x2d, 0x31, 0x33, 0x03, 0x63, 0x6f, /* ns-13.co */ + 0x6d, 0x00, 0x11, 0x61, 0x77, 0x73, 0x64, 0x6e, /* m..awsdn */ + 0x73, 0x2d, 0x68, 0x6f, 0x73, 0x74, 0x6d, 0x61, /* s-hostma */ + 0x73, 0x74, 0x65, 0x72, 0x06, 0x61, 0x6d, 0x61, /* ster.ama */ + 0x7a, 0x6f, 0x6e, 0xc0, 0x3b, 0x00, 0x00, 0x00, /* zon.;... */ + 0x01, 0x00, 0x00, 0x1c, 0x20, 0x00, 0x00, 0x03, /* .... ... */ + 0x84, 0x00, 0x12, 0x75, 0x00, 0x00, 0x01, 0x51, /* ...u...Q */ + 0x80, 0x00, 0x00, 0x29, 0x02, 0x00, 0x00, 0x00, /* ...).... */ + 0x00, 0x00, 0x00, 0x00 /* .... */ + ]; + + let res = dns_parse_response(pkt); + match res { + Ok((rem, response)) => { + + // For now we have some remainder data as there is an + // additional record type we don't parse yet. + assert!(rem.len() > 0); + + assert_eq!(response.header, DNSHeader{ + tx_id: 0x8295, + flags: 0x8183, + questions: 1, + answer_rr: 0, + authority_rr: 1, + additional_rr: 1, + }); + + assert_eq!(response.authorities.len(), 1); + + let authority = &response.authorities[0]; + assert_eq!(authority.name, + "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(), + serial: 1, + refresh: 7200, + retry: 900, + expire: 1209600, + minimum: 86400, + })); + }, + _ => { + assert!(false); + } + } + } + #[test] fn test_dns_parse_rdata_sshfp() { // Dummy data since we don't have a pcap sample. From f915f56b928aac37d9513f93f827472cbbcc541e Mon Sep 17 00:00:00 2001 From: Simon Dugas Date: Thu, 23 Apr 2020 18:29:23 +0000 Subject: [PATCH 4/4] doc: dns - document additional fields in eve event Documentation of additional fields for soa and sshfp. Also some minor doc fixes and updates. --- doc/userguide/output/eve/eve-json-format.rst | 41 +++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/doc/userguide/output/eve/eve-json-format.rst b/doc/userguide/output/eve/eve-json-format.rst index 3087692d2ae9..407a040e1c2c 100644 --- a/doc/userguide/output/eve/eve-json-format.rst +++ b/doc/userguide/output/eve/eve-json-format.rst @@ -401,11 +401,30 @@ Outline of fields seen in the different kinds of DNS events: * "rcode": (ex: NOERROR) * "rrname": Resource Record Name (ex: a domain name) * "rrtype": Resource Record Type (ex: A, AAAA, NS, PTR) -* "rdata": Resource Data (ex. IP that domain name resolves to) +* "rdata": Resource Data (ex: IP that domain name resolves to) * "ttl": Time-To-Live for this resource record +More complex DNS record types may log additional fields for resource data: -One can also control which RR types are logged explicitly from additional custom field enabled in the suricata.yaml file. If custom field is not specified, all RR types are logged. More than 50 values can be specified with the custom field and can be used as following: +* "soa": Section containing fields for the SOA (start of authority) record type + + * "mname": Primary name server for this zone + * "rname": Authority's mailbox + * "serial": Serial version number + * "refresh": Refresh interval (seconds) + * "retry": Retry interval (seconds) + * "expire": Upper time limit until zone is no longer authoritative (seconds) + * "minimum": Minimum ttl for records in this zone (seconds) + +* "sshfp": section containing fields for the SSHFP (ssh fingerprint) record type + + * "fingerprint": Hex format of the fingerprint (ex: ``12:34:56:78:9a:bc:de:...``) + * "algo": Algorithm number (ex: 1 for RSA, 2 for DSS) + * "type": Fingerprint type (ex: 1 for SHA-1) + +One can control which RR types are logged by using the "types" field in the +suricata.yaml file. If this field is not specified, all RR types are logged. +More than 50 values can be specified with this field as shown below: :: @@ -423,14 +442,16 @@ One can also control which RR types are logged explicitly from additional custom types: - alert - dns: - # control logging of queries and answers - # default yes, no to disable - query: yes # enable logging of DNS queries - answer: yes # enable logging of DNS answers - # control which RR types are logged - # all enabled if custom not specified - #custom: [a, aaaa, cname, mx, ns, ptr, txt] - custom: [a, ns, md, mf, cname, soa, mb, mg, mr, null, + # Control logging of requests and responses: + # - requests: enable logging of DNS queries + # - responses: enable logging of DNS answers + # By default both requests and responses are logged. + requests: yes + responses: yes + # DNS record types to log, based on the query type. + # Default: all. + #types: [a, aaaa, cname, mx, ns, ptr, txt] + types: [a, ns, md, mf, cname, soa, mb, mg, mr, null, wks, ptr, hinfo, minfo, mx, txt, rp, afsdb, x25, isdn, rt, nsap, nsapptr, sig, key, px, gpos, aaaa, loc, nxt, srv, atma, naptr, kx, cert, a6, dname, opt, apl, ds,