From: Jason Ish Date: Wed, 28 May 2025 18:43:05 +0000 (-0600) Subject: mdns: add mdns parser, logger and detection X-Git-Tag: suricata-8.0.0-rc1~211 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F13334%2Fhead;p=thirdparty%2Fsuricata.git mdns: add mdns parser, logger and detection The mDNS support is based heavily on the DNS support, reusing the existing DNS parser where possible. This meant adding variations on DNS, as mDNS is a little different. Mainly being that *all* mDNS traffic is to_server, yet there is still the concept of request and responses. Keywords added are: - mdns.queries.rrname - mdns.answers.rrname - mdns.additionals.rrname - mdns.authorities.rrname - mdns.response.rrname They are mostly in-line with the DNS keywords, except mdns.answers.rdata which is a better than that mdns.response.rrname, as its actually looking at the rdata, and not rrnames. mDNS has its own logger that differs from the DNS logger: - No grouped logging - In answers/additionals/authorities, the rdata is logged in a field that is named after the rdata type. For example, "txt" data is no longer logged in the "rdata" field, but instead a "txt" field. We currently already did this in DNS for fields that were not a single buffer, like SOA, SRV, etc. So this makes things more consistent. And gives query like semantics that the "grouped" object was trying to provide. - Types are logged in lower case ("txt" instead of "TXT") - Flags are logged as an array: "flags": ["aa", "z"] Ticket: #3952 --- 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