]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
mdns: add mdns parser, logger and detection 13334/head
authorJason Ish <jason.ish@oisf.net>
Wed, 28 May 2025 18:43:05 +0000 (12:43 -0600)
committerVictor Julien <victor@inliniac.net>
Thu, 29 May 2025 08:59:16 +0000 (10:59 +0200)
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

22 files changed:
doc/userguide/rules/index.rst
doc/userguide/rules/mdns-keywords.rst [new file with mode: 0644]
etc/schema.json
rust/src/dns/detect.rs
rust/src/dns/dns.rs
rust/src/dns/log.rs
rust/src/http2/http2.rs
rust/src/lib.rs
rust/src/mdns/log.rs [new file with mode: 0644]
rust/src/mdns/mdns.rs [new file with mode: 0644]
rust/src/mdns/mod.rs [new file with mode: 0644]
rust/sys/src/sys.rs
src/Makefile.am
src/app-layer-parser.c
src/app-layer-protos.h
src/app-layer.c
src/detect-dns-name.c
src/detect-dns-response.c
src/output-json-mdns.c [new file with mode: 0644]
src/output-json-mdns.h [new file with mode: 0644]
src/output.c
suricata.yaml.in

index a1192741c92f5e1e97deb5c8e8f90109867d22f2..b34c5519c56edab4b8ab160f66b6a0ff0be76024 100644 (file)
@@ -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 (file)
index 0000000..845b3d8
--- /dev/null
@@ -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;)
index f07c2ebbd018b4c9ea2866aa4f6306fe19b563cc..2205fbc92aadde68ca7bba5d2ec5975e27b43534 100644 (file)
         "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,
                                     "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"
                                     "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"
                                     "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"
index 64959317040bdbee908395dfea85acbc49b1cefa..db373e90366bd39f14f4e1e9eb79a104fafeba85 100644 (file)
@@ -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 {
index 579ae7a4e11d2e27a9d9f2e7f7ea95a48e5f2bfb..3420a9938337e7e5de989fcdce91c7f037b16020 100644 (file)
@@ -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<DNSTransaction, DNSParseError> {
+pub(crate) fn dns_parse_request(input: &[u8], variant: &DnsVariant) -> Result<DNSTransaction, DNSParseError> {
     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<DNSTransaction, DNSParse
 
     match parser::dns_parse_body(body, input, header) {
         Ok((_, (request, parse_flags))) => {
-            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<DNSTransaction, DNSParse
             if request.invalid_authorities {
                 tx.set_event(DNSEvent::InvalidAuthorities);
             }
-            tx.request = Some(request);
+
+            if variant.is_mdns() && request.header.flags & 0x8000 != 0 {
+                tx.response = Some(request);
+            } else {
+                tx.request = Some(request);
+            }
 
             if z_flag {
                 SCLogDebug!("Z-flag set on DNS request");
@@ -525,7 +546,25 @@ pub(crate) fn dns_parse_response(input: &[u8]) -> Result<DNSTransaction, DNSPars
 
 impl DNSState {
     fn new() -> 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<Frame>, 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::<DNSHeader>() as u32 {
index f3e14596946606f03831426bde98ba223caf332f..910572127f6655cbd47121d925f5e3949c5620c7 100644 (file)
@@ -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<JsonBuilder, JsonError> {
+pub(crate) fn dns_log_opt(opt: &DNSRDataOPT) -> Result<JsonBuilder, JsonError> {
     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<JsonBuilder, JsonError> {
 }
 
 /// Log SOA section fields.
-fn dns_log_soa(soa: &DNSRDataSOA) -> Result<JsonBuilder, JsonError> {
+pub(crate) fn dns_log_soa(soa: &DNSRDataSOA) -> Result<JsonBuilder, JsonError> {
     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<JsonBuilder, JsonError> {
 }
 
 /// Log SSHFP section fields.
-fn dns_log_sshfp(sshfp: &DNSRDataSSHFP) -> Result<JsonBuilder, JsonError> {
+pub(crate) fn dns_log_sshfp(sshfp: &DNSRDataSSHFP) -> Result<JsonBuilder, JsonError> {
     let mut js = JsonBuilder::try_new_object()?;
 
     let mut hex = Vec::new();
@@ -373,7 +373,7 @@ fn dns_log_sshfp(sshfp: &DNSRDataSSHFP) -> Result<JsonBuilder, JsonError> {
 }
 
 /// Log SRV section fields.
-fn dns_log_srv(srv: &DNSRDataSRV) -> Result<JsonBuilder, JsonError> {
+pub(crate) fn dns_log_srv(srv: &DNSRDataSRV) -> Result<JsonBuilder, JsonError> {
     let mut js = JsonBuilder::try_new_object()?;
 
     js.set_uint("priority", srv.priority as u64)?;
index e11525613adb033c18e5d3eebb0333960e5bf3a1..e844021c76cdb794d4d9b65477bf0e6063572835 100644 (file)
@@ -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);
index 15df3ee1acefe103eab6fab76d4530bb9debabc5..bb7fe6e9fdf628b2e763c6cfe462af958577d4ce 100644 (file)
@@ -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 (file)
index 0000000..0d62d4a
--- /dev/null
@@ -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<JsonBuilder, JsonError> {
+    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 (file)
index 0000000..c8918e7
--- /dev/null
@@ -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::<dns::DNSHeader>() 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::<dns::DNSState, dns::DNSTransaction>,
+        ),
+        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 (file)
index 0000000..f55d116
--- /dev/null
@@ -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;
index 75e75f80811a9ce30d9fd2a928fac776d4ba89fa..ac806db0f473943245530446e0a4b3f21a0fffd9 100644 (file)
@@ -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" {
index 36f4ad12bb86b6b8bee79c7800be3cb8c7a31223..4b10a970cccd66f70ba2046480f215a2d5907af4 100755 (executable)
@@ -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 \
index 42b0548b8364c01af2097804186d4db841ec948d..ff4d13c4c206ad3fd3feb793df94b253bd4c62b9 100644 (file)
@@ -1798,6 +1798,7 @@ void AppLayerParserRegisterProtocolParsers(void)
     SCRegisterWebSocketParser();
     SCRegisterLdapTcpParser();
     SCRegisterLdapUdpParser();
+    SCRegisterMdnsParser();
     SCRegisterTemplateParser();
     SCRfbRegisterParser();
     SCMqttRegisterParser();
index d3a1932fd017f0bf3ba351d70a3d9b66fbe03a61..2498c744d2f42722bea7a07a43bd15661dc5f8c2 100644 (file)
@@ -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)
index ed08fba6fb9b1dd1d14ff19e7f6fdafdcf9f7247..0171db8cf5d66c96d6ee2c8360aa8abb81974cc1 100644 (file)
@@ -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");
index 5d06bad0da3945084383a301de8121c9bdb12770..82f72881eaa5e553621a8eca4de890cda7e46c99 100644 (file)
@@ -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);
 }
index 0ec2aff4164102fa05ae5cde82d6106b96869e3d..81ac31304de12e628cfe893174feb1bc0d051c16 100644 (file)
 #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 (file)
index 0000000..be45b43
--- /dev/null
@@ -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 (file)
index 0000000..beeb423
--- /dev/null
@@ -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 */
index d03cdcaa593d56e4ff77eb48d2f2d0b1b5cb0c08..a913d31bf3a8f1ecff2567ccb587640051dfa22e 100644 (file)
@@ -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,
index 32d733e7d9aa8ca87ff850673dc4e9880085e4de..4bc0835468ffc7d55850619d67f41b8f66d154cf 100644 (file)
@@ -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