From 1d0b4f198533a2d0ea60a6a286c198a8823ff33c Mon Sep 17 00:00:00 2001 From: Jeff Lucovsky Date: Sat, 10 Oct 2026 10:39:45 -0400 Subject: [PATCH] anomaly/eve: include unknown ethertype in events Add the ethertype the decoder could not handle to the anomaly records for a packet with the decoder.ethernet.unknown_ethertype event, as a top-level "unknown_ether_type" field. That includes the decoder.vlan.unknown_type, decoder.etag.unknown_type and decoder.vntag.unknown_type records the tag decoders raise for the same ethertype. The anomaly object held only the event type and name. The only ethertype in the record was "ether.ether_type", logged when eve "ethernet" is enabled and read from the last ethernet header decoded, so when the unknown ethertype followed a VLAN, 802.1ah, E-Tag or VN-Tag header the record showed the type that announced that header (0x8100 for VLAN) and never the type that raised the event. Look the value up when the event is logged, using the new DecodeGetUnknownEthertype(). The lookup starts at the type field of the last ethernet header decoded. Without an ethernet header it starts in the VLAN header that a GRE VLAN tunnel packet begins with, or at the protocol field of the SLL, SLL2 or Cisco HDLC header. It then steps over VLAN, 802.1ah, E-Tag and VN-Tag headers as the decoder does, and the first type that is none of those is the one the decoder rejected. Move the 802.1ah header struct to decode-vlan.h so the lookup can use its layout, note in DecodeNetworkLayer() that both lists of tag headers must match, and add unit tests for untagged, QinQ, 802.1ah, SLL, SLL2, Cisco HDLC, DCE and truncated frames. Issue: 7849 Issue: 8142 --- etc/schema.json | 4 ++ src/decode-ethernet.c | 145 +++++++++++++++++++++++++++++++++++++- src/decode-vlan.c | 11 +-- src/decode-vlan.h | 11 ++- src/decode.c | 94 +++++++++++++++++++++++- src/decode.h | 6 +- src/output-json-anomaly.c | 12 +++- 7 files changed, 268 insertions(+), 15 deletions(-) diff --git a/etc/schema.json b/etc/schema.json index ed0f01107b..5234f8295a 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -9510,6 +9510,10 @@ "tx_id": { "type": "integer" }, + "unknown_ether_type": { + "type": "integer", + "description": "The ethertype the decoder could not handle, in anomaly records for packets with the decoder.ethernet.unknown_ethertype event" + }, "verdict": { "$ref": "#/$defs/verdict_type" }, diff --git a/src/decode-ethernet.c b/src/decode-ethernet.c index 8ada77382a..31b6ae8c5e 100644 --- a/src/decode-ethernet.c +++ b/src/decode-ethernet.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2007-2021 Open Information Security Foundation +/* Copyright (C) 2007-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 @@ -166,6 +166,136 @@ static int DecodeEthernetTestDceNextTooSmall(void) PASS; } +/** + * \brief decode a frame with an unhandled ethertype and check the + * ethertype DecodeGetUnknownEthertype() reports for it + */ +static int DecodeEthernetUnknownEthertypeCheck( + int datalink, const uint8_t *raw, uint32_t len, uint16_t expected) +{ + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&dtv, 0, sizeof(DecodeThreadVars)); + memset(&tv, 0, sizeof(ThreadVars)); + + FAIL_IF(PacketCopyData(p, raw, len) != 0); + p->datalink = datalink; + DecodeLinkLayer(&tv, &dtv, datalink, p, GET_PKT_DATA(p), GET_PKT_LEN(p)); + + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, ETHERNET_UNKNOWN_ETHERTYPE)); + uint16_t ethertype = 0; + FAIL_IF_NOT(DecodeGetUnknownEthertype(p, ðertype)); + FAIL_IF(ethertype != expected); + + PacketFree(p); + PASS; +} + +/** \test untagged frame: the unknown ethertype is the ethernet type */ +static int DecodeEthernetTestUnknownEthertype(void) +{ + /* MACs, RARP ethertype, RARP payload */ + const uint8_t raw[] = { 0x00, 0x10, 0x94, 0x55, 0x00, 0x01, 0x00, 0x10, 0x94, 0x56, 0x00, 0x01, + 0x80, 0x35, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x03 }; + return DecodeEthernetUnknownEthertypeCheck(LINKTYPE_ETHERNET, raw, sizeof(raw), 0x8035); +} + +/** \test QinQ frame: the unknown ethertype follows the inner VLAN tag */ +static int DecodeEthernetTestUnknownEthertypeQinQ(void) +{ + /* MACs, 802.1ad, outer tag (VID 100, next 802.1Q), inner tag (VID 200, + * next RARP), RARP payload */ + const uint8_t raw[] = { 0x00, 0x10, 0x94, 0x55, 0x00, 0x01, 0x00, 0x10, 0x94, 0x56, 0x00, 0x01, + 0x88, 0xa8, 0x00, 0x64, 0x81, 0x00, 0x00, 0xc8, 0x80, 0x35, 0x00, 0x01, 0x08, 0x00, 0x06, + 0x04, 0x00, 0x03 }; + return DecodeEthernetUnknownEthertypeCheck(LINKTYPE_ETHERNET, raw, sizeof(raw), 0x8035); +} + +/** \test 802.1ah frame: the unknown ethertype follows the customer MACs */ +static int DecodeEthernetTestUnknownEthertype8021ah(void) +{ + /* MACs, 802.1ah, flags, customer MACs, RARP ethertype, RARP payload */ + const uint8_t raw[] = { 0x00, 0x10, 0x94, 0x55, 0x00, 0x01, 0x00, 0x10, 0x94, 0x56, 0x00, 0x01, + 0x88, 0xe7, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10, 0x94, 0x57, 0x00, 0x01, 0x00, 0x10, 0x94, + 0x58, 0x00, 0x01, 0x80, 0x35, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x03 }; + return DecodeEthernetUnknownEthertypeCheck(LINKTYPE_ETHERNET, raw, sizeof(raw), 0x8035); +} + +/** \test SLL frame without an ethernet header: the unknown ethertype + * follows a VLAN tag after the SLL protocol field */ +static int DecodeEthernetTestUnknownEthertypeSll(void) +{ + /* SLL packet type, ARPHRD type, address length, address, protocol 802.1Q, + * tag (VID 100, next RARP), RARP payload */ + const uint8_t raw[] = { 0x00, 0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x10, 0x94, 0x55, 0x00, 0x01, + 0x00, 0x00, 0x81, 0x00, 0x00, 0x64, 0x80, 0x35, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, + 0x03 }; + return DecodeEthernetUnknownEthertypeCheck(LINKTYPE_LINUX_SLL, raw, sizeof(raw), 0x8035); +} + +/** \test SLL2 frame: the SLL2 protocol is the first field of its header, + * so the VLAN tag starts after the whole header, not after that field */ +static int DecodeEthernetTestUnknownEthertypeSll2(void) +{ + /* protocol 802.1Q, reserved, ifindex, ARPHRD type, packet type, address + * length, address, tag (VID 100, next RARP), RARP payload */ + const uint8_t raw[] = { 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x06, + 0x00, 0x10, 0x94, 0x55, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x80, 0x35, 0x00, 0x01, 0x08, + 0x00, 0x06, 0x04, 0x00, 0x03 }; + return DecodeEthernetUnknownEthertypeCheck(LINKTYPE_LINUX_SLL2, raw, sizeof(raw), 0x8035); +} + +/** \test Cisco HDLC frame: the unknown ethertype is the HDLC protocol */ +static int DecodeEthernetTestUnknownEthertypeCHDLC(void) +{ + /* address, control, RARP protocol, RARP payload */ + const uint8_t raw[] = { 0x0f, 0x00, 0x80, 0x35, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, + 0x03 }; + return DecodeEthernetUnknownEthertypeCheck(LINKTYPE_CISCO_HDLC, raw, sizeof(raw), 0x8035); +} + +/** \test DCE frame: the unknown ethertype is in the inner ethernet header, + * not the outer DCE ethertype */ +static int DecodeEthernetTestUnknownEthertypeDce(void) +{ + /* MACs, DCE, 2 DCE bytes, inner MACs, RARP ethertype, RARP payload */ + const uint8_t raw[] = { 0x00, 0x10, 0x94, 0x55, 0x00, 0x01, 0x00, 0x10, 0x94, 0x56, 0x00, 0x01, + 0x89, 0x03, 0x00, 0x00, 0x00, 0x10, 0x94, 0x55, 0x00, 0x01, 0x00, 0x10, 0x94, 0x56, 0x00, + 0x01, 0x80, 0x35, 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x03 }; + return DecodeEthernetUnknownEthertypeCheck(LINKTYPE_ETHERNET, raw, sizeof(raw), 0x8035); +} + +/** \test frame cut off inside a VLAN tag: no unknown ethertype event, and + * the lookup stops at the end of the packet instead of reading past it */ +static int DecodeEthernetTestUnknownEthertypeTruncated(void) +{ + /* macs, 802.1Q, first half of the tag */ + const uint8_t raw[] = { 0x00, 0x10, 0x94, 0x55, 0x00, 0x01, 0x00, 0x10, 0x94, 0x56, 0x00, 0x01, + 0x81, 0x00, 0x00, 0x64 }; + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&dtv, 0, sizeof(DecodeThreadVars)); + memset(&tv, 0, sizeof(ThreadVars)); + + FAIL_IF(PacketCopyData(p, raw, sizeof(raw)) != 0); + p->datalink = LINKTYPE_ETHERNET; + DecodeEthernet(&tv, &dtv, p, GET_PKT_DATA(p), GET_PKT_LEN(p)); + + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, VLAN_HEADER_TOO_SMALL)); + FAIL_IF(ENGINE_ISSET_EVENT(p, ETHERNET_UNKNOWN_ETHERTYPE)); + uint16_t ethertype = 0; + FAIL_IF(DecodeGetUnknownEthertype(p, ðertype)); + + PacketFree(p); + PASS; +} + #endif /* UNITTESTS */ @@ -181,6 +311,19 @@ void DecodeEthernetRegisterTests(void) DecodeEthernetTestDceNextTooSmall); UtRegisterTest("DecodeEthernetTestDceTooSmall", DecodeEthernetTestDceTooSmall); + UtRegisterTest("DecodeEthernetTestUnknownEthertype", DecodeEthernetTestUnknownEthertype); + UtRegisterTest( + "DecodeEthernetTestUnknownEthertypeQinQ", DecodeEthernetTestUnknownEthertypeQinQ); + UtRegisterTest( + "DecodeEthernetTestUnknownEthertype8021ah", DecodeEthernetTestUnknownEthertype8021ah); + UtRegisterTest("DecodeEthernetTestUnknownEthertypeSll", DecodeEthernetTestUnknownEthertypeSll); + UtRegisterTest( + "DecodeEthernetTestUnknownEthertypeSll2", DecodeEthernetTestUnknownEthertypeSll2); + UtRegisterTest( + "DecodeEthernetTestUnknownEthertypeCHDLC", DecodeEthernetTestUnknownEthertypeCHDLC); + UtRegisterTest("DecodeEthernetTestUnknownEthertypeDce", DecodeEthernetTestUnknownEthertypeDce); + UtRegisterTest("DecodeEthernetTestUnknownEthertypeTruncated", + DecodeEthernetTestUnknownEthertypeTruncated); #endif /* UNITTESTS */ } /** diff --git a/src/decode-vlan.c b/src/decode-vlan.c index beb3042909..b7ae31c922 100644 --- a/src/decode-vlan.c +++ b/src/decode-vlan.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2007-2022 Open Information Security Foundation +/* Copyright (C) 2007-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 @@ -94,15 +94,6 @@ int DecodeVLAN(ThreadVars *tv, DecodeThreadVars *dtv, Packet *p, return TM_ECODE_OK; } -typedef struct IEEE8021ahHdr_ { - uint32_t flags; - uint8_t c_destination[6]; - uint8_t c_source[6]; - uint16_t type; /**< next protocol */ -} __attribute__((__packed__)) IEEE8021ahHdr; - -#define IEEE8021AH_HEADER_LEN sizeof(IEEE8021ahHdr) - int DecodeIEEE8021ah(ThreadVars *tv, DecodeThreadVars *dtv, Packet *p, const uint8_t *pkt, uint32_t len) { diff --git a/src/decode-vlan.h b/src/decode-vlan.h index 586e210ae9..a14dc28ddd 100644 --- a/src/decode-vlan.h +++ b/src/decode-vlan.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2007-2022 Open Information Security Foundation +/* Copyright (C) 2007-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 @@ -45,6 +45,15 @@ typedef struct VLANHdr_ { /** VLAN header length */ #define VLAN_HEADER_LEN 4 +typedef struct IEEE8021ahHdr_ { + uint32_t flags; + uint8_t c_destination[6]; + uint8_t c_source[6]; + uint16_t type; /**< next protocol */ +} __attribute__((__packed__)) IEEE8021ahHdr; + +#define IEEE8021AH_HEADER_LEN sizeof(IEEE8021ahHdr) + void DecodeVLANRegisterTests(void); /** VLAN max encapsulation layer count/index */ diff --git a/src/decode.c b/src/decode.c index e6800d8006..300b9745b2 100644 --- a/src/decode.c +++ b/src/decode.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2007-2024 Open Information Security Foundation +/* Copyright (C) 2007-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 @@ -62,6 +62,11 @@ #include "decode-erspan.h" #include "decode-teredo.h" #include "decode-arp.h" +#include "decode-chdlc.h" +#include "decode-etag.h" +#include "decode-sll.h" +#include "decode-sll2.h" +#include "decode-vntag.h" #include "defrag-hash.h" @@ -1192,6 +1197,93 @@ inline void PcapPacketCntSet(Packet *p, uint64_t pcap_cnt) } } +/** \brief get the ethertype that stopped layer 2 decoding + * + * The decoder raises ETHERNET_UNKNOWN_ETHERTYPE and stops when it reads a + * type it has no decoder for. Find that type again in the raw packet. + * Start at the type field of the last ethernet header decoded. Without an + * ethernet header, start at the type field of the VLAN header that a GRE + * VLAN tunnel packet begins with, or at the protocol field of the SLL, SLL2 + * or Cisco HDLC header. Then step over VLAN, 802.1ah, E-Tag and VN-Tag + * headers as the decoder does, until reaching a type that is none of those. + * + * \param p packet carrying the ETHERNET_UNKNOWN_ETHERTYPE event + * \param ethertype set to the ethertype in host byte order + * \retval true if found, false otherwise + */ +bool DecodeGetUnknownEthertype(const Packet *p, uint16_t *ethertype) +{ + const uint8_t *pkt = GET_PKT_DATA(p); + const uint32_t len = GET_PKT_LEN(p); + /* type_off is where the type field is, next_off is where the header it + * announces starts. These are not adjacent in SLL2, where the protocol + * field comes first. */ + uint32_t type_off; + uint32_t next_off; + + if (PacketIsEthernet(p)) { + const uint8_t *ethh = (const uint8_t *)PacketGetEthernet(p); + if (ethh < pkt || ethh >= pkt + len) + return false; + type_off = (uint32_t)(ethh - pkt) + offsetof(EthernetHdr, eth_type); + next_off = (uint32_t)(ethh - pkt) + ETHERNET_HEADER_LEN; + } else if (PacketIsTunnelChild(p)) { + /* the only tunnel packet that reaches DecodeNetworkLayer() without + * an ethernet header is a GRE VLAN one (GRE protocol 0x8100), and + * its data starts with the VLAN header */ + type_off = offsetof(VLANHdr, protocol); + next_off = VLAN_HEADER_LEN; + } else { + switch (p->datalink) { + case LINKTYPE_LINUX_SLL: + type_off = offsetof(SllHdr, sll_protocol); + next_off = SLL_HEADER_LEN; + break; + case LINKTYPE_LINUX_SLL2: + type_off = offsetof(Sll2Hdr, sll_protocol); + next_off = SLL2_HEADER_LEN; + break; + case LINKTYPE_CISCO_HDLC: + type_off = offsetof(CHDLCHdr, protocol); + next_off = CHDLC_HEADER_LEN; + break; + default: + return false; + } + } + + /* every step moves both offsets forward, so the loop ends */ + while (len >= sizeof(uint16_t) && type_off <= len - sizeof(uint16_t)) { + uint16_t proto; + memcpy(&proto, pkt + type_off, sizeof(proto)); + proto = SCNtohs(proto); + switch (proto) { + case ETHERNET_TYPE_VLAN: + case ETHERNET_TYPE_8021AD: + case ETHERNET_TYPE_8021QINQ: + type_off = next_off + offsetof(VLANHdr, protocol); + next_off += VLAN_HEADER_LEN; + break; + case ETHERNET_TYPE_8021AH: + type_off = next_off + offsetof(IEEE8021ahHdr, type); + next_off += IEEE8021AH_HEADER_LEN; + break; + case ETHERNET_TYPE_ETAG: + type_off = next_off + offsetof(ETagHdr, protocol); + next_off += ETAG_HEADER_LEN; + break; + case ETHERNET_TYPE_VNTAG: + type_off = next_off + offsetof(VNTagHdr, protocol); + next_off += VNTAG_HEADER_LEN; + break; + default: + *ethertype = proto; + return true; + } + } + return false; +} + /** * @} */ diff --git a/src/decode.h b/src/decode.h index c7e8e31d98..0b2420d7ff 100644 --- a/src/decode.h +++ b/src/decode.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2007-2024 Open Information Security Foundation +/* Copyright (C) 2007-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 @@ -1496,6 +1496,9 @@ static inline void DecodeLinkLayer(ThreadVars *tv, DecodeThreadVars *dtv, static inline bool DecodeNetworkLayer(ThreadVars *tv, DecodeThreadVars *dtv, const uint16_t proto, Packet *p, const uint8_t *data, const uint32_t len) { + /* DecodeGetUnknownEthertype() steps over the same VLAN, 802.1ah, E-Tag + * and VN-Tag headers to find the type reported for the unknown + * ethertype event: a header type added here needs adding there too */ switch (proto) { case ETHERNET_TYPE_IP: { uint16_t ip_len = (len < USHRT_MAX) ? (uint16_t)len : (uint16_t)USHRT_MAX; @@ -1560,5 +1563,6 @@ static inline bool DecodeNetworkLayer(ThreadVars *tv, DecodeThreadVars *dtv, uint64_t PcapPacketCntGet(const Packet *p); void PcapPacketCntSet(Packet *p, uint64_t pcap_cnt); +bool DecodeGetUnknownEthertype(const Packet *p, uint16_t *ethertype); #endif /* SURICATA_DECODE_H */ diff --git a/src/output-json-anomaly.c b/src/output-json-anomaly.c index 3081baeaab..153d890d60 100644 --- a/src/output-json-anomaly.c +++ b/src/output-json-anomaly.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2019-2021 Open Information Security Foundation +/* Copyright (C) 2019-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 @@ -107,6 +107,12 @@ static int AnomalyDecodeEventJson(ThreadVars *tv, JsonAnomalyLogThread *aft, const bool log_stream = log_type & LOG_JSON_STREAM_TYPE; const bool log_decode = log_type & LOG_JSON_DECODE_TYPE; + /* decoding stopped at an ethertype with no decoder: log that ethertype + * in every record for the packet */ + uint16_t ethertype = 0; + const bool has_ethertype = ENGINE_ISSET_EVENT(p, ETHERNET_UNKNOWN_ETHERTYPE) && + DecodeGetUnknownEthertype(p, ðertype); + for (int i = 0; i < p->events.cnt; i++) { uint8_t event_code = p->events.events[i]; bool is_decode = EVENT_IS_DECODER_PACKET_ERROR(event_code); @@ -139,6 +145,10 @@ static int AnomalyDecodeEventJson(ThreadVars *tv, JsonAnomalyLogThread *aft, /* Close anomaly object. */ SCJbClose(js); + if (has_ethertype) { + SCJbSetUint(js, "unknown_ether_type", ethertype); + } + if (aft->json_output_ctx->flags & LOG_JSON_PACKETHDR) { EvePacket(p, js, GET_PKT_LEN(p) < 32 ? GET_PKT_LEN(p) : 32); }