diff --git a/etc/schema.json b/etc/schema.json index bb6a8238c3..356e4fe982 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -6368,6 +6368,22 @@ }, "ttl": { "type": "integer" + }, + "opt": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "type": "string" + } + }, + "additionalProperties": false + } } }, "additionalProperties": false diff --git a/rust/src/dns/dns.rs b/rust/src/dns/dns.rs index 0b6f803930..ea12b43ac3 100644 --- a/rust/src/dns/dns.rs +++ b/rust/src/dns/dns.rs @@ -149,6 +149,14 @@ pub struct DNSQueryEntry { pub rrclass: u16, } +#[derive(Debug, PartialEq, Eq)] +pub struct DNSRDataOPT { + /// Option Code + pub code: u16, + /// Option Data + pub data: Vec, +} + #[derive(Debug, PartialEq, Eq)] pub struct DNSRDataSOA { /// Primary name server for this zone @@ -207,6 +215,7 @@ pub enum DNSRData { SOA(DNSRDataSOA), SRV(DNSRDataSRV), SSHFP(DNSRDataSSHFP), + OPT(Vec), // RData for remaining types is sometimes ignored Unknown(Vec), } diff --git a/rust/src/dns/log.rs b/rust/src/dns/log.rs index c1043f8925..e4bfd91976 100644 --- a/rust/src/dns/log.rs +++ b/rust/src/dns/log.rs @@ -394,6 +394,17 @@ pub fn dns_print_addr(addr: &[u8]) -> std::string::String { } } +/// Log OPT section fields +fn dns_log_opt(opt: &DNSRDataOPT) -> Result { + let mut js = JsonBuilder::try_new_object()?; + + js.set_uint("code", opt.code as u64)?; + js.set_hex("data", &opt.data)?; + + js.close()?; + Ok(js) +} + /// Log SOA section fields. fn dns_log_soa(soa: &DNSRDataSOA) -> Result { let mut js = JsonBuilder::try_new_object()?; @@ -468,6 +479,13 @@ fn dns_log_json_answer_detail(answer: &DNSAnswerEntry) -> Result { jsa.set_object("srv", &dns_log_srv(srv)?)?; } + DNSRData::OPT(opt) => { + jsa.open_array("opt")?; + for val in opt { + jsa.append_object(&dns_log_opt(val)?)?; + } + jsa.close()?; + } _ => {} } @@ -609,7 +627,7 @@ fn dns_log_json_answer( if !response.additionals.is_empty() { let mut is_js_open = false; for add in &response.additionals { - if let DNSRData::Unknown(rdata) = &add.data { + if let DNSRData::OPT(rdata) = &add.data { if rdata.is_empty() { continue; } diff --git a/rust/src/dns/lua.rs b/rust/src/dns/lua.rs index a7847ecaf3..436a75115a 100644 --- a/rust/src/dns/lua.rs +++ b/rust/src/dns/lua.rs @@ -180,6 +180,16 @@ pub extern "C" fn SCDnsLuaGetAnswerTable(clua: &mut CLuaState, tx: &mut DNSTrans lua.pushstring(&String::from_utf8_lossy(&srv.target)); lua.settable(-3); } + DNSRData::OPT(ref opt) => { + if !opt.is_empty() { + lua.pushstring("addr"); + for option in opt.iter() { + lua.pushstring(&String::from_utf8_lossy(&option.code.to_be_bytes())); + lua.pushstring(&String::from_utf8_lossy(&option.data)); + } + lua.settable(-3); + } + } } lua.settable(-3); } diff --git a/rust/src/dns/parser.rs b/rust/src/dns/parser.rs index 238b83a102..31332c1659 100644 --- a/rust/src/dns/parser.rs +++ b/rust/src/dns/parser.rs @@ -149,7 +149,7 @@ fn dns_parse_answer<'a>( rrtype: val.rrtype, rrclass: val.rrclass, ttl: val.ttl, - data: DNSRData::Unknown(Vec::new()), + data: DNSRData::OPT(Vec::new()), }); input = rem; continue; @@ -295,6 +295,22 @@ fn dns_parse_rdata_sshfp(input: &[u8]) -> IResult<&[u8], DNSRData> { )) } +fn dns_parse_rdata_opt(input: &[u8]) -> IResult<&[u8], DNSRData> { + let mut dns_rdata_opt_vec = Vec::new(); + let mut i = input; + + while !i.is_empty() { + let (j, code) = be_u16(i)?; + let (j, data) = length_data(be_u16)(j)?; + i = j; + dns_rdata_opt_vec.push(DNSRDataOPT { + code, + data: data.to_vec(), + }); + } + Ok((i, DNSRData::OPT(dns_rdata_opt_vec))) +} + fn dns_parse_rdata_unknown(input: &[u8]) -> IResult<&[u8], DNSRData> { rest(input).map(|(input, data)| (input, DNSRData::Unknown(data.to_vec()))) } @@ -314,6 +330,7 @@ fn dns_parse_rdata<'a>( 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_RECORD_TYPE_OPT => dns_parse_rdata_opt(input), _ => dns_parse_rdata_unknown(input), } } @@ -493,7 +510,7 @@ mod tests { let (body, header) = dns_parse_header(pkt).unwrap(); let res = dns_parse_body(body, pkt, header); let (rem, request) = res.unwrap(); - // The response should be fully parsed. + // The request should be fully parsed. assert!(rem.is_empty()); assert_eq!( @@ -517,15 +534,84 @@ mod tests { // verify additional section assert_eq!(request.additionals.len(), 1); + let additional = &request.additionals[0]; assert_eq!( additional, &DNSAnswerEntry { name: vec![], rrtype: DNS_RECORD_TYPE_OPT, - rrclass: 0x1000, // for OPT this is UDP payload size + rrclass: 0x1000, // for OPT this is UDP payload size + ttl: 0, // for OPT this is extended RCODE and flags + data: DNSRData::OPT(vec![]), // empty rdata + } + ); + } + + #[test] + fn test_dns_parse_request_multi_opt() { + 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, 0x10, /* total rdata len: 16 */ + /* Option Cookie */ + 0x00, 0x0a, /* Code: 10 */ + 0x00, 0x08, /* Option len: 8 */ + 0x7f, 0x86, 0xcf, 0x8b, 0x81, 0xf6, 0xf9, 0x55, /* Option NSID */ + 0x00, 0x03, /* Code: 3 */ + 0x00, 0x00, /* option len: 0 */ + ]; + + let (body, header) = dns_parse_header(pkt).unwrap(); + let res = dns_parse_body(body, pkt, header); + let (rem, request) = res.unwrap(); + + 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); + + // verify additional section + assert_eq!(request.additionals.len(), 1); + + let additional = &request.additionals[0]; + assert_eq!( + additional, + &DNSAnswerEntry { + name: vec![], + 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 - data: DNSRData::Unknown(vec![]), // empty rdata + // verify two options + data: DNSRData::OPT(vec![ + DNSRDataOPT { + code: 0x000a, + data: vec![0x7f, 0x86, 0xcf, 0x8b, 0x81, 0xf6, 0xf9, 0x55] + }, + DNSRDataOPT { + code: 0x0003, + data: vec![] + }, + ]) } ); } @@ -677,9 +763,9 @@ mod tests { &DNSAnswerEntry { name: vec![], rrtype: DNS_RECORD_TYPE_OPT, - rrclass: 0x0200, // for OPT this is UDP payload size - ttl: 0, // for OPT this is extended RCODE and flags - data: DNSRData::Unknown(vec![]), // no rdata + rrclass: 0x0200, // for OPT this is UDP payload size + ttl: 0, // for OPT this is extended RCODE and flags + data: DNSRData::OPT(vec![]), // no rdata } ); }