summaryrefslogtreecommitdiffstats
path: root/rust/src/dns/parser.rs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 17:39:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 17:39:49 +0000
commita0aa2307322cd47bbf416810ac0292925e03be87 (patch)
tree37076262a026c4b48c8a0e84f44ff9187556ca35 /rust/src/dns/parser.rs
parentInitial commit. (diff)
downloadsuricata-a0aa2307322cd47bbf416810ac0292925e03be87.tar.xz
suricata-a0aa2307322cd47bbf416810ac0292925e03be87.zip
Adding upstream version 1:7.0.3.upstream/1%7.0.3
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'rust/src/dns/parser.rs')
-rw-r--r--rust/src/dns/parser.rs851
1 files changed, 851 insertions, 0 deletions
diff --git a/rust/src/dns/parser.rs b/rust/src/dns/parser.rs
new file mode 100644
index 0000000..a1d97a5
--- /dev/null
+++ b/rust/src/dns/parser.rs
@@ -0,0 +1,851 @@
+/* Copyright (C) 2017 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+//! Nom parsers for DNS.
+
+use crate::dns::dns::*;
+use nom7::combinator::{complete, rest};
+use nom7::error::ErrorKind;
+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};
+
+// Parse a DNS header.
+pub fn dns_parse_header(i: &[u8]) -> IResult<&[u8], DNSHeader> {
+ let (i, tx_id) = be_u16(i)?;
+ let (i, flags) = be_u16(i)?;
+ let (i, questions) = be_u16(i)?;
+ let (i, answer_rr) = be_u16(i)?;
+ let (i, authority_rr) = be_u16(i)?;
+ let (i, additional_rr) = be_u16(i)?;
+ Ok((
+ i,
+ DNSHeader {
+ tx_id,
+ flags,
+ questions,
+ answer_rr,
+ authority_rr,
+ additional_rr,
+ },
+ ))
+}
+
+/// Parse a DNS name.
+///
+/// Parameters:
+/// start: the start of the name
+/// message: the complete message that start is a part of with the DNS header
+pub fn dns_parse_name<'b>(start: &'b [u8], message: &'b [u8]) -> IResult<&'b [u8], Vec<u8>> {
+ let mut pos = start;
+ let mut pivot = start;
+ let mut name: Vec<u8> = Vec::with_capacity(32);
+ let mut count = 0;
+
+ loop {
+ if pos.is_empty() {
+ break;
+ }
+
+ let len = pos[0];
+
+ if len == 0x00 {
+ pos = &pos[1..];
+ break;
+ } else if len & 0b1100_0000 == 0 {
+ let (rem, label) = length_data(be_u8)(pos)?;
+ if !name.is_empty() {
+ name.push(b'.');
+ }
+ name.extend(label);
+ pos = rem;
+ } else if len & 0b1100_0000 == 0b1100_0000 {
+ let (rem, leader) = be_u16(pos)?;
+ let offset = usize::from(leader) & 0x3fff;
+ if offset > message.len() {
+ return Err(Err::Error(error_position!(pos, ErrorKind::OctDigit)));
+ }
+ pos = &message[offset..];
+ if pivot == start {
+ pivot = rem;
+ }
+ } else {
+ return Err(Err::Error(error_position!(pos, ErrorKind::OctDigit)));
+ }
+
+ // Return error if we've looped a certain number of times.
+ count += 1;
+ if count > 255 {
+ return Err(Err::Error(error_position!(pos, ErrorKind::OctDigit)));
+ }
+ }
+
+ // 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));
+ }
+ return Ok((pos, name));
+}
+
+/// Parse answer entries.
+///
+/// In keeping with the C implementation, answer values that can
+/// contain multiple answers get expanded into their own answer
+/// records. An example of this is a TXT record with multiple strings
+/// in it - each string will be expanded to its own answer record.
+///
+/// This function could be a made a whole lot simpler if we logged a
+/// multi-string TXT entry as a single quote string, similar to the
+/// output of dig. Something to consider for a future version.
+fn dns_parse_answer<'a>(
+ slice: &'a [u8], message: &'a [u8], count: usize,
+) -> IResult<&'a [u8], Vec<DNSAnswerEntry>> {
+ let mut answers = Vec::new();
+ let mut input = slice;
+
+ struct Answer<'a> {
+ name: Vec<u8>,
+ rrtype: u16,
+ rrclass: u16,
+ ttl: u32,
+ data: &'a [u8],
+ }
+
+ fn subparser<'a>(i: &'a [u8], message: &'a [u8]) -> IResult<&'a [u8], Answer<'a>> {
+ let (i, name) = dns_parse_name(i, message)?;
+ let (i, rrtype) = be_u16(i)?;
+ let (i, rrclass) = be_u16(i)?;
+ let (i, ttl) = be_u32(i)?;
+ let (i, data) = length_data(be_u16)(i)?;
+ let answer = Answer {
+ name,
+ rrtype,
+ rrclass,
+ ttl,
+ data,
+ };
+ Ok((i, answer))
+ }
+
+ for _ in 0..count {
+ match subparser(input, message) {
+ Ok((rem, val)) => {
+ let n = match val.rrtype {
+ DNS_RECORD_TYPE_TXT => {
+ // For TXT records we need to run the parser
+ // multiple times. Set n high, to the maximum
+ // value based on a max txt side of 65535, but
+ // taking into considering that strings need
+ // to be quoted, so half that.
+ 32767
+ }
+ _ => {
+ // For all other types we only want to run the
+ // parser once, so set n to 1.
+ 1
+ }
+ };
+ let result: IResult<&'a [u8], Vec<DNSRData>> =
+ many_m_n(1, n, complete(|b| dns_parse_rdata(b, message, val.rrtype)))(val.data);
+ match result {
+ Ok((_, rdatas)) => {
+ for rdata in rdatas {
+ answers.push(DNSAnswerEntry {
+ name: val.name.clone(),
+ rrtype: val.rrtype,
+ rrclass: val.rrclass,
+ ttl: val.ttl,
+ data: rdata,
+ });
+ }
+ }
+ Err(e) => {
+ return Err(e);
+ }
+ }
+ input = rem;
+ }
+ Err(e) => {
+ return Err(e);
+ }
+ }
+ }
+
+ return Ok((input, answers));
+}
+
+pub fn dns_parse_response_body<'a>(
+ i: &'a [u8], message: &'a [u8], header: DNSHeader,
+) -> IResult<&'a [u8], DNSResponse> {
+ let (i, queries) = count(|b| dns_parse_query(b, message), header.questions as usize)(i)?;
+ let (i, answers) = dns_parse_answer(i, message, header.answer_rr as usize)?;
+ let (i, authorities) = dns_parse_answer(i, message, header.authority_rr as usize)?;
+ Ok((
+ i,
+ DNSResponse {
+ header,
+ queries,
+ answers,
+ authorities,
+ },
+ ))
+}
+
+/// Parse a single DNS query.
+///
+/// Arguments are suitable for using with call!:
+///
+/// call!(complete_dns_message_buffer)
+pub fn dns_parse_query<'a>(input: &'a [u8], message: &'a [u8]) -> IResult<&'a [u8], DNSQueryEntry> {
+ let i = input;
+ let (i, name) = dns_parse_name(i, message)?;
+ let (i, rrtype) = be_u16(i)?;
+ let (i, rrclass) = be_u16(i)?;
+ Ok((
+ i,
+ DNSQueryEntry {
+ name,
+ rrtype,
+ rrclass,
+ },
+ ))
+}
+
+fn dns_parse_rdata_a(input: &[u8]) -> IResult<&[u8], DNSRData> {
+ rest(input).map(|(input, data)| (input, DNSRData::A(data.to_vec())))
+}
+
+fn dns_parse_rdata_aaaa(input: &[u8]) -> IResult<&[u8], DNSRData> {
+ rest(input).map(|(input, data)| (input, 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_ns<'a>(input: &'a [u8], message: &'a [u8]) -> IResult<&'a [u8], DNSRData> {
+ dns_parse_name(input, message).map(|(input, name)| (input, DNSRData::NS(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> {
+ let i = input;
+ let (i, mname) = dns_parse_name(i, message)?;
+ let (i, rname) = dns_parse_name(i, message)?;
+ let (i, serial) = be_u32(i)?;
+ let (i, refresh) = be_u32(i)?;
+ let (i, retry) = be_u32(i)?;
+ let (i, expire) = be_u32(i)?;
+ let (i, minimum) = be_u32(i)?;
+ Ok((
+ i,
+ DNSRData::SOA(DNSRDataSOA {
+ mname,
+ rname,
+ serial,
+ refresh,
+ retry,
+ expire,
+ minimum,
+ }),
+ ))
+}
+
+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.
+ let (i, _) = be_u16(input)?;
+ let (i, name) = dns_parse_name(i, message)?;
+ Ok((i, DNSRData::MX(name)))
+}
+
+fn dns_parse_rdata_srv<'a>(input: &'a [u8], message: &'a [u8]) -> IResult<&'a [u8], DNSRData> {
+ let i = input;
+ let (i, priority) = be_u16(i)?;
+ let (i, weight) = be_u16(i)?;
+ let (i, port) = be_u16(i)?;
+ let (i, target) = dns_parse_name(i, message)?;
+ Ok((
+ i,
+ DNSRData::SRV(DNSRDataSRV {
+ priority,
+ weight,
+ port,
+ target,
+ }),
+ ))
+}
+
+fn dns_parse_rdata_txt(input: &[u8]) -> IResult<&[u8], DNSRData> {
+ let (i, txt) = length_data(be_u8)(input)?;
+ Ok((i, DNSRData::TXT(txt.to_vec())))
+}
+
+fn dns_parse_rdata_null(input: &[u8]) -> IResult<&[u8], DNSRData> {
+ rest(input).map(|(input, data)| (input, DNSRData::NULL(data.to_vec())))
+}
+
+fn dns_parse_rdata_sshfp(input: &[u8]) -> IResult<&[u8], DNSRData> {
+ let i = input;
+ let (i, algo) = be_u8(i)?;
+ let (i, fp_type) = be_u8(i)?;
+ let fingerprint = i;
+ Ok((
+ &[],
+ DNSRData::SSHFP(DNSRDataSSHFP {
+ algo,
+ fp_type,
+ fingerprint: fingerprint.to_vec(),
+ }),
+ ))
+}
+
+fn dns_parse_rdata_unknown(input: &[u8]) -> IResult<&[u8], DNSRData> {
+ 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,
+) -> IResult<&'a [u8], DNSRData> {
+ match rrtype {
+ 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_NS => dns_parse_rdata_ns(input, message),
+ DNS_RECORD_TYPE_TXT => dns_parse_rdata_txt(input),
+ DNS_RECORD_TYPE_NULL => dns_parse_rdata_null(input),
+ DNS_RECORD_TYPE_SSHFP => dns_parse_rdata_sshfp(input),
+ DNS_RECORD_TYPE_SRV => dns_parse_rdata_srv(input, message),
+ _ => dns_parse_rdata_unknown(input),
+ }
+}
+
+/// Parse a DNS request.
+pub fn dns_parse_request(input: &[u8]) -> IResult<&[u8], DNSRequest> {
+ let i = input;
+ let (i, header) = dns_parse_header(i)?;
+ dns_parse_request_body(i, input, header)
+}
+
+pub fn dns_parse_request_body<'a>(
+ input: &'a [u8], message: &'a [u8], header: DNSHeader,
+) -> IResult<&'a [u8], DNSRequest> {
+ let i = input;
+ let (i, queries) = count(|b| dns_parse_query(b, message), header.questions as usize)(i)?;
+ Ok((i, DNSRequest { header, queries }))
+}
+
+#[cfg(test)]
+mod tests {
+
+ use crate::dns::dns::{DNSAnswerEntry, DNSHeader};
+ use crate::dns::parser::*;
+
+ /// Parse a simple name with no pointers.
+ #[test]
+ fn test_dns_parse_name() {
+ let buf: &[u8] = &[
+ 0x09, 0x63, /* .......c */
+ 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2d, 0x63, 0x66, /* lient-cf */
+ 0x07, 0x64, 0x72, 0x6f, 0x70, 0x62, 0x6f, 0x78, /* .dropbox */
+ 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, /* .com.... */
+ ];
+ 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!(remainder, expected_remainder);
+ }
+
+ /// Test parsing a name with pointers.
+ #[test]
+ fn test_dns_parse_name_with_pointer() {
+ let buf: &[u8] = &[
+ 0xd8, 0xcb, 0x8a, 0xed, 0xa1, 0x46, 0x00, 0x15, /* 0 - .....F.. */
+ 0x17, 0x0d, 0x06, 0xf7, 0x08, 0x00, 0x45, 0x00, /* 8 - ......E. */
+ 0x00, 0x7b, 0x71, 0x6e, 0x00, 0x00, 0x39, 0x11, /* 16 - .{qn..9. */
+ 0xf4, 0xd9, 0x08, 0x08, 0x08, 0x08, 0x0a, 0x10, /* 24 - ........ */
+ 0x01, 0x0b, 0x00, 0x35, 0xe1, 0x8e, 0x00, 0x67, /* 32 - ...5...g */
+ 0x60, 0x00, 0xef, 0x08, 0x81, 0x80, 0x00, 0x01, /* 40 - `....... */
+ 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x03, 0x77, /* 48 - .......w */
+ 0x77, 0x77, 0x0c, 0x73, 0x75, 0x72, 0x69, 0x63, /* 56 - ww.suric */
+ 0x61, 0x74, 0x61, 0x2d, 0x69, 0x64, 0x73, 0x03, /* 64 - ata-ids. */
+ 0x6f, 0x72, 0x67, 0x00, 0x00, 0x01, 0x00, 0x01, /* 72 - org..... */
+ 0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, /* 80 - ........ */
+ 0x0e, 0x0f, 0x00, 0x02, 0xc0, 0x10, 0xc0, 0x10, /* 88 - ........ */
+ 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x2b, /* 96 - .......+ */
+ 0x00, 0x04, 0xc0, 0x00, 0x4e, 0x19, 0xc0, 0x10, /* 104 - ....N... */
+ 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x2b, /* 112 - .......+ */
+ 0x00, 0x04, 0xc0, 0x00, 0x4e, 0x18, 0x00, 0x00, /* 120 - ....N... */
+ 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* 128 - )....... */
+ 0x00, /* 136 - . */
+ ];
+
+ // The DNS payload starts at offset 42.
+ let message = &buf[42..];
+
+ // The name at offset 54 is the complete name.
+ let start1 = &buf[54..];
+ let res1 = dns_parse_name(start1, message);
+ assert_eq!(
+ res1,
+ Ok((&start1[22..], "www.suricata-ids.org".as_bytes().to_vec()))
+ );
+
+ // The second name starts at offset 80, but is just a pointer
+ // to the first.
+ let start2 = &buf[80..];
+ let res2 = dns_parse_name(start2, message);
+ assert_eq!(
+ res2,
+ Ok((&start2[2..], "www.suricata-ids.org".as_bytes().to_vec()))
+ );
+
+ // The third name starts at offset 94, but is a pointer to a
+ // portion of the first.
+ let start3 = &buf[94..];
+ let res3 = dns_parse_name(start3, message);
+ assert_eq!(
+ res3,
+ Ok((&start3[2..], "suricata-ids.org".as_bytes().to_vec()))
+ );
+
+ // The fourth name starts at offset 110, but is a pointer to a
+ // portion of the first.
+ let start4 = &buf[110..];
+ let res4 = dns_parse_name(start4, message);
+ assert_eq!(
+ res4,
+ Ok((&start4[2..], "suricata-ids.org".as_bytes().to_vec()))
+ );
+ }
+
+ #[test]
+ fn test_dns_parse_name_double_pointer() {
+ let buf: &[u8] = &[
+ 0xd8, 0xcb, 0x8a, 0xed, 0xa1, 0x46, 0x00, 0x15, /* 0: .....F.. */
+ 0x17, 0x0d, 0x06, 0xf7, 0x08, 0x00, 0x45, 0x00, /* 8: ......E. */
+ 0x00, 0x66, 0x5e, 0x20, 0x40, 0x00, 0x40, 0x11, /* 16: .f^ @.@. */
+ 0xc6, 0x3b, 0x0a, 0x10, 0x01, 0x01, 0x0a, 0x10, /* 24: .;...... */
+ 0x01, 0x0b, 0x00, 0x35, 0xc2, 0x21, 0x00, 0x52, /* 32: ...5.!.R */
+ 0x35, 0xc5, 0x0d, 0x4f, 0x81, 0x80, 0x00, 0x01, /* 40: 5..O.... */
+ 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x05, 0x62, /* 48: .......b */
+ 0x6c, 0x6f, 0x63, 0x6b, 0x07, 0x64, 0x72, 0x6f, /* 56: lock.dro */
+ 0x70, 0x62, 0x6f, 0x78, 0x03, 0x63, 0x6f, 0x6d, /* 64: pbox.com */
+ 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, /* 72: ........ */
+ 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00, /* 80: ........ */
+ 0x0b, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x02, /* 88: ..block. */
+ 0x67, 0x31, 0xc0, 0x12, 0xc0, 0x2f, 0x00, 0x01, /* 96: g1.../.. */
+ 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, /* 104: ........ */
+ 0x2d, 0x3a, 0x46, 0x21, /* 112: -:F! */
+ ];
+
+ // The start of the DNS message in the above packet.
+ let message: &[u8] = &buf[42..];
+
+ // The start of the name we want to parse, 0xc0 0x2f, a
+ // pointer to offset 47 in the message (or 89 in the full
+ // packet).
+ let start: &[u8] = &buf[100..];
+
+ let res = dns_parse_name(start, message);
+ assert_eq!(
+ res,
+ Ok((&start[2..], "block.g1.dropbox.com".as_bytes().to_vec()))
+ );
+ }
+
+ #[test]
+ fn test_dns_parse_request() {
+ // DNS request from dig-a-www.suricata-ids.org.pcap.
+ let pkt: &[u8] = &[
+ 0x8d, 0x32, 0x01, 0x20, 0x00, 0x01, /* ...2. .. */
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x77, /* .......w */
+ 0x77, 0x77, 0x0c, 0x73, 0x75, 0x72, 0x69, 0x63, /* ww.suric */
+ 0x61, 0x74, 0x61, 0x2d, 0x69, 0x64, 0x73, 0x03, /* ata-ids. */
+ 0x6f, 0x72, 0x67, 0x00, 0x00, 0x01, 0x00, 0x01, /* org..... */
+ 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, 0x00, /* ..)..... */
+ 0x00, 0x00, 0x00, /* ... */
+ ];
+
+ let res = dns_parse_request(pkt);
+ match res {
+ Ok((rem, request)) => {
+ // For now we have some remainder data as there is an
+ // additional record type we don't parse yet.
+ assert!(!rem.is_empty());
+
+ assert_eq!(
+ request.header,
+ DNSHeader {
+ tx_id: 0x8d32,
+ flags: 0x0120,
+ questions: 1,
+ answer_rr: 0,
+ authority_rr: 0,
+ additional_rr: 1,
+ }
+ );
+
+ 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.rrtype, 1);
+ assert_eq!(query.rrclass, 1);
+ }
+ _ => {
+ assert!(false);
+ }
+ }
+ }
+
+ /// Parse a DNS response.
+ fn dns_parse_response(message: &[u8]) -> IResult<&[u8], DNSResponse> {
+ let i = message;
+ let (i, header) = dns_parse_header(i)?;
+ dns_parse_response_body(i, message, header)
+ }
+
+ #[test]
+ fn test_dns_parse_response() {
+ // DNS response from dig-a-www.suricata-ids.org.pcap.
+ let pkt: &[u8] = &[
+ 0x8d, 0x32, 0x81, 0xa0, 0x00, 0x01, /* ...2.... */
+ 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, /* .......w */
+ 0x77, 0x77, 0x0c, 0x73, 0x75, 0x72, 0x69, 0x63, /* ww.suric */
+ 0x61, 0x74, 0x61, 0x2d, 0x69, 0x64, 0x73, 0x03, /* ata-ids. */
+ 0x6f, 0x72, 0x67, 0x00, 0x00, 0x01, 0x00, 0x01, /* org..... */
+ 0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, /* ........ */
+ 0x0d, 0xd8, 0x00, 0x12, 0x0c, 0x73, 0x75, 0x72, /* .....sur */
+ 0x69, 0x63, 0x61, 0x74, 0x61, 0x2d, 0x69, 0x64, /* icata-id */
+ 0x73, 0x03, 0x6f, 0x72, 0x67, 0x00, 0xc0, 0x32, /* s.org..2 */
+ 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xf4, /* ........ */
+ 0x00, 0x04, 0xc0, 0x00, 0x4e, 0x18, 0xc0, 0x32, /* ....N..2 */
+ 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xf4, /* ........ */
+ 0x00, 0x04, 0xc0, 0x00, 0x4e, 0x19, /* ....N. */
+ ];
+
+ let res = dns_parse_response(pkt);
+ match res {
+ Ok((rem, response)) => {
+ // The response should be full parsed.
+ assert_eq!(rem.len(), 0);
+
+ assert_eq!(
+ response.header,
+ DNSHeader {
+ tx_id: 0x8d32,
+ flags: 0x81a0,
+ questions: 1,
+ answer_rr: 3,
+ authority_rr: 0,
+ additional_rr: 0,
+ }
+ );
+
+ 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.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())
+ );
+
+ let answer2 = &response.answers[1];
+ assert_eq!(
+ answer2,
+ &DNSAnswerEntry {
+ name: "suricata-ids.org".as_bytes().to_vec(),
+ rrtype: 1,
+ rrclass: 1,
+ ttl: 244,
+ data: DNSRData::A([192, 0, 78, 24].to_vec()),
+ }
+ );
+
+ let answer3 = &response.answers[2];
+ assert_eq!(
+ answer3,
+ &DNSAnswerEntry {
+ name: "suricata-ids.org".as_bytes().to_vec(),
+ rrtype: 1,
+ rrclass: 1,
+ ttl: 244,
+ data: DNSRData::A([192, 0, 78, 25].to_vec()),
+ }
+ )
+ }
+ _ => {
+ assert!(false);
+ }
+ }
+ }
+
+ #[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.is_empty());
+
+ 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_response_null() {
+ // DNS response with a NULL record from
+ // https://redmine.openinfosecfoundation.org/attachments/2062
+
+ let pkt: &[u8] = &[
+ 0x12, 0xb0, 0x84, 0x00, 0x00, 0x01, 0x00, 0x01, /* ........ */
+ 0x00, 0x00, 0x00, 0x00, 0x0b, 0x76, 0x61, 0x61, /* .....vaa */
+ 0x61, 0x61, 0x6b, 0x61, 0x72, 0x64, 0x6c, 0x69, /* aakardli */
+ 0x06, 0x70, 0x69, 0x72, 0x61, 0x74, 0x65, 0x03, /* .pirate. */
+ 0x73, 0x65, 0x61, 0x00, 0x00, 0x0a, 0x00, 0x01, /* sea..... */
+ 0xc0, 0x0c, 0x00, 0x0a, 0x00, 0x01, 0x00, 0x00, /* ........ */
+ 0x00, 0x00, 0x00, 0x09, 0x56, 0x41, 0x43, 0x4b, /* ....VACK */
+ 0x44, 0x03, 0xc5, 0xe9, 0x01, /* D.... */
+ ];
+
+ let res = dns_parse_response(pkt);
+ match res {
+ Ok((rem, response)) => {
+ // The response should be fully parsed.
+ assert_eq!(rem.len(), 0);
+
+ assert_eq!(
+ response.header,
+ DNSHeader {
+ tx_id: 0x12b0,
+ flags: 0x8400,
+ questions: 1,
+ answer_rr: 1,
+ authority_rr: 0,
+ additional_rr: 0,
+ }
+ );
+
+ 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.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.rrtype, DNS_RECORD_TYPE_NULL);
+ assert_eq!(answer.rrclass, 1);
+ assert_eq!(answer.ttl, 0);
+ assert_eq!(
+ answer.data,
+ DNSRData::NULL(vec![
+ 0x56, 0x41, 0x43, 0x4b, /* VACK */
+ 0x44, 0x03, 0xc5, 0xe9, 0x01, /* D.... */
+ ])
+ );
+ }
+ _ => {
+ assert!(false);
+ }
+ }
+ }
+
+ #[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);
+ }
+ }
+ }
+
+ #[test]
+ fn test_dns_parse_rdata_srv() {
+ /* ; <<>> DiG 9.11.5-P4-5.1+deb10u2-Debian <<>> _sip._udp.sip.voice.google.com SRV
+ ;; global options: +cmd
+ ;; Got answer:
+ ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 1524
+ ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 3
+
+ [...]
+
+ ;; ANSWER SECTION:
+ _sip._udp.sip.voice.google.com. 300 IN SRV 10 1 5060 sip-anycast-1.voice.google.com.
+ _sip._udp.sip.voice.google.com. 300 IN SRV 20 1 5060 sip-anycast-2.voice.google.com.
+
+ [...]
+
+ ;; Query time: 72 msec
+ ;; MSG SIZE rcvd: 191 */
+
+ let pkt: &[u8] = &[
+ 0xeb, 0x56, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x04, 0x5f,
+ 0x73, 0x69, 0x70, 0x04, 0x5f, 0x75, 0x64, 0x70, 0x03, 0x73, 0x69, 0x70, 0x05, 0x76,
+ 0x6f, 0x69, 0x63, 0x65, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f,
+ 0x6d, 0x00, 0x00, 0x21, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00,
+ 0x01, 0x13, 0x00, 0x26, 0x00, 0x14, 0x00, 0x01, 0x13, 0xc4, 0x0d, 0x73, 0x69, 0x70,
+ 0x2d, 0x61, 0x6e, 0x79, 0x63, 0x61, 0x73, 0x74, 0x2d, 0x32, 0x05, 0x76, 0x6f, 0x69,
+ 0x63, 0x65, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00,
+ 0xc0, 0x0c, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x01, 0x13, 0x00, 0x26, 0x00, 0x0a,
+ 0x00, 0x01, 0x13, 0xc4, 0x0d, 0x73, 0x69, 0x70, 0x2d, 0x61, 0x6e, 0x79, 0x63, 0x61,
+ 0x73, 0x74, 0x2d, 0x31, 0x05, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x06, 0x67, 0x6f, 0x6f,
+ 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00,
+ ];
+
+ let res = dns_parse_response(pkt);
+ match res {
+ Ok((rem, response)) => {
+ // The data should be fully parsed.
+ assert_eq!(rem.len(), 0);
+
+ assert_eq!(response.answers.len(), 2);
+
+ let answer1 = &response.answers[0];
+ match &answer1.data {
+ DNSRData::SRV(srv) => {
+ assert_eq!(srv.priority, 20);
+ assert_eq!(srv.weight, 1);
+ assert_eq!(srv.port, 5060);
+ assert_eq!(
+ srv.target,
+ "sip-anycast-2.voice.google.com".as_bytes().to_vec()
+ );
+ }
+ _ => {
+ assert!(false);
+ }
+ }
+ let answer2 = &response.answers[1];
+ match &answer2.data {
+ DNSRData::SRV(srv) => {
+ assert_eq!(srv.priority, 10);
+ assert_eq!(srv.weight, 1);
+ assert_eq!(srv.port, 5060);
+ assert_eq!(
+ srv.target,
+ "sip-anycast-1.voice.google.com".as_bytes().to_vec()
+ );
+ }
+ _ => {
+ assert!(false);
+ }
+ }
+ }
+ _ => {
+ assert!(false);
+ }
+ }
+ }
+}