llmnr: implement parser

This adds a parser for LLMNR protocol over both UDP and TCP.

The parser reuses the existing DNS functions since LLMNR shares
the same wire format as DNS, but implements its own protocol-specific
validation:

- LLMNR-specific flag checks (C, TC, T bits)
- Opcode validation (only standard query opcode 0 is valid)
- An Event is set if Z-flag is set, since it's must be zero per RFC4795

LLMNR transactions inherit DNS behavior where each packet creates a new state
with one transaction.

Ticket #8366
pull/15509/head
Giuseppe Longo 2 years ago committed by Victor Julien
parent 9e82ff82e2
commit 13df0f7a06

@ -361,6 +361,7 @@ impl ConfigTracker {
pub(crate) enum DnsVariant {
Dns,
MulticastDns,
Llmnr,
}
impl DnsVariant {
@ -371,6 +372,10 @@ impl DnsVariant {
pub fn is_mdns(&self) -> bool {
matches!(self, DnsVariant::MulticastDns)
}
pub fn is_llmnr(&self) -> bool {
matches!(self, DnsVariant::Llmnr)
}
}
//#[derive(Default)]
@ -454,7 +459,7 @@ pub(crate) fn dns_parse_request(
tx.set_event(DNSEvent::ZFlagSet);
}
if opcode >= 7 {
if (variant.is_llmnr() && opcode != 0) || opcode >= 7 {
tx.set_event(DNSEvent::InvalidOpcode);
}
@ -939,7 +944,7 @@ pub(crate) unsafe extern "C" fn parse_request(
AppLayerResult::ok()
}
unsafe extern "C" fn parse_response(
pub(crate) unsafe extern "C" fn parse_response(
flow: *mut Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState,
stream_slice: StreamSlice, _data: *mut std::os::raw::c_void,
) -> AppLayerResult {
@ -949,7 +954,7 @@ unsafe extern "C" fn parse_response(
}
/// C binding parse a DNS request. Returns 1 on success, -1 on failure.
unsafe extern "C" fn parse_request_tcp(
pub(crate) unsafe extern "C" fn parse_request_tcp(
flow: *mut Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState,
stream_slice: StreamSlice, _data: *mut std::os::raw::c_void,
) -> AppLayerResult {
@ -962,7 +967,7 @@ unsafe extern "C" fn parse_request_tcp(
AppLayerResult::ok()
}
unsafe extern "C" fn parse_response_tcp(
pub(crate) unsafe extern "C" fn parse_response_tcp(
flow: *mut Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState,
stream_slice: StreamSlice, _data: *mut std::os::raw::c_void,
) -> AppLayerResult {
@ -1228,7 +1233,7 @@ pub(crate) unsafe extern "C" fn probe_udp(
return 0;
}
unsafe extern "C" fn c_probe_tcp(
pub(crate) unsafe extern "C" fn c_probe_tcp(
_flow: *const Flow, direction: u8, input: *const u8, len: u32, rdir: *mut u8,
) -> AppProto {
if input.is_null() || len < std::mem::size_of::<DNSHeader>() as u32 + 2 {

@ -140,6 +140,7 @@ pub mod sdp;
pub mod ldap;
pub mod flow;
pub use suricata_ffi::direction;
pub mod llmnr;
#[allow(unused_imports)]
pub use suricata_lua_sys;

@ -0,0 +1,162 @@
/* Copyright (C) 2026 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.
*/
// written by Giuseppe Longo <giuseppe@glongo.it>
use std;
use std::ffi::CString;
use crate::applayer::*;
use crate::core::*;
use crate::dns::dns;
use crate::dns::dns::DNSHeader;
use crate::flow::Flow;
use suricata_sys::sys::{
AppProto, AppProtoEnum, SCAppLayerParserConfParserEnabled,
SCAppLayerProtoDetectConfProtoDetectionEnabled,
};
static mut ALPROTO_LLMNR: AppProto = ALPROTO_UNKNOWN;
unsafe extern "C" fn probe_udp(
_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
{
return ALPROTO_LLMNR;
}
return 0;
}
unsafe extern "C" fn probe_tcp(
_flow: *const Flow, _dir: u8, input: *const u8, len: u32, _rdir: *mut u8,
) -> AppProto {
if crate::dns::dns::c_probe_tcp(_flow, _dir, input, len, _rdir)
== AppProtoEnum::ALPROTO_DNS as u16
{
return ALPROTO_LLMNR;
}
return 0;
}
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::Llmnr);
let boxed = Box::new(state);
return Box::into_raw(boxed) as *mut _;
}
#[no_mangle]
pub unsafe extern "C" fn SCRegisterLLMNRUdpParser() {
let default_port = std::ffi::CString::new("5355").unwrap();
let parser = RustParser {
name: b"llmnr\0".as_ptr() as *const std::os::raw::c_char,
default_port: default_port.as_ptr(),
ipproto: IPPROTO_UDP,
probe_ts: Some(probe_udp),
probe_tc: Some(probe_udp),
min_depth: 0,
max_depth: std::mem::size_of::<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_response,
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 SCAppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
let alproto = applayer_register_protocol_detection(&parser, 1);
ALPROTO_LLMNR = alproto;
if SCAppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
let _ = AppLayerRegisterParser(&parser, alproto);
}
}
}
#[no_mangle]
pub unsafe extern "C" fn SCRegisterLLMNRTcpParser() {
let default_port = std::ffi::CString::new("5355").unwrap();
let parser = RustParser {
name: b"llmnr\0".as_ptr() as *const std::os::raw::c_char,
default_port: default_port.as_ptr(),
ipproto: IPPROTO_TCP,
probe_ts: Some(probe_tcp),
probe_tc: Some(probe_tcp),
min_depth: 0,
max_depth: std::mem::size_of::<DNSHeader>() as u16 + 2,
state_new,
state_free: dns::state_free,
tx_free: dns::state_tx_free,
parse_ts: dns::parse_request_tcp,
parse_tc: dns::parse_response_tcp,
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: APP_LAYER_PARSER_OPT_ACCEPT_GAPS,
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("tcp").unwrap();
if SCAppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
let alproto = applayer_register_protocol_detection(&parser, 1);
ALPROTO_LLMNR = alproto;
if SCAppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
let _ = AppLayerRegisterParser(&parser, alproto);
}
}
}

@ -0,0 +1,22 @@
/* Copyright (C) 2026 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.
*/
//! LLMNR protocol parser and logger module.
// written by Giuseppe Longo <giuseppe@glongo.it>
pub mod llmnr;

@ -67,8 +67,9 @@ pub enum AppProtoEnum {
ALPROTO_BITTORRENT_DHT = 35,
ALPROTO_POP3 = 36,
ALPROTO_MDNS = 37,
ALPROTO_HTTP = 38,
ALPROTO_MAX_STATIC = 39,
ALPROTO_LLMNR = 38,
ALPROTO_HTTP = 39,
ALPROTO_MAX_STATIC = 40,
}
pub type AppProto = u16;
extern "C" {

@ -1851,6 +1851,8 @@ void AppLayerParserRegisterProtocolParsers(void)
RegisterHTTP2Parsers();
SCRegisterTelnetParser();
RegisterIMAPParsers();
SCRegisterLLMNRUdpParser();
SCRegisterLLMNRTcpParser();
for (size_t i = 0; i < preregistered_callbacks_nb; i++) {
PreRegisteredCallbacks[i]();

@ -70,6 +70,7 @@ enum AppProtoEnum {
ALPROTO_BITTORRENT_DHT,
ALPROTO_POP3,
ALPROTO_MDNS,
ALPROTO_LLMNR,
// signature-only (ie not seen in flow)
// HTTP for any version (ALPROTO_HTTP1 (version 1) or ALPROTO_HTTP2)

@ -1072,6 +1072,7 @@ static void AppLayerNamesSetup(void)
AppProtoRegisterProtoString(ALPROTO_LDAP, "ldap");
AppProtoRegisterProtoString(ALPROTO_DOH2, "doh2");
AppProtoRegisterProtoString(ALPROTO_MDNS, "mdns");
AppProtoRegisterProtoString(ALPROTO_LLMNR, "llmnr");
AppProtoRegisterProtoString(ALPROTO_TEMPLATE, "template");
AppProtoRegisterProtoString(ALPROTO_RDP, "rdp");
AppProtoRegisterProtoString(ALPROTO_HTTP2, "http2");

@ -1296,6 +1296,12 @@ app-layer:
mdns:
enabled: yes
llmnr:
enabled: yes
detection-ports:
dp: 5355
sp: 5355
# Limit for the maximum number of asn1 frames to decode (default 256)
asn1-max-frames: 256

Loading…
Cancel
Save