diff --git a/doc/userguide/rules/index.rst b/doc/userguide/rules/index.rst index a1192741c9..b34c5519c5 100644 --- a/doc/userguide/rules/index.rst +++ b/doc/userguide/rules/index.rst @@ -15,6 +15,7 @@ Suricata Rules http-keywords file-keywords dns-keywords + mdns-keywords tls-keywords ssh-keywords ja-keywords diff --git a/doc/userguide/rules/mdns-keywords.rst b/doc/userguide/rules/mdns-keywords.rst new file mode 100644 index 0000000000..845b3d857d --- /dev/null +++ b/doc/userguide/rules/mdns-keywords.rst @@ -0,0 +1,93 @@ +mDNS Keywords +============= + +Suricata supports sticky buffers for efficiently matching on specific +fields in mDNS (Multicast DNS) messages. + +Note that sticky buffers are expected to be followed by one or more +:doc:`payload-keywords`. + +mdns.queries.rrname +------------------- + +``mdns.queries.rrname`` is a sticky buffer that is used to look at the +name field in mDNS query resource records. + +The buffer being matched on contains the complete re-assembled +resource name, for example "host.local". + +``mdns.queries.rrname`` supports :doc:`multi-buffer-matching`. + +Example:: + + alert udp any any -> any 5353 (msg:"mDNS query for .local domain"; \ + mdns.queries.rrname; content:".local"; sid:1;) + +mdns.answers.rrname +------------------- + +``mdns.answers.rrname`` is a sticky buffer that is used to look at the +name field in mDNS answer resource records. + +The buffer being matched on contains the complete re-assembled +resource name, for example "printer.local". + +``mdns.answers.rrname`` supports :doc:`multi-buffer-matching`. + +Example:: + + alert udp any 5353 -> any any (msg:"mDNS answer for printer.local"; \ + mdns.answers.rrname; content:"printer.local"; sid:2;) + +mdns.authorities.rrname +----------------------- + +``mdns.authorities.rrname`` is a sticky buffer that is used to look at the +rrname field in mDNS authority resource records. + +The buffer being matched on contains the complete re-assembled +resource name, for example "device.local". + +``mdns.authorities.rrname`` supports :doc:`multi-buffer-matching`. + +Example:: + + alert udp any 5353 -> any any (msg:"mDNS authority record check"; \ + mdns.authorities.rrname; content:"auth.local"; sid:3;) + +mdns.additionals.rrname +----------------------- + +``mdns.additionals.rrname`` is a sticky buffer that is used to look at +the rrname field in mDNS additional resource records. + +The buffer being matched on contains the complete re-assembled +resource name, for example "service.local". + +``mdns.additionals.rrname`` supports :doc:`multi-buffer-matching`. + +Example:: + + alert udp any any -> any 5353 (msg:"mDNS additional record check"; \ + mdns.additionals.rrname; content:"_companion-link._tcp.local"; nocase; sid:4;) + +mdns.response.rrname +-------------------- + +``mdns.response.rrname`` is a sticky buffer that is used to inspect +all the rrname fields in a response, in the queries, answers, +additionals and authorities. Additionally it will also inspect rdata +fields that have the same format as an rrname (hostname). + +``rdata`` types that will be inspected are: + +* CNAME +* PTR +* MX +* NS +* SOA + +Example:: + + alert udp any 5353 -> any any (msg:"mDNS answer data match"; \ + mdns.response.rrname; content:"Apple TV"; sid:5;) diff --git a/etc/schema.json b/etc/schema.json index f07c2ebbd0..2205fbc92a 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -2806,6 +2806,117 @@ "log_level": { "type": "string" }, + "mdns": { + "description": "mDNS requests and responses", + "type": "object", + "additionalProperties": false, + "properties": { + "additionals": { + "description": "mDNS additional records", + "type": "array", + "minItems": 1 + }, + "answers": { + "description": "mDNS answer records", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "ptr": { + "type": "string" + }, + "rrname": { + "type": "string" + }, + "txt": { + "type": "array", + "minItems": 1 + } + } + } + }, + "authorities": { + "description": "mDNS authority records", + "type": "array", + "minItems": 1 + }, + "flags": { + "description": "mDNS message flags", + "type": "array", + "items": { + "oneOf": [ + { + "const": "aa", + "title": "Authoritative Answer" + }, + { + "const": "tc", + "title": "Truncated" + }, + { + "const": "rd", + "title": "Recursion Desired" + }, + { + "const": "ra", + "title": "Recursion Available" + }, + { + "const": "z", + "title": "Z (reserved)" + }, + { + "const": "ad", + "title": "Authentic Data" + }, + { + "const": "cd", + "title": "Checking Disabled" + } + ] + } + }, + "id": { + "description": "mDNS transaction ID", + "type": "integer" + }, + "opcode": { + "description": "mDNS opcode value", + "type": "integer" + }, + "queries": { + "description": "mDNS query records", + "type": "array", + "additionalProperties": false, + "minItems": 1, + "items": { + "type": "object", + "properties": { + "rrname": { + "type": "string" + }, + "rrtype": { + "type": "string" + } + } + } + }, + "rcode": { + "description": "mDNS reply (error) code", + "type": "integer" + }, + "type": { + "description": "Type of message, either a request or response", + "type": "string", + "enum": [ + "request", + "response" + ] + } + } + }, "metadata": { "type": "object", "additionalProperties": false, @@ -4801,6 +4912,10 @@ "description": "Errors encountered parsing LDAP/UDP protocol", "$ref": "#/$defs/stats_applayer_error" }, + "mdns": { + "description": "Errors encountered parsing mDNS", + "$ref": "#/$defs/stats_applayer_error" + }, "modbus": { "description": "Errors encountered parsing Modbus protocol", "$ref": "#/$defs/stats_applayer_error" @@ -4980,6 +5095,10 @@ "type": "integer", "description": "Number of flows LDAP/UDP protocol" }, + "mdns": { + "description": "Number of flows for mDNS", + "type": "integer" + }, "modbus": { "type": "integer", "description": "Number of flows for Modbus protocol" @@ -5147,6 +5266,10 @@ "type": "integer", "description": "Number of transactions for LDAP/UDP protocol" }, + "mdns": { + "description": "Number of transactions for mDNS", + "type": "integer" + }, "modbus": { "type": "integer", "description": "Number of transactions for Modbus protocol" diff --git a/rust/src/dns/detect.rs b/rust/src/dns/detect.rs index 6495931704..db373e9036 100644 --- a/rust/src/dns/detect.rs +++ b/rust/src/dns/detect.rs @@ -264,7 +264,7 @@ unsafe extern "C" fn dns_detect_answer_name_setup( } /// Get the DNS response answer name and index i. -unsafe extern "C" fn dns_tx_get_answer_name( +pub(crate) unsafe extern "C" fn dns_tx_get_answer_name( _de: *mut DetectEngineThreadCtx, tx: *const c_void, flags: u8, i: u32, buf: *mut *const u8, len: *mut u32, ) -> bool { @@ -302,7 +302,7 @@ unsafe extern "C" fn dns_detect_query_name_setup( } /// Get the DNS response answer name and index i. -unsafe extern "C" fn dns_tx_get_query_name( +pub(crate) unsafe extern "C" fn dns_tx_get_query_name( _de: *mut DetectEngineThreadCtx, tx: *const c_void, flags: u8, i: u32, buf: *mut *const u8, len: *mut u32, ) -> bool { diff --git a/rust/src/dns/dns.rs b/rust/src/dns/dns.rs index 579ae7a4e1..3420a99383 100644 --- a/rust/src/dns/dns.rs +++ b/rust/src/dns/dns.rs @@ -127,7 +127,7 @@ pub enum DNSRcode { pub(super) static mut ALPROTO_DNS: AppProto = ALPROTO_UNKNOWN; #[derive(AppLayerFrameType)] -enum DnsFrameType { +pub(crate) enum DnsFrameType { /// DNS PDU frame. For UDP DNS this is the complete UDP payload, for TCP /// this is the DNS payload not including the leading length field allowing /// this frame to be used for UDP and TCP DNS. @@ -290,7 +290,7 @@ impl Transaction for DNSTransaction { } impl DNSTransaction { - fn new(direction: Direction) -> Self { + pub(crate) fn new(direction: Direction) -> Self { Self { tx_data: AppLayerTxData::for_direction(direction), ..Default::default() @@ -355,8 +355,24 @@ impl ConfigTracker { } } -#[derive(Default)] +pub(crate) enum DnsVariant { + Dns, + MulticastDns, +} + +impl DnsVariant { + pub fn is_dns(&self) -> bool { + matches!(self, DnsVariant::Dns) + } + + pub fn is_mdns(&self) -> bool { + matches!(self, DnsVariant::MulticastDns) + } +} + +//#[derive(Default)] pub struct DNSState { + variant: DnsVariant, state_data: AppLayerStateData, // Internal transaction ID. @@ -397,7 +413,7 @@ pub(crate) enum DNSParseError { OtherError, } -pub(crate) fn dns_parse_request(input: &[u8]) -> Result { +pub(crate) fn dns_parse_request(input: &[u8], variant: &DnsVariant) -> Result { let (body, header) = if let Some((body, header)) = dns_validate_header(input) { (body, header) } else { @@ -406,7 +422,7 @@ pub(crate) fn dns_parse_request(input: &[u8]) -> Result { - if request.header.flags & 0x8000 != 0 { + if variant.is_dns() && request.header.flags & 0x8000 != 0 { SCLogDebug!("DNS message is not a request"); return Err(DNSParseError::NotRequest); } @@ -421,7 +437,12 @@ pub(crate) fn dns_parse_request(input: &[u8]) -> Result Result Self { - Default::default() + Self { + variant: DnsVariant::Dns, + state_data: AppLayerStateData::default(), + tx_id: 0, + transactions: VecDeque::default(), + config: None, + gap: false, + } + } + + pub(crate) fn new_variant(variant: DnsVariant) -> Self { + Self { + variant, + state_data: AppLayerStateData::default(), + tx_id: 0, + transactions: VecDeque::default(), + config: None, + gap: false, + } } fn free_tx(&mut self, tx_id: u64) { @@ -563,7 +602,7 @@ impl DNSState { fn parse_request( &mut self, input: &[u8], is_tcp: bool, frame: Option, flow: *const Flow, ) -> bool { - match dns_parse_request(input) { + match dns_parse_request(input, &self.variant) { Ok(mut tx) => { self.tx_id += 1; tx.id = self.tx_id; @@ -593,7 +632,7 @@ impl DNSState { } } - fn parse_request_udp(&mut self, flow: *const Flow, stream_slice: StreamSlice) -> bool { + pub(crate) fn parse_request_udp(&mut self, flow: *const Flow, stream_slice: StreamSlice) -> bool { let input = stream_slice.as_slice(); let frame = Frame::new( flow, @@ -797,7 +836,7 @@ impl DNSState { const DNS_HEADER_SIZE: usize = 12; -fn probe_header_validity(header: &DNSHeader, rlen: usize) -> (bool, bool, bool) { +pub(crate) fn probe_header_validity(header: &DNSHeader, rlen: usize) -> (bool, bool, bool) { let nb_records = header.additional_rr as usize + header.answer_rr as usize + header.authority_rr as usize @@ -869,7 +908,7 @@ fn probe_tcp(input: &[u8]) -> (bool, bool, bool) { } /// Returns *mut DNSState -extern "C" fn state_new( +pub(crate) extern "C" fn state_new( _orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto, ) -> *mut std::os::raw::c_void { let state = DNSState::new(); @@ -879,18 +918,18 @@ extern "C" fn state_new( /// Params: /// - state: *mut DNSState as void pointer -extern "C" fn state_free(state: *mut std::os::raw::c_void) { +pub(crate) extern "C" fn state_free(state: *mut std::os::raw::c_void) { // Just unbox... std::mem::drop(unsafe { Box::from_raw(state as *mut DNSState) }); } -unsafe extern "C" fn state_tx_free(state: *mut std::os::raw::c_void, tx_id: u64) { +pub(crate) unsafe extern "C" fn state_tx_free(state: *mut std::os::raw::c_void, tx_id: u64) { let state = cast_pointer!(state, DNSState); state.free_tx(tx_id); } /// C binding parse a DNS request. Returns 1 on success, -1 on failure. -unsafe extern "C" fn parse_request( +pub(crate) unsafe extern "C" fn parse_request( flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, stream_slice: StreamSlice, _data: *const std::os::raw::c_void, ) -> AppLayerResult { @@ -935,7 +974,7 @@ unsafe extern "C" fn parse_response_tcp( AppLayerResult::ok() } -extern "C" fn tx_get_alstate_progress( +pub(crate) extern "C" fn tx_get_alstate_progress( _tx: *mut std::os::raw::c_void, _direction: u8, ) -> std::os::raw::c_int { // This is a stateless parser, just the existence of a transaction @@ -944,13 +983,13 @@ extern "C" fn tx_get_alstate_progress( return 1; } -unsafe extern "C" fn state_get_tx_count(state: *mut std::os::raw::c_void) -> u64 { +pub(crate) unsafe extern "C" fn state_get_tx_count(state: *mut std::os::raw::c_void) -> u64 { let state = cast_pointer!(state, DNSState); SCLogDebug!("state_get_tx_count: returning {}", state.tx_id); return state.tx_id; } -unsafe extern "C" fn state_get_tx( +pub(crate) unsafe extern "C" fn state_get_tx( state: *mut std::os::raw::c_void, tx_id: u64, ) -> *mut std::os::raw::c_void { let state = cast_pointer!(state, DNSState); @@ -974,12 +1013,17 @@ pub extern "C" fn SCDnsTxIsResponse(tx: &mut DNSTransaction) -> bool { tx.response.is_some() } -unsafe extern "C" fn state_get_tx_data(tx: *mut std::os::raw::c_void) -> *mut AppLayerTxData { +pub(crate) unsafe extern "C" fn state_get_tx_data(tx: *mut std::os::raw::c_void) -> *mut AppLayerTxData { let tx = cast_pointer!(tx, DNSTransaction); return &mut tx.tx_data; } -export_state_data_get!(dns_get_state_data, DNSState); +pub(crate) unsafe extern "C" fn dns_get_state_data( + state: *mut std::os::raw::c_void, +) -> *mut AppLayerStateData { + let state = cast_pointer!(state, DNSState); + return &mut state.state_data; +} /// Get the DNS query name at index i. #[no_mangle] @@ -1161,7 +1205,7 @@ pub extern "C" fn SCDnsTxGetResponseFlags(tx: &mut DNSTransaction) -> u16 { return tx.rcode(); } -unsafe extern "C" fn probe_udp( +pub(crate) unsafe extern "C" fn probe_udp( _flow: *const Flow, _dir: u8, input: *const u8, len: u32, rdir: *mut u8, ) -> AppProto { if input.is_null() || len < std::mem::size_of::() as u32 { diff --git a/rust/src/dns/log.rs b/rust/src/dns/log.rs index f3e1459694..910572127f 100644 --- a/rust/src/dns/log.rs +++ b/rust/src/dns/log.rs @@ -323,7 +323,7 @@ pub fn dns_print_addr(addr: &[u8]) -> std::string::String { } /// Log OPT section fields -fn dns_log_opt(opt: &DNSRDataOPT) -> Result { +pub(crate) fn dns_log_opt(opt: &DNSRDataOPT) -> Result { let mut js = JsonBuilder::try_new_object()?; js.set_uint("code", opt.code as u64)?; @@ -334,7 +334,7 @@ fn dns_log_opt(opt: &DNSRDataOPT) -> Result { } /// Log SOA section fields. -fn dns_log_soa(soa: &DNSRDataSOA) -> Result { +pub(crate) fn dns_log_soa(soa: &DNSRDataSOA) -> Result { let mut js = JsonBuilder::try_new_object()?; js.set_string_from_bytes("mname", &soa.mname.value)?; @@ -356,7 +356,7 @@ fn dns_log_soa(soa: &DNSRDataSOA) -> Result { } /// Log SSHFP section fields. -fn dns_log_sshfp(sshfp: &DNSRDataSSHFP) -> Result { +pub(crate) fn dns_log_sshfp(sshfp: &DNSRDataSSHFP) -> Result { let mut js = JsonBuilder::try_new_object()?; let mut hex = Vec::new(); @@ -373,7 +373,7 @@ fn dns_log_sshfp(sshfp: &DNSRDataSSHFP) -> Result { } /// Log SRV section fields. -fn dns_log_srv(srv: &DNSRDataSRV) -> Result { +pub(crate) fn dns_log_srv(srv: &DNSRDataSRV) -> Result { let mut js = JsonBuilder::try_new_object()?; js.set_uint("priority", srv.priority as u64)?; diff --git a/rust/src/http2/http2.rs b/rust/src/http2/http2.rs index e11525613a..e844021c76 100644 --- a/rust/src/http2/http2.rs +++ b/rust/src/http2/http2.rs @@ -24,6 +24,7 @@ use crate::applayer::{self, *}; use crate::conf::conf_get; use crate::core::*; use crate::direction::Direction; +use crate::dns::dns::DnsVariant; use crate::filecontainer::*; use crate::filetracker::*; use crate::flow::Flow; @@ -479,7 +480,7 @@ impl HTTP2Transaction { AppLayerForceProtocolChange(flow, ALPROTO_DOH2); } } - } else if let Ok(mut dtx) = dns_parse_request(&doh.data_buf[dir.index()]) { + } else if let Ok(mut dtx) = dns_parse_request(&doh.data_buf[dir.index()], &DnsVariant::Dns) { dtx.id = 1; doh.dns_request_tx = Some(dtx); unsafe { @@ -1206,7 +1207,7 @@ impl HTTP2State { frame.set_tx(flow, tx.tx_id); } if let Some(doh_req_buf) = tx.handle_frame(&head, &txdata, dir) { - if let Ok(mut dtx) = dns_parse_request(&doh_req_buf) { + if let Ok(mut dtx) = dns_parse_request(&doh_req_buf, &DnsVariant::Dns) { dtx.id = 1; unsafe { AppLayerForceProtocolChange(flow, ALPROTO_DOH2); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 15df3ee1ac..bb7fe6e9fd 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -101,6 +101,7 @@ pub mod handshake; pub mod lua; pub mod dns; +pub mod mdns; pub mod nfs; pub mod ftp; pub mod smb; diff --git a/rust/src/mdns/log.rs b/rust/src/mdns/log.rs new file mode 100644 index 0000000000..0d62d4af39 --- /dev/null +++ b/rust/src/mdns/log.rs @@ -0,0 +1,180 @@ +/* Copyright (C) 2025 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. + */ + +use crate::dns::dns::*; +use crate::dns::log::{ + dns_log_opt, dns_log_soa, dns_log_srv, dns_log_sshfp, dns_print_addr, dns_rrtype_string, +}; +use crate::jsonbuilder::{JsonBuilder, JsonError}; + +fn mdns_log_json_answer_detail(answer: &DNSAnswerEntry) -> Result { + let mut jsa = JsonBuilder::try_new_object()?; + + jsa.set_string_from_bytes("rrname", &answer.name.value)?; + if answer.name.flags.contains(DNSNameFlags::TRUNCATED) { + jsa.set_bool("rrname_truncated", true)?; + } + let rrtype = dns_rrtype_string(answer.rrtype).to_lowercase(); + + match &answer.data { + DNSRData::A(addr) | DNSRData::AAAA(addr) => { + jsa.set_string(&rrtype, &dns_print_addr(addr))?; + } + DNSRData::CNAME(name) | DNSRData::MX(name) | DNSRData::NS(name) | DNSRData::PTR(name) => { + jsa.set_string_from_bytes(&rrtype, &name.value)?; + if name.flags.contains(DNSNameFlags::TRUNCATED) { + jsa.set_bool("rdata_truncated", true)?; + } + } + DNSRData::TXT(txt) => { + jsa.open_array(&rrtype)?; + for txt in txt { + jsa.append_string_from_bytes(txt)?; + } + jsa.close()?; + } + DNSRData::NULL(bytes) | DNSRData::Unknown(bytes) => { + jsa.set_string_from_bytes(&rrtype, bytes)?; + } + DNSRData::SOA(soa) => { + jsa.set_object(&rrtype, &dns_log_soa(soa)?)?; + } + DNSRData::SSHFP(sshfp) => { + jsa.set_object(&rrtype, &dns_log_sshfp(sshfp)?)?; + } + DNSRData::SRV(srv) => { + jsa.set_object(&rrtype, &dns_log_srv(srv)?)?; + } + DNSRData::OPT(opt) => { + jsa.open_array(&rrtype)?; + for val in opt { + jsa.append_object(&dns_log_opt(val)?)?; + } + jsa.close()?; + } + } + + jsa.close()?; + return Ok(jsa); +} + +fn log_json(tx: &DNSTransaction, jb: &mut JsonBuilder) -> Result<(), JsonError> { + jb.open_object("mdns")?; + + let message = if let Some(request) = &tx.request { + jb.set_string("type", "request")?; + request + } else if let Some(response) = &tx.response { + jb.set_string("type", "response")?; + response + } else { + debug_validate_fail!("unreachable"); + return Ok(()); + }; + + // The on the wire mDNS transaction ID. + jb.set_uint("id", tx.tx_id() as u64)?; + + let header = &message.header; + if header.flags & (0x0400 | 0x0200 | 0x0100 | 0x0080 | 0x0040 | 0x0020 | 0x0010) != 0 { + jb.open_array("flags")?; + if header.flags & 0x0400 != 0 { + jb.append_string("aa")?; + } + if header.flags & 0x0200 != 0 { + jb.append_string("tc")?; + } + if header.flags & 0x0100 != 0 { + jb.append_string("rd")?; + } + if header.flags & 0x0080 != 0 { + jb.append_string("ra")?; + } + if header.flags & 0x0040 != 0 { + jb.append_string("z")?; + } + if header.flags & 0x0020 != 0 { + jb.append_string("ad")?; + } + if header.flags & 0x0010 != 0 { + jb.append_string("cd")?; + } + jb.close()?; + } + + let opcode = ((header.flags >> 11) & 0xf) as u8; + jb.set_uint("opcode", opcode as u64)?; + jb.set_uint("rcode", header.flags & 0x000f)?; + + if !message.queries.is_empty() { + jb.open_array("queries")?; + for query in &message.queries { + jb.start_object()? + .set_string_from_bytes("rrname", &query.name.value)? + .set_string("rrtype", &dns_rrtype_string(query.rrtype).to_lowercase())?; + if query.name.flags.contains(DNSNameFlags::TRUNCATED) { + jb.set_bool("rrname_truncated", true)?; + } + jb.close()?; + } + jb.close()?; + } + + if !message.answers.is_empty() { + jb.open_array("answers")?; + for entry in &message.answers { + jb.append_object(&mdns_log_json_answer_detail(entry)?)?; + } + jb.close()?; + } + + if !message.authorities.is_empty() { + jb.open_array("authorities")?; + for entry in &message.authorities { + jb.append_object(&mdns_log_json_answer_detail(entry)?)?; + } + jb.close()?; + } + + if !message.additionals.is_empty() { + let mut is_jb_open = false; + for entry in &message.additionals { + if let DNSRData::OPT(rdata) = &entry.data { + if rdata.is_empty() { + continue; + } + } + if !is_jb_open { + jb.open_array("additionals")?; + is_jb_open = true; + } + jb.append_object(&mdns_log_json_answer_detail(entry)?)?; + } + if is_jb_open { + jb.close()?; + } + } + + jb.close()?; + Ok(()) +} + +/// FFI wrapper around the common V3 style mDNS logger. +#[no_mangle] +pub extern "C" fn SCMdnsLogJson(tx: &DNSTransaction, jb: &mut JsonBuilder) -> bool { + log_json(tx, jb).is_ok() +} diff --git a/rust/src/mdns/mdns.rs b/rust/src/mdns/mdns.rs new file mode 100644 index 0000000000..c8918e7e19 --- /dev/null +++ b/rust/src/mdns/mdns.rs @@ -0,0 +1,131 @@ +/* Copyright (C) 2025 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. + */ + +use std; +use std::ffi::CString; +use std::os::raw::c_void; + +use crate::applayer::*; +use crate::core::*; +use crate::direction::Direction; +use crate::dns::dns; +use crate::flow::Flow; + +use suricata_sys::sys::DetectEngineThreadCtx; +use suricata_sys::sys::{AppProto, AppProtoEnum}; + +pub(super) static mut ALPROTO_MDNS: AppProto = ALPROTO_UNKNOWN; + +unsafe extern "C" fn probe( + _flow: *const Flow, _dir: u8, input: *const u8, len: u32, _rdir: *mut u8, +) -> AppProto { + if crate::dns::dns::probe_udp(_flow, _dir, input, len, _rdir) + == AppProtoEnum::ALPROTO_DNS as u16 + { + let dir = Direction::ToServer; + *_rdir = dir as u8; + return ALPROTO_MDNS; + } + return 0; +} + +/// Returns *mut DNSState +pub(crate) extern "C" fn state_new( + _orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto, +) -> *mut std::os::raw::c_void { + let state = dns::DNSState::new_variant(dns::DnsVariant::MulticastDns); + let boxed = Box::new(state); + return Box::into_raw(boxed) as *mut _; +} + +/// Get the mDNS response answer name and index i. +/// +/// Very similar to the DNS version, but mDNS is always to_server. +#[no_mangle] +pub unsafe extern "C" fn SCMdnsTxGetAnswerName( + _de: *mut DetectEngineThreadCtx, tx: *const c_void, _flow_flags: u8, i: u32, + buf: *mut *const u8, len: *mut u32, +) -> bool { + let tx = cast_pointer!(tx, dns::DNSTransaction); + let answers = if tx.request.is_some() { + tx.request.as_ref().map(|request| &request.answers) + } else { + tx.response.as_ref().map(|response| &response.answers) + }; + let index = i as usize; + + if let Some(answers) = answers { + if let Some(answer) = answers.get(index) { + if !answer.name.value.is_empty() { + *buf = answer.name.value.as_ptr(); + *len = answer.name.value.len() as u32; + return true; + } + } + } + + false +} + +#[no_mangle] +pub unsafe extern "C" fn SCRegisterMdnsParser() { + let default_port = std::ffi::CString::new("[5353]").unwrap(); + let parser = RustParser { + name: b"mdns\0".as_ptr() as *const std::os::raw::c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_UDP, + probe_ts: Some(probe), + probe_tc: Some(probe), + min_depth: 0, + max_depth: std::mem::size_of::() as u16, + state_new, + state_free: dns::state_free, + tx_free: dns::state_tx_free, + parse_ts: dns::parse_request, + parse_tc: dns::parse_request, + get_tx_count: dns::state_get_tx_count, + get_tx: dns::state_get_tx, + tx_comp_st_ts: 1, + tx_comp_st_tc: 1, + tx_get_progress: dns::tx_get_alstate_progress, + get_eventinfo: Some(dns::DNSEvent::get_event_info), + get_eventinfo_byid: Some(dns::DNSEvent::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: None, + get_tx_iterator: Some( + crate::applayer::state_get_tx_iterator::, + ), + get_tx_data: dns::state_get_tx_data, + get_state_data: dns::dns_get_state_data, + apply_tx_config: None, + flags: 0, + get_frame_id_by_name: Some(dns::DnsFrameType::ffi_id_from_name), + get_frame_name_by_id: Some(dns::DnsFrameType::ffi_name_from_id), + get_state_id_by_name: None, + get_state_name_by_id: None, + }; + + let ip_proto_str = CString::new("udp").unwrap(); + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_MDNS = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + } +} diff --git a/rust/src/mdns/mod.rs b/rust/src/mdns/mod.rs new file mode 100644 index 0000000000..f55d1166be --- /dev/null +++ b/rust/src/mdns/mod.rs @@ -0,0 +1,21 @@ +/* Copyright (C) 2025 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. + */ + +//! mDNS parser, detection, logger and application layer module. + +pub mod log; +pub mod mdns; diff --git a/rust/sys/src/sys.rs b/rust/sys/src/sys.rs index 75e75f8081..ac806db0f4 100644 --- a/rust/sys/src/sys.rs +++ b/rust/sys/src/sys.rs @@ -43,8 +43,9 @@ pub enum AppProtoEnum { ALPROTO_HTTP2 = 34, ALPROTO_BITTORRENT_DHT = 35, ALPROTO_POP3 = 36, - ALPROTO_HTTP = 37, - ALPROTO_MAX_STATIC = 38, + ALPROTO_MDNS = 37, + ALPROTO_HTTP = 38, + ALPROTO_MAX_STATIC = 39, } pub type AppProto = u16; extern "C" { diff --git a/src/Makefile.am b/src/Makefile.am index 36f4ad12bb..4b10a970cc 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -392,6 +392,7 @@ noinst_HEADERS = \ output-json-ftp.h \ output-json-http.h \ output-json-ike.h \ + output-json-mdns.h \ output-json-metadata.h \ output-json-mqtt.h \ output-json-netflow.h \ @@ -983,6 +984,7 @@ libsuricata_c_a_SOURCES = \ output-json-ftp.c \ output-json-http.c \ output-json-ike.c \ + output-json-mdns.c \ output-json-metadata.c \ output-json-mqtt.c \ output-json-netflow.c \ diff --git a/src/app-layer-parser.c b/src/app-layer-parser.c index 42b0548b83..ff4d13c4c2 100644 --- a/src/app-layer-parser.c +++ b/src/app-layer-parser.c @@ -1798,6 +1798,7 @@ void AppLayerParserRegisterProtocolParsers(void) SCRegisterWebSocketParser(); SCRegisterLdapTcpParser(); SCRegisterLdapUdpParser(); + SCRegisterMdnsParser(); SCRegisterTemplateParser(); SCRfbRegisterParser(); SCMqttRegisterParser(); diff --git a/src/app-layer-protos.h b/src/app-layer-protos.h index d3a1932fd0..2498c744d2 100644 --- a/src/app-layer-protos.h +++ b/src/app-layer-protos.h @@ -69,6 +69,7 @@ enum AppProtoEnum { ALPROTO_HTTP2, ALPROTO_BITTORRENT_DHT, ALPROTO_POP3, + ALPROTO_MDNS, // signature-only (ie not seen in flow) // HTTP for any version (ALPROTO_HTTP1 (version 1) or ALPROTO_HTTP2) diff --git a/src/app-layer.c b/src/app-layer.c index ed08fba6fb..0171db8cf5 100644 --- a/src/app-layer.c +++ b/src/app-layer.c @@ -1065,6 +1065,7 @@ static void AppLayerNamesSetup(void) AppProtoRegisterProtoString(ALPROTO_WEBSOCKET, "websocket"); AppProtoRegisterProtoString(ALPROTO_LDAP, "ldap"); AppProtoRegisterProtoString(ALPROTO_DOH2, "doh2"); + AppProtoRegisterProtoString(ALPROTO_MDNS, "mdns"); AppProtoRegisterProtoString(ALPROTO_TEMPLATE, "template"); AppProtoRegisterProtoString(ALPROTO_RDP, "rdp"); AppProtoRegisterProtoString(ALPROTO_HTTP2, "http2"); diff --git a/src/detect-dns-name.c b/src/detect-dns-name.c index 5d06bad0da..82f72881ea 100644 --- a/src/detect-dns-name.c +++ b/src/detect-dns-name.c @@ -46,12 +46,17 @@ static int answer_buffer_id = 0; static int authority_buffer_id = 0; static int additional_buffer_id = 0; +static int mdns_query_buffer_id = 0; +static int mdns_answer_buffer_id = 0; +static int mdns_authority_buffer_id = 0; +static int mdns_additional_buffer_id = 0; + static int DetectSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str, int id) { if (SCDetectBufferSetActiveList(de_ctx, s, id) < 0) { return -1; } - if (SCDetectSignatureSetAppProto(s, ALPROTO_DNS) < 0) { + if (SCDetectSignatureSetAppProto(s, s->alproto) < 0) { return -1; } @@ -78,9 +83,29 @@ static int SetupAuthoritiesBuffer(DetectEngineCtx *de_ctx, Signature *s, const c return DetectSetup(de_ctx, s, str, authority_buffer_id); } +static int SetupQueryBufferMdns(DetectEngineCtx *de_ctx, Signature *s, const char *str) +{ + return DetectSetup(de_ctx, s, str, mdns_query_buffer_id); +} + +static int SetupAnswerBufferMdns(DetectEngineCtx *de_ctx, Signature *s, const char *str) +{ + return DetectSetup(de_ctx, s, str, mdns_answer_buffer_id); +} + +static int SetupAdditionalsBufferMdns(DetectEngineCtx *de_ctx, Signature *s, const char *str) +{ + return DetectSetup(de_ctx, s, str, mdns_additional_buffer_id); +} + +static int SetupAuthoritiesBufferMdns(DetectEngineCtx *de_ctx, Signature *s, const char *str) +{ + return DetectSetup(de_ctx, s, str, mdns_authority_buffer_id); +} + static int Register(const char *keyword, const char *desc, const char *doc, int (*Setup)(DetectEngineCtx *, Signature *, const char *), - InspectionMultiBufferGetDataPtr GetBufferFn) + InspectionMultiBufferGetDataPtr GetBufferFn, AppProto alproto) { int keyword_id = SCDetectHelperNewKeywordId(); sigmatch_table[keyword_id].name = keyword; @@ -90,8 +115,8 @@ static int Register(const char *keyword, const char *desc, const char *doc, sigmatch_table[keyword_id].flags |= SIGMATCH_NOOPT; sigmatch_table[keyword_id].flags |= SIGMATCH_INFO_STICKY_BUFFER; - DetectAppLayerMultiRegister(keyword, ALPROTO_DNS, SIG_FLAG_TOSERVER, 1, GetBufferFn, 2); - DetectAppLayerMultiRegister(keyword, ALPROTO_DNS, SIG_FLAG_TOCLIENT, 1, GetBufferFn, 2); + DetectAppLayerMultiRegister(keyword, alproto, SIG_FLAG_TOSERVER, 1, GetBufferFn, 2); + DetectAppLayerMultiRegister(keyword, alproto, SIG_FLAG_TOCLIENT, 1, GetBufferFn, 2); DetectBufferTypeSetDescriptionByName(keyword, keyword); DetectBufferTypeSupportsMultiInstance(keyword); @@ -102,14 +127,31 @@ static int Register(const char *keyword, const char *desc, const char *doc, void DetectDnsNameRegister(void) { query_buffer_id = Register("dns.queries.rrname", "DNS query rrname sticky buffer", - "/rules/dns-keywords.html#dns.queries.rrname", SetupQueryBuffer, SCDnsTxGetQueryName); + "/rules/dns-keywords.html#dns.queries.rrname", SetupQueryBuffer, SCDnsTxGetQueryName, + ALPROTO_DNS); answer_buffer_id = Register("dns.answers.rrname", "DNS answer rrname sticky buffer", - "/rules/dns-keywords.html#dns.answers.rrname", SetupAnswerBuffer, SCDnsTxGetAnswerName); + "/rules/dns-keywords.html#dns.answers.rrname", SetupAnswerBuffer, SCDnsTxGetAnswerName, + ALPROTO_DNS); additional_buffer_id = Register("dns.additionals.rrname", "DNS additionals rrname sticky buffer", "/rules/dns-keywords.html#dns-additionals-rrname", SetupAdditionalsBuffer, - SCDnsTxGetAdditionalName); + SCDnsTxGetAdditionalName, ALPROTO_DNS); authority_buffer_id = Register("dns.authorities.rrname", "DNS authorities rrname sticky buffer", "/rules/dns-keywords.html#dns-authorities-rrname", SetupAuthoritiesBuffer, - SCDnsTxGetAuthorityName); + SCDnsTxGetAuthorityName, ALPROTO_DNS); + + mdns_query_buffer_id = Register("mdns.queries.rrname", "mDNS query rrname sticky buffer", + "/rules/mdns-keywords.html#mdns.queries.rrname", SetupQueryBufferMdns, + SCDnsTxGetQueryName, ALPROTO_MDNS); + mdns_answer_buffer_id = Register("mdns.answers.rrname", "mDNS answer rrname sticky buffer", + "/rules/mdns-keywords.html#mdns.answers.rrname", SetupAnswerBufferMdns, + SCMdnsTxGetAnswerName, ALPROTO_MDNS); + mdns_additional_buffer_id = + Register("mdns.additionals.rrname", "mDNS additionals rrname sticky buffer", + "/rules/mdns-keywords.html#mdns-additionals-rrname", SetupAdditionalsBufferMdns, + SCDnsTxGetAdditionalName, ALPROTO_MDNS); + mdns_authority_buffer_id = + Register("mdns.authorities.rrname", "mDNS authorities rrname sticky buffer", + "/rules/mdns-keywords.html#mdns-authorities-rrname", SetupAuthoritiesBufferMdns, + SCDnsTxGetAuthorityName, ALPROTO_MDNS); } diff --git a/src/detect-dns-response.c b/src/detect-dns-response.c index 0ec2aff416..81ac31304d 100644 --- a/src/detect-dns-response.c +++ b/src/detect-dns-response.c @@ -28,11 +28,14 @@ #include "detect-engine-mpm.h" #include "detect-engine-prefilter.h" #include "detect-engine-content-inspection.h" +#include "detect-engine-helper.h" #include "detect-dns-response.h" #include "util-profiling.h" #include "rust.h" static int detect_buffer_id = 0; +static int mdns_detect_buffer_id = 0; + typedef struct PrefilterMpm { int list_id; const MpmCtx *mpm_ctx; @@ -67,6 +70,18 @@ static int DetectSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str) return 0; } +static int MdnsDetectSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str) +{ + if (SCDetectBufferSetActiveList(de_ctx, s, mdns_detect_buffer_id) < 0) { + return -1; + } + if (SCDetectSignatureSetAppProto(s, ALPROTO_MDNS) < 0) { + return -1; + } + + return 0; +} + static InspectionBuffer *GetBuffer(DetectEngineThreadCtx *det_ctx, uint8_t flags, const DetectEngineTransforms *transforms, void *txv, struct DnsResponseGetDataArgs *cbdata, int list_id, bool get_rdata) @@ -311,6 +326,29 @@ static int DetectDnsResponsePrefilterMpmRegister(DetectEngineCtx *de_ctx, SigGro DetectDnsResponsePrefilterMpmFree, mpm_reg->pname); } +static void SCDetectMdnsResponseRrnameRegister(void) +{ + static const char *keyword = "mdns.response.rrname"; + int keyword_id = SCDetectHelperNewKeywordId(); + sigmatch_table[keyword_id].name = keyword; + sigmatch_table[keyword_id].desc = "mDNS response rrname buffer"; + sigmatch_table[keyword_id].url = "/rules/mdns-keywords.html#mdns-response-rrname"; + sigmatch_table[keyword_id].Setup = MdnsDetectSetup; + sigmatch_table[keyword_id].flags |= SIGMATCH_NOOPT; + sigmatch_table[keyword_id].flags |= SIGMATCH_INFO_STICKY_BUFFER; + + /* Register in the TO_SERVER direction, as all mDNS is toserver. */ + DetectAppLayerInspectEngineRegister( + keyword, ALPROTO_MDNS, SIG_FLAG_TOSERVER, 1, DetectEngineInspectCb, NULL); + DetectAppLayerMpmRegister(keyword, SIG_FLAG_TOSERVER, 2, DetectDnsResponsePrefilterMpmRegister, + NULL, ALPROTO_MDNS, 1); + + DetectBufferTypeSetDescriptionByName(keyword, "mdns response rdata"); + DetectBufferTypeSupportsMultiInstance(keyword); + + mdns_detect_buffer_id = DetectBufferTypeGetByName(keyword); +} + void DetectDnsResponseRegister(void) { static const char *keyword = "dns.response.rrname"; @@ -331,4 +369,6 @@ void DetectDnsResponseRegister(void) DetectBufferTypeSupportsMultiInstance(keyword); detect_buffer_id = DetectBufferTypeGetByName(keyword); + + SCDetectMdnsResponseRrnameRegister(); } diff --git a/src/output-json-mdns.c b/src/output-json-mdns.c new file mode 100644 index 0000000000..be45b434a1 --- /dev/null +++ b/src/output-json-mdns.c @@ -0,0 +1,160 @@ +/* Copyright (C) 2025 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. + */ + +#include "suricata-common.h" +#include "conf.h" + +#include "threadvars.h" + +#include "util-debug.h" +#include "app-layer-parser.h" +#include "output.h" + +#include "output-json.h" +#include "output-json-mdns.h" +#include "rust.h" + +typedef struct SCDnsLogFileCtx_ { + uint64_t flags; /** Store mode */ + OutputJsonCtx *eve_ctx; + uint8_t version; +} SCDnsLogFileCtx; + +typedef struct SCDnsLogThread_ { + SCDnsLogFileCtx *dnslog_ctx; + OutputJsonThreadCtx *ctx; +} SCDnsLogThread; + +bool AlertJsonMdns(void *txptr, SCJsonBuilder *js) +{ + return SCMdnsLogJson(txptr, js); +} + +static int JsonMdnsLogger(ThreadVars *tv, void *thread_data, const Packet *p, Flow *f, + void *alstate, void *txptr, uint64_t tx_id) +{ + SCDnsLogThread *td = (SCDnsLogThread *)thread_data; + SCDnsLogFileCtx *dnslog_ctx = td->dnslog_ctx; + + SCJsonBuilder *jb = CreateEveHeader(p, LOG_DIR_FLOW, "mdns", NULL, dnslog_ctx->eve_ctx); + if (unlikely(jb == NULL)) { + return TM_ECODE_OK; + } + + if (SCMdnsLogJson(txptr, jb)) { + OutputJsonBuilderBuffer(tv, p, p->flow, jb, td->ctx); + } + SCJbFree(jb); + + return TM_ECODE_OK; +} + +static TmEcode SCDnsLogThreadInit(ThreadVars *t, const void *initdata, void **data) +{ + SCDnsLogThread *aft = SCCalloc(1, sizeof(SCDnsLogThread)); + if (unlikely(aft == NULL)) + return TM_ECODE_FAILED; + + if (initdata == NULL) { + SCLogDebug("Error getting log context for eve-log.mdns. \"initdata\" argument NULL"); + goto error_exit; + } + + /* Use the Output Context (file pointer and mutex) */ + aft->dnslog_ctx = ((OutputCtx *)initdata)->data; + aft->ctx = CreateEveThreadCtx(t, aft->dnslog_ctx->eve_ctx); + if (!aft->ctx) { + goto error_exit; + } + + *data = (void *)aft; + return TM_ECODE_OK; + +error_exit: + SCFree(aft); + return TM_ECODE_FAILED; +} + +static TmEcode SCDnsLogThreadDeinit(ThreadVars *t, void *data) +{ + SCDnsLogThread *aft = (SCDnsLogThread *)data; + if (aft == NULL) { + return TM_ECODE_OK; + } + FreeEveThreadCtx(aft->ctx); + + /* clear memory */ + memset(aft, 0, sizeof(SCDnsLogThread)); + + SCFree(aft); + return TM_ECODE_OK; +} + +static void DnsLogDeInitCtxSub(OutputCtx *output_ctx) +{ + SCDnsLogFileCtx *dnslog_ctx = (SCDnsLogFileCtx *)output_ctx->data; + SCFree(dnslog_ctx); + SCFree(output_ctx); +} + +static OutputInitResult DnsLogInitCtxSub(SCConfNode *conf, OutputCtx *parent_ctx) +{ + OutputInitResult result = { NULL, false }; + const char *enabled = SCConfNodeLookupChildValue(conf, "enabled"); + if (enabled != NULL && !SCConfValIsTrue(enabled)) { + result.ok = true; + return result; + } + + OutputJsonCtx *ojc = parent_ctx->data; + + SCDnsLogFileCtx *dnslog_ctx = SCCalloc(1, sizeof(SCDnsLogFileCtx)); + if (unlikely(dnslog_ctx == NULL)) { + return result; + } + + dnslog_ctx->eve_ctx = ojc; + dnslog_ctx->version = DNS_LOG_VERSION_3; + + /* For mDNS, log everything. + * + * TODO: Maybe add flags for request and/or response only. + */ + dnslog_ctx->flags = ~0ULL; + + OutputCtx *output_ctx = SCCalloc(1, sizeof(OutputCtx)); + if (unlikely(output_ctx == NULL)) { + SCFree(dnslog_ctx); + return result; + } + + output_ctx->data = dnslog_ctx; + output_ctx->DeInit = DnsLogDeInitCtxSub; + + AppLayerParserRegisterLogger(IPPROTO_UDP, ALPROTO_MDNS); + + result.ctx = output_ctx; + result.ok = true; + return result; +} + +void JsonMdnsLogRegister(void) +{ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonMdnsLog", "eve-log.mdns", + DnsLogInitCtxSub, ALPROTO_MDNS, JsonMdnsLogger, SCDnsLogThreadInit, + SCDnsLogThreadDeinit); +} diff --git a/src/output-json-mdns.h b/src/output-json-mdns.h new file mode 100644 index 0000000000..beeb42382a --- /dev/null +++ b/src/output-json-mdns.h @@ -0,0 +1,24 @@ +/* Copyright (C) 2025 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. + */ + +#ifndef SURICATA_OUTPUT_JSON_MDNS_H +#define SURICATA_OUTPUT_JSON_MDNS_H + +void JsonMdnsLogRegister(void); +bool AlertJsonMdns(void *txptr, SCJsonBuilder *js); + +#endif /* SURICATA_OUTPUT_JSON_MDNS_H */ diff --git a/src/output.c b/src/output.c index d03cdcaa59..a913d31bf3 100644 --- a/src/output.c +++ b/src/output.c @@ -55,6 +55,7 @@ #include "log-httplog.h" #include "output-json-http.h" #include "output-json-dns.h" +#include "output-json-mdns.h" #include "log-tlslog.h" #include "log-tlsstore.h" #include "output-json-tls.h" @@ -900,6 +901,7 @@ void OutputRegisterRootLoggers(void) // ALPROTO_SMB special: uses state // ALPROTO_DCERPC special: uses state RegisterSimpleJsonApplayerLogger(ALPROTO_DNS, (EveJsonSimpleTxLogFunc)AlertJsonDns, NULL); + RegisterSimpleJsonApplayerLogger(ALPROTO_MDNS, (EveJsonSimpleTxLogFunc)AlertJsonMdns, NULL); // either need a cast here or in rust for ModbusTransaction, done here RegisterSimpleJsonApplayerLogger(ALPROTO_MODBUS, (EveJsonSimpleTxLogFunc)SCModbusToJson, NULL); RegisterSimpleJsonApplayerLogger(ALPROTO_ENIP, (EveJsonSimpleTxLogFunc)SCEnipLoggerLog, NULL); @@ -1059,6 +1061,8 @@ void OutputRegisterLoggers(void) OutputFilestoreRegister(); /* dns */ JsonDnsLogRegister(); + /* mdns */ + JsonMdnsLogRegister(); /* modbus */ OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonModbusLog", "eve-log.modbus", OutputJsonLogInitSub, ALPROTO_MODBUS, JsonGenericDirFlowLogger, JsonLogThreadInit, @@ -1150,6 +1154,10 @@ void OutputRegisterLoggers(void) OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonPop3Log", "eve-log.pop3", OutputJsonLogInitSub, ALPROTO_POP3, JsonGenericDirFlowLogger, JsonLogThreadInit, JsonLogThreadDeinit); + /* Mdns JSON logger. */ + OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonMdnsLog", "eve-log.template", + OutputJsonLogInitSub, ALPROTO_MDNS, JsonGenericDirPacketLogger, JsonLogThreadInit, + JsonLogThreadDeinit); /* Template JSON logger. */ OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonTemplateLog", "eve-log.template", OutputJsonLogInitSub, ALPROTO_TEMPLATE, JsonGenericDirPacketLogger, JsonLogThreadInit, diff --git a/suricata.yaml.in b/suricata.yaml.in index 32d733e7d9..4bc0835468 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -281,6 +281,7 @@ outputs: # DNS record types to log, based on the query type. # Default: all. #types: [a, aaaa, cname, mx, ns, ptr, txt] + - mdns: - tls: extended: yes # enable this for extended logging information # output TLS transaction where the session is resumed using a @@ -1255,6 +1256,9 @@ app-layer: # Maximum number of live LDAP transactions per flow # max-tx: 1024 + mdns: + enabled: yes + # Limit for the maximum number of asn1 frames to decode (default 256) asn1-max-frames: 256