From 8005f50647d5c2256aca7c9e5e4af40f1ab061f3 Mon Sep 17 00:00:00 2001 From: Simon Dugas Date: Wed, 15 Apr 2020 15:20:22 +0000 Subject: [PATCH] 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 0dd8baa772..2a1ef6dd0c 100644 --- a/rust/src/dns/dns.rs +++ b/rust/src/dns/dns.rs @@ -231,13 +231,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 209ede26b6..d2cb6d1fdd 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 f40ea6836e..0165fa8e77 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 458f161083..ec900ab0cd 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); + } + } + } + }