mirror of https://github.com/OISF/suricata
llmnr: implement logger
This adds an LLMNR protocol logger that reuses existing DNS functions, following the same approach as the mDNS logger: - No grouped logging - Rdata is logged in a field that is named after the rdata type - Types are logged in lower case - Flags are logged as an array Ticket #8366pull/15509/head
parent
13df0f7a06
commit
8f205bb34f
@ -0,0 +1,168 @@
|
||||
/* 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 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 llmnr_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("llmnr")?;
|
||||
|
||||
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 LLMNR transaction ID.
|
||||
jb.set_uint("id", tx.tx_id() as u64)?;
|
||||
|
||||
let header = &message.header;
|
||||
if header.flags & (0x0400 | 0x0200 | 0x0100) != 0 {
|
||||
jb.open_array("flags")?;
|
||||
if header.flags & 0x0400 != 0 {
|
||||
jb.append_string("c")?;
|
||||
}
|
||||
if header.flags & 0x0200 != 0 {
|
||||
jb.append_string("tc")?;
|
||||
}
|
||||
if header.flags & 0x0100 != 0 {
|
||||
jb.append_string("t")?;
|
||||
}
|
||||
jb.close()?;
|
||||
}
|
||||
|
||||
let opcode = ((header.flags >> 11) & 0xf) as u8;
|
||||
jb.set_uint("opcode", opcode as u64)?;
|
||||
|
||||
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(&llmnr_log_json_answer_detail(entry)?)?;
|
||||
}
|
||||
jb.close()?;
|
||||
}
|
||||
|
||||
if !message.authorities.is_empty() {
|
||||
jb.open_array("authorities")?;
|
||||
for entry in &message.authorities {
|
||||
jb.append_object(&llmnr_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(&llmnr_log_json_answer_detail(entry)?)?;
|
||||
}
|
||||
if is_jb_open {
|
||||
jb.close()?;
|
||||
}
|
||||
}
|
||||
|
||||
jb.close()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn SCLLMNRLogJson(tx: &mut DNSTransaction, jb: &mut JsonBuilder) -> bool {
|
||||
log_json(tx, jb).is_ok()
|
||||
}
|
||||
@ -0,0 +1,161 @@
|
||||
/* 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>
|
||||
|
||||
#include "suricata-common.h"
|
||||
#include "conf.h"
|
||||
|
||||
#include "threadvars.h"
|
||||
|
||||
#include "util-byte.h"
|
||||
#include "util-debug.h"
|
||||
#include "util-mem.h"
|
||||
#include "app-layer-parser.h"
|
||||
#include "output.h"
|
||||
|
||||
#include "output-json.h"
|
||||
#include "output-json-llmnr.h"
|
||||
#include "rust.h"
|
||||
|
||||
typedef struct SCLLMNRLogFileCtx_ {
|
||||
uint64_t flags; /** Store mode */
|
||||
OutputJsonCtx *eve_ctx;
|
||||
} SCLLMNRLogFileCtx;
|
||||
|
||||
typedef struct LogLLMNRLogThread_ {
|
||||
SCLLMNRLogFileCtx *llmnrlog_ctx;
|
||||
OutputJsonThreadCtx *ctx;
|
||||
} SCLLMNRLogThread;
|
||||
|
||||
bool AlertJsonLLMNR(void *txptr, SCJsonBuilder *js)
|
||||
{
|
||||
return SCLLMNRLogJson(txptr, js);
|
||||
}
|
||||
|
||||
static int JsonLLMNRLogger(ThreadVars *tv, void *thread_data, const Packet *p, Flow *f,
|
||||
void *alstate, void *txptr, uint64_t tx_id)
|
||||
{
|
||||
SCLLMNRLogThread *td = (SCLLMNRLogThread *)thread_data;
|
||||
SCLLMNRLogFileCtx *llmnrlog_ctx = td->llmnrlog_ctx;
|
||||
|
||||
SCJsonBuilder *jb = CreateEveHeader(p, LOG_DIR_FLOW, "llmnr", NULL, llmnrlog_ctx->eve_ctx);
|
||||
if (unlikely(jb == NULL)) {
|
||||
return TM_ECODE_OK;
|
||||
}
|
||||
|
||||
if (SCLLMNRLogJson(txptr, jb)) {
|
||||
OutputJsonBuilderBuffer(tv, p, p->flow, jb, td->ctx);
|
||||
}
|
||||
SCJbFree(jb);
|
||||
|
||||
return TM_ECODE_OK;
|
||||
}
|
||||
|
||||
static TmEcode SCLLMNRLogThreadInit(ThreadVars *t, const void *initdata, void **data)
|
||||
{
|
||||
SCLLMNRLogThread *aft = SCCalloc(1, sizeof(SCLLMNRLogThread));
|
||||
if (unlikely(aft == NULL))
|
||||
return TM_ECODE_FAILED;
|
||||
|
||||
if (initdata == NULL) {
|
||||
SCLogDebug("Error getting context for EveLogLLMNR. \"initdata\" argument NULL");
|
||||
goto error_exit;
|
||||
}
|
||||
|
||||
/* Use the Output Context (file pointer and mutex) */
|
||||
aft->llmnrlog_ctx = ((OutputCtx *)initdata)->data;
|
||||
aft->ctx = CreateEveThreadCtx(t, aft->llmnrlog_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 SCLLMNRLogThreadDeinit(ThreadVars *t, void *data)
|
||||
{
|
||||
SCLLMNRLogThread *aft = (SCLLMNRLogThread *)data;
|
||||
if (aft == NULL) {
|
||||
return TM_ECODE_OK;
|
||||
}
|
||||
FreeEveThreadCtx(aft->ctx);
|
||||
|
||||
/* clear memory */
|
||||
memset(aft, 0, sizeof(SCLLMNRLogThread));
|
||||
|
||||
SCFree(aft);
|
||||
return TM_ECODE_OK;
|
||||
}
|
||||
|
||||
static void SCLLMNRLogDeInitCtxSub(OutputCtx *output_ctx)
|
||||
{
|
||||
SCLogDebug("cleaning up sub output_ctx %p", output_ctx);
|
||||
SCLLMNRLogFileCtx *llmnrlog_ctx = (SCLLMNRLogFileCtx *)output_ctx->data;
|
||||
SCFree(llmnrlog_ctx);
|
||||
SCFree(output_ctx);
|
||||
}
|
||||
|
||||
static OutputInitResult JsonLLMNRLogInitCtxSub(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;
|
||||
|
||||
SCLLMNRLogFileCtx *llmnrlog_ctx = SCCalloc(1, sizeof(SCLLMNRLogFileCtx));
|
||||
if (unlikely(llmnrlog_ctx == NULL)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
llmnrlog_ctx->eve_ctx = ojc;
|
||||
llmnrlog_ctx->flags = ~0ULL;
|
||||
|
||||
OutputCtx *output_ctx = SCCalloc(1, sizeof(OutputCtx));
|
||||
if (unlikely(output_ctx == NULL)) {
|
||||
SCFree(llmnrlog_ctx);
|
||||
return result;
|
||||
}
|
||||
|
||||
output_ctx->data = llmnrlog_ctx;
|
||||
output_ctx->DeInit = SCLLMNRLogDeInitCtxSub;
|
||||
|
||||
SCLogDebug("LLMNR log sub-module initialized");
|
||||
|
||||
SCAppLayerParserRegisterLogger(IPPROTO_UDP, ALPROTO_LLMNR);
|
||||
SCAppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_LLMNR);
|
||||
|
||||
result.ctx = output_ctx;
|
||||
result.ok = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
void JsonLLMNRLogRegister(void)
|
||||
{
|
||||
OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonLLMNRLog", "eve-log.llmnr",
|
||||
JsonLLMNRLogInitCtxSub, ALPROTO_LLMNR, JsonLLMNRLogger, SCLLMNRLogThreadInit,
|
||||
SCLLMNRLogThreadDeinit);
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/* 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>
|
||||
|
||||
#ifndef SURICATA_OUTPUT_JSON_LLMNR_H
|
||||
#define SURICATA_OUTPUT_JSON_LLMNR_H
|
||||
|
||||
void JsonLLMNRLogRegister(void);
|
||||
bool AlertJsonLLMNR(void *txptr, SCJsonBuilder *js);
|
||||
|
||||
#endif /* SURICATA_OUTPUT_JSON_LLMNR_H */
|
||||
Loading…
Reference in New Issue