diff --git a/doc/userguide/rules/index.rst b/doc/userguide/rules/index.rst index 04ea736c9d23..b8d21f0c5916 100644 --- a/doc/userguide/rules/index.rst +++ b/doc/userguide/rules/index.rst @@ -33,6 +33,7 @@ Suricata Rules base64-keywords sip-keywords sdp-keywords + sctp-keywords rfb-keywords mqtt-keywords ike-keywords diff --git a/doc/userguide/rules/sctp-keywords.rst b/doc/userguide/rules/sctp-keywords.rst new file mode 100644 index 000000000000..2220c4363f6e --- /dev/null +++ b/doc/userguide/rules/sctp-keywords.rst @@ -0,0 +1,150 @@ +.. role:: example-rule-emphasis + +SCTP Keywords +============= + +Suricata supports sticky buffers and keywords for matching on SCTP +packet headers, chunks, and metadata. + +Sticky buffers are expected to be followed by one or more +:doc:`payload-keywords`. + +sctp.hdr +-------- + +Sticky buffer to match on the raw SCTP header and all chunks. + +Example rule: + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP header match"; :example-rule-emphasis:`sctp.hdr; content:"|01|"; offset:8; depth:1;` sid:1; rev:1;) + +``sctp.hdr`` is a 'sticky buffer'. + +``sctp.hdr`` can be used as ``fast_pattern``. + +sctp.chunk_data +--------------- + +Sticky buffer to match on any SCTP DATA chunk user payload. + +When a packet contains DATA chunks, the packet payload (``p->payload``) +is set to the user data of the first DATA chunk. A bare ``content`` +match (without a sticky buffer) therefore inspects the first DATA +chunk's payload. Use ``sctp.chunk_data`` to inspect all DATA chunks +independently. + +Example rule: + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP DATA payload match"; :example-rule-emphasis:`sctp.chunk_data; content:"test";` sid:2; rev:1;) + +``sctp.chunk_data`` is a 'sticky buffer'. + +``sctp.chunk_data`` can be used as ``fast_pattern``. + +sctp.vtag +--------- + +Match on the SCTP verification tag field in the common header. + +sctp.vtag uses an :ref:`unsigned 32-bit integer `. + +Syntax:: + + sctp.vtag:[op] + +The verification tag can be matched exactly, or compared using the _op_ setting:: + + sctp.vtag:12345 # exactly 12345 + sctp.vtag:>0 # greater than 0 + sctp.vtag:100-200 # range 100 to 200 + +Example rule: + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP vtag match"; :example-rule-emphasis:`sctp.vtag:0;` sid:3; rev:1;) + +sctp.chunk_type +--------------- + +Match on the type of any SCTP chunk in the packet. + +sctp.chunk_type uses an :ref:`unsigned 8-bit integer `. + +Syntax:: + + sctp.chunk_type:[!] + sctp.chunk_type:[op] + +Values can be specified by name or by numeric value. The following +named chunk types are supported: + +================= ===== +Name Value +================= ===== +data 0 +init 1 +init_ack 2 +sack 3 +heartbeat 4 +hb_ack 5 +abort 6 +shutdown 7 +shutdown_ack 8 +error 9 +cookie_echo 10 +cookie_ack 11 +ecne 12 +cwr 13 +shutdown_complete 14 +forward_tsn 192 +================= ===== + +Named values are case-insensitive and can be negated with ``!``:: + + sctp.chunk_type:init # INIT chunk + sctp.chunk_type:init_ack # INIT ACK chunk + sctp.chunk_type:!data # any chunk that is not DATA + +Numeric values support comparison operators and ranges:: + + sctp.chunk_type:1 # INIT chunk (type 1) + sctp.chunk_type:0-4 # range 0 to 4 + +Example rules: + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP INIT chunk detected"; :example-rule-emphasis:`sctp.chunk_type:init;` sid:4; rev:1;) + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP INIT chunk detected"; :example-rule-emphasis:`sctp.chunk_type:1;` sid:5; rev:1;) + +sctp.chunk_cnt +-------------- + +Match on the number of SCTP chunks in the packet. + +sctp.chunk_cnt uses an :ref:`unsigned 8-bit integer `. + +Syntax:: + + sctp.chunk_cnt:[op] + +The chunk count can be matched exactly, or compared using the _op_ setting:: + + sctp.chunk_cnt:1 # exactly 1 chunk + sctp.chunk_cnt:>3 # more than 3 chunks + sctp.chunk_cnt:2-5 # range 2 to 5 + +Example rule: + +.. container:: example-rule + + alert sctp any any -> any any (msg:"SCTP packet with multiple chunks"; :example-rule-emphasis:`sctp.chunk_cnt:>1;` sid:5; rev:1;) + diff --git a/etc/schema.json b/etc/schema.json index c08ee1001156..c3ec340679b4 100644 --- a/etc/schema.json +++ b/etc/schema.json @@ -5641,6 +5641,59 @@ } } }, + "sctp": { + "type": "object", + "description": "SCTP protocol information", + "additionalProperties": false, + "properties": { + "chunk_cnt": { + "type": "integer", + "description": "Number of SCTP chunks in the packet", + "suricata": { + "keywords": [ + "sctp.chunk_cnt" + ] + } + }, + "chunk_types": { + "type": "array", + "description": "Array of SCTP chunk type names present in the packet", + "items": { + "type": "string" + }, + "suricata": { + "keywords": [ + "sctp.chunk_type" + ] + } + }, + "has_abort": { + "type": "boolean", + "description": "Whether the packet contains an ABORT chunk" + }, + "has_data": { + "type": "boolean", + "description": "Whether the packet contains a DATA chunk" + }, + "has_init": { + "type": "boolean", + "description": "Whether the packet contains an INIT chunk" + }, + "has_init_ack": { + "type": "boolean", + "description": "Whether the packet contains an INIT_ACK chunk" + }, + "vtag": { + "type": "integer", + "description": "SCTP verification tag", + "suricata": { + "keywords": [ + "sctp.vtag" + ] + } + } + } + }, "sip": { "type": "object", "additionalProperties": false, @@ -7459,8 +7512,37 @@ "type": "object", "additionalProperties": false, "properties": { + "chunk_len_invalid": { + "type": "integer", + "description": "SCTP chunk length < 4 or exceeds remaining packet" + }, + "chunk_too_small": { + "type": "integer", + "description": "Remaining data too small for SCTP chunk header" + }, + "data_with_zero_vtag": { + "type": "integer", + "description": "SCTP DATA chunk with verification tag == 0" + }, + "init_chunk_bundled": { + "type": "integer", + "description": "RFC 4960 sec 6.10 violation: INIT/INIT_ACK bundled with other chunks" + }, + "init_with_non_zero_vtag": { + "type": "integer", + "description": "SCTP INIT with verification tag != 0" + }, "pkt_too_small": { - "type": "integer" + "type": "integer", + "description": "SCTP packet smaller than minimum size" + }, + "too_many_chunks": { + "type": "integer", + "description": "More chunks than SCTP_MAX_TRACKED_CHUNKS" + }, + "too_many_data_chunks": { + "type": "integer", + "description": "More DATA chunks than SCTP_MAX_DATA_CHUNKS" } } }, @@ -8547,6 +8629,33 @@ } } }, + "sctp": { + "type": "object", + "description": "Statistics on SCTP chunk types", + "additionalProperties": false, + "properties": { + "abort": { + "type": "integer", + "description": "Number of SCTP packets with ABORT chunk" + }, + "data": { + "type": "integer", + "description": "Number of SCTP packets with DATA chunk" + }, + "init": { + "type": "integer", + "description": "Number of SCTP packets with INIT chunk" + }, + "init_ack": { + "type": "integer", + "description": "Number of SCTP packets with INIT_ACK chunk" + }, + "shutdown": { + "type": "integer", + "description": "Number of SCTP packets with SHUTDOWN chunk" + } + } + }, "stream": { "type": "object", "description": "Observational statistics on TCP stream events", diff --git a/rules/Makefile.am b/rules/Makefile.am index 1524c1df5692..785b25b26999 100644 --- a/rules/Makefile.am +++ b/rules/Makefile.am @@ -24,6 +24,7 @@ pgsql-events.rules \ pop3-events.rules \ quic-events.rules \ rfb-events.rules \ +sctp-events.rules \ smb-events.rules \ smtp-events.rules \ snmp-events.rules \ diff --git a/rules/README.md b/rules/README.md index 303802c22329..b4abaf6128d8 100644 --- a/rules/README.md +++ b/rules/README.md @@ -34,6 +34,7 @@ signature IDs. | POP3 | 2236000 | 2236999 | | LDAP | 2237000 | 2237999 | | SNMP | 2238000 | 2238999 | +| SCTP | 2239000 | 2239999 | | DNS | 2240000 | 2240999 | | PGSQL | 2241000 | 2241999 | | mDNS | 2242000 | 2242999 | diff --git a/rules/sctp-events.rules b/rules/sctp-events.rules new file mode 100644 index 000000000000..72ebebad7a93 --- /dev/null +++ b/rules/sctp-events.rules @@ -0,0 +1,13 @@ +# SCTP decoder event rules. +# SID's fall in the 2239000+ range. See rules/README.md + +alert sctp any any -> any any (msg:"SURICATA SCTP packet too small"; decode-event:sctp.pkt_too_small; classtype:protocol-command-decode; sid:2239001; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP chunk too small"; decode-event:sctp.chunk_too_small; classtype:protocol-command-decode; sid:2239002; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP chunk length invalid"; decode-event:sctp.chunk_len_invalid; classtype:protocol-command-decode; sid:2239003; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP INIT chunk bundled"; decode-event:sctp.init_chunk_bundled; classtype:protocol-command-decode; sid:2239004; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP INIT with non-zero vtag"; decode-event:sctp.init_with_non_zero_vtag; classtype:protocol-command-decode; sid:2239005; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP DATA with zero vtag"; decode-event:sctp.data_with_zero_vtag; classtype:protocol-command-decode; sid:2239006; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP too many chunks"; decode-event:sctp.too_many_chunks; classtype:protocol-command-decode; sid:2239007; rev:1;) +alert sctp any any -> any any (msg:"SURICATA SCTP too many data chunks"; decode-event:sctp.too_many_data_chunks; classtype:protocol-command-decode; sid:2239008; rev:1;) + +#next sid is 2239009 diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 45f1688d9157..10c65e66a2b7 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -136,6 +136,7 @@ pub mod lzma; pub mod util; pub mod ffi; pub mod feature; +pub mod sctp; pub mod sdp; pub mod ldap; pub mod flow; diff --git a/rust/src/sctp/detect.rs b/rust/src/sctp/detect.rs new file mode 100644 index 000000000000..c1067c5eb79b --- /dev/null +++ b/rust/src/sctp/detect.rs @@ -0,0 +1,146 @@ +/* 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. + */ + +// Author: Giuseppe Longo + +use crate::detect::uint::{detect_parse_uint_enum, DetectUintData}; + +use std::ffi::CStr; + +/// SCTP chunk types (RFC 4960 sec 3.2) +#[repr(u8)] +#[derive(EnumStringU8)] +pub enum SctpChunkType { + Data = 0x00, + Init = 0x01, + InitAck = 0x02, + Sack = 0x03, + Heartbeat = 0x04, + HbAck = 0x05, + Abort = 0x06, + Shutdown = 0x07, + ShutdownAck = 0x08, + Error = 0x09, + CookieEcho = 0x0A, + CookieAck = 0x0B, + Ecne = 0x0C, + Cwr = 0x0D, + ShutdownComplete = 0x0E, + ForwardTsn = 0xC0, +} + +#[no_mangle] +pub unsafe extern "C" fn SCSctpDetectChunkTypeParse( + ustr: *const std::os::raw::c_char, +) -> *mut DetectUintData { + let ft_name: &CStr = CStr::from_ptr(ustr); + if let Ok(s) = ft_name.to_str() { + if let Some(ctx) = detect_parse_uint_enum::(s) { + let boxed = Box::new(ctx); + return Box::into_raw(boxed) as *mut _; + } + } + return std::ptr::null_mut(); +} + +/// Returns the string name for a chunk type value, or NULL for unknown types. +#[no_mangle] +pub extern "C" fn SCSctpChunkTypeToString(val: u8) -> *const std::os::raw::c_char { + let s: &[u8] = match val { + 0x00 => b"data\0", + 0x01 => b"init\0", + 0x02 => b"init_ack\0", + 0x03 => b"sack\0", + 0x04 => b"heartbeat\0", + 0x05 => b"hb_ack\0", + 0x06 => b"abort\0", + 0x07 => b"shutdown\0", + 0x08 => b"shutdown_ack\0", + 0x09 => b"error\0", + 0x0A => b"cookie_echo\0", + 0x0B => b"cookie_ack\0", + 0x0C => b"ecne\0", + 0x0D => b"cwr\0", + 0x0E => b"shutdown_complete\0", + 0xC0 => b"forward_tsn\0", + _ => return std::ptr::null(), + }; + s.as_ptr() as *const std::os::raw::c_char +} + +#[cfg(test)] +mod test { + use super::*; + use crate::detect::uint::DetectUintMode; + + #[test] + fn parse_numeric() { + let ctx = detect_parse_uint_enum::("0").unwrap(); + assert_eq!(ctx.arg1, 0); + let ctx = detect_parse_uint_enum::("1").unwrap(); + assert_eq!(ctx.arg1, 1); + let ctx = detect_parse_uint_enum::("192").unwrap(); + assert_eq!(ctx.arg1, 0xC0); + } + + #[test] + fn parse_named() { + let ctx = detect_parse_uint_enum::("data").unwrap(); + assert_eq!(ctx.arg1, 0); + let ctx = detect_parse_uint_enum::("init").unwrap(); + assert_eq!(ctx.arg1, 1); + let ctx = detect_parse_uint_enum::("init_ack").unwrap(); + assert_eq!(ctx.arg1, 2); + let ctx = detect_parse_uint_enum::("sack").unwrap(); + assert_eq!(ctx.arg1, 3); + let ctx = detect_parse_uint_enum::("heartbeat").unwrap(); + assert_eq!(ctx.arg1, 4); + let ctx = detect_parse_uint_enum::("hb_ack").unwrap(); + assert_eq!(ctx.arg1, 5); + let ctx = detect_parse_uint_enum::("abort").unwrap(); + assert_eq!(ctx.arg1, 6); + let ctx = detect_parse_uint_enum::("shutdown").unwrap(); + assert_eq!(ctx.arg1, 7); + let ctx = detect_parse_uint_enum::("cookie_echo").unwrap(); + assert_eq!(ctx.arg1, 0x0A); + let ctx = detect_parse_uint_enum::("forward_tsn").unwrap(); + assert_eq!(ctx.arg1, 0xC0); + } + + #[test] + fn parse_case_insensitive() { + let ctx = detect_parse_uint_enum::("INIT").unwrap(); + assert_eq!(ctx.arg1, 1); + let ctx = detect_parse_uint_enum::("Init").unwrap(); + assert_eq!(ctx.arg1, 1); + let ctx = detect_parse_uint_enum::("INIT_ACK").unwrap(); + assert_eq!(ctx.arg1, 2); + } + + #[test] + fn parse_negation() { + let ctx = detect_parse_uint_enum::("!init").unwrap(); + assert_eq!(ctx.arg1, 1); + assert_eq!(ctx.mode, DetectUintMode::DetectUintModeNe); + } + + #[test] + fn parse_invalid() { + assert!(detect_parse_uint_enum::("foo").is_none()); + assert!(detect_parse_uint_enum::("").is_none()); + } +} diff --git a/rust/src/sctp/mod.rs b/rust/src/sctp/mod.rs new file mode 100644 index 000000000000..b772cdd0ebd0 --- /dev/null +++ b/rust/src/sctp/mod.rs @@ -0,0 +1,20 @@ +/* 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. + */ + +// Author: Giuseppe Longo + +pub mod detect; diff --git a/src/Makefile.am b/src/Makefile.am index 44ff227012d1..7d6d0ebf0c02 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -228,6 +228,11 @@ noinst_HEADERS = \ detect-id.h \ detect-igmphdr.h \ detect-igmp-type.h \ + detect-sctphdr.h \ + detect-sctp-chunk-cnt.h \ + detect-sctp-chunk-type.h \ + detect-sctp-chunk-data.h \ + detect-sctp-vtag.h \ detect-ipaddr.h \ detect-ipopts.h \ detect-ipproto.h \ @@ -804,6 +809,11 @@ libsuricata_c_a_SOURCES = \ detect-id.c \ detect-igmphdr.c \ detect-igmp-type.c \ + detect-sctphdr.c \ + detect-sctp-chunk-cnt.c \ + detect-sctp-chunk-type.c \ + detect-sctp-chunk-data.c \ + detect-sctp-vtag.c \ detect-ipaddr.c \ detect-ipopts.c \ detect-ipproto.c \ diff --git a/src/decode-events.c b/src/decode-events.c index 32ce23dcda6b..3f37713dd3be 100644 --- a/src/decode-events.c +++ b/src/decode-events.c @@ -467,6 +467,34 @@ const struct DecodeEvents_ DEvents[] = { "decoder.sctp.pkt_too_small", SCTP_PKT_TOO_SMALL, }, + { + "decoder.sctp.chunk_too_small", + SCTP_CHUNK_TOO_SMALL, + }, + { + "decoder.sctp.chunk_len_invalid", + SCTP_CHUNK_LEN_INVALID, + }, + { + "decoder.sctp.init_chunk_bundled", + SCTP_INIT_CHUNK_BUNDLED, + }, + { + "decoder.sctp.init_with_non_zero_vtag", + SCTP_INIT_WITH_NON_ZERO_VTAG, + }, + { + "decoder.sctp.data_with_zero_vtag", + SCTP_DATA_WITH_ZERO_VTAG, + }, + { + "decoder.sctp.too_many_chunks", + SCTP_TOO_MANY_CHUNKS, + }, + { + "decoder.sctp.too_many_data_chunks", + SCTP_TOO_MANY_DATA_CHUNKS, + }, /* ESP EVENTS */ { diff --git a/src/decode-events.h b/src/decode-events.h index dc958c9c45f8..45602235d58c 100644 --- a/src/decode-events.h +++ b/src/decode-events.h @@ -172,7 +172,14 @@ enum { LTNULL_UNSUPPORTED_TYPE, /**< pkt has a type that the decoder doesn't support */ /* SCTP EVENTS */ - SCTP_PKT_TOO_SMALL, /**< sctp packet smaller than minimum size */ + SCTP_PKT_TOO_SMALL, /**< sctp packet smaller than minimum size */ + SCTP_CHUNK_TOO_SMALL, /**< remaining data too small for chunk header */ + SCTP_CHUNK_LEN_INVALID, /**< chunk length < 4 or exceeds remaining packet */ + SCTP_INIT_CHUNK_BUNDLED, /**< RFC 4960 sec 6.10: INIT/INIT_ACK bundled with other chunks */ + SCTP_INIT_WITH_NON_ZERO_VTAG, /**< INIT with vtag != 0 */ + SCTP_DATA_WITH_ZERO_VTAG, /**< DATA chunk with vtag == 0 */ + SCTP_TOO_MANY_CHUNKS, /**< more chunks than SCTP_MAX_TRACKED_CHUNKS */ + SCTP_TOO_MANY_DATA_CHUNKS, /**< more DATA chunks than SCTP_MAX_DATA_CHUNKS */ /* ESP EVENTS */ ESP_PKT_TOO_SMALL, /**< esp packet smaller than minimum size */ diff --git a/src/decode-sctp.c b/src/decode-sctp.c index 48d62028431f..48df9e1b4c93 100644 --- a/src/decode-sctp.c +++ b/src/decode-sctp.c @@ -21,7 +21,6 @@ * @{ */ - /** * \file * @@ -41,6 +40,128 @@ #include "util-optimize.h" #include "flow.h" +/** + * \brief Parse SCTP chunks after the common header. + * + * Iterates over chunks, validates each chunk header, and populates + * SCTPVars with chunk metadata. + * + * \param p Packet to decode + * \param pkt Pointer to the start of chunk data (after 12-byte common header) + * \param len Length of chunk data remaining + * + * \retval 0 on success (even if some events were set) + * \retval -1 on fatal error (packet should be rejected) + */ +static int DecodeSCTPChunks(Packet *p, const uint8_t *pkt, uint16_t len) +{ + const SCTPHdr *sctph = PacketGetSCTP(p); + const uint32_t vtag = SCTP_GET_RAW_VTAG(sctph); + uint32_t offset = 0; + uint8_t chunk_cnt = 0; + uint8_t tracked_chunk_cnt = 0; + bool has_init = false; + bool has_init_ack = false; + bool has_data = false; + bool has_abort = false; + uint8_t data_chunk_cnt = 0; + int ret = 0; + + while (offset < len) { + /* need at least a chunk header */ + if (len - offset < SCTP_CHUNK_HDR_LEN) { + ENGINE_SET_INVALID_EVENT(p, SCTP_CHUNK_TOO_SMALL); + ret = -1; + break; + } + + SCTPChunkHdr chunk; + memcpy(&chunk, pkt + offset, sizeof(chunk)); + const uint16_t chunk_len = SCNtohs(chunk.length); + + /* RFC 4960 sec 3.2: chunk length includes the header and must be >= 4 */ + if (chunk_len < SCTP_CHUNK_HDR_LEN) { + ENGINE_SET_INVALID_EVENT(p, SCTP_CHUNK_LEN_INVALID); + ret = -1; + break; + } + + /* chunk must not extend beyond available data */ + if (chunk_len > (len - offset)) { + ENGINE_SET_INVALID_EVENT(p, SCTP_CHUNK_LEN_INVALID); + ret = -1; + break; + } + + if (chunk_cnt < SCTP_MAX_TRACKED_CHUNKS) { + p->l4.vars.sctp.chunk_types[chunk_cnt] = chunk.type; + tracked_chunk_cnt++; + } else if (chunk_cnt == SCTP_MAX_TRACKED_CHUNKS) { + ENGINE_SET_EVENT(p, SCTP_TOO_MANY_CHUNKS); + } + chunk_cnt++; + + switch (chunk.type) { + case SCTP_CHUNK_TYPE_INIT: + has_init = true; + /* RFC 4960 sec 8.5.1: INIT must have vtag == 0 */ + if (vtag != 0) { + ENGINE_SET_EVENT(p, SCTP_INIT_WITH_NON_ZERO_VTAG); + } + break; + case SCTP_CHUNK_TYPE_INIT_ACK: + has_init_ack = true; + break; + case SCTP_CHUNK_TYPE_DATA: + if (data_chunk_cnt < SCTP_MAX_DATA_CHUNKS && chunk_len >= SCTP_DATA_CHUNK_HDR_LEN) { + p->l4.vars.sctp.data_offsets[data_chunk_cnt] = + (uint16_t)(SCTP_HEADER_LEN + offset + SCTP_DATA_CHUNK_HDR_LEN); + p->l4.vars.sctp.data_lens[data_chunk_cnt] = chunk_len - SCTP_DATA_CHUNK_HDR_LEN; + data_chunk_cnt++; + } else if (data_chunk_cnt == SCTP_MAX_DATA_CHUNKS && + chunk_len >= SCTP_DATA_CHUNK_HDR_LEN) { + ENGINE_SET_EVENT(p, SCTP_TOO_MANY_DATA_CHUNKS); + } + has_data = true; + /* DATA chunks must not have vtag == 0 */ + if (vtag == 0) { + ENGINE_SET_EVENT(p, SCTP_DATA_WITH_ZERO_VTAG); + } + break; + case SCTP_CHUNK_TYPE_ABORT: + has_abort = true; + break; + default: + break; + } + + /* advance to next chunk: padded to 4-byte boundary (RFC 4960 sec 3.2) */ + uint32_t padded_len = (chunk_len + 3) & ~3U; + /* guard against infinite loop with zero-padding overshoot */ + if (padded_len < SCTP_CHUNK_HDR_LEN) { + padded_len = SCTP_CHUNK_HDR_LEN; + } + offset += padded_len; + DEBUG_VALIDATE_BUG_ON(offset > (uint32_t)len + 3); + } + + /* RFC 4960 sec 6.10: INIT/INIT_ACK must be the only chunk in the packet */ + if ((has_init || has_init_ack) && chunk_cnt > 1) { + ENGINE_SET_EVENT(p, SCTP_INIT_CHUNK_BUNDLED); + } + + p->l4.vars.sctp.hlen = (uint16_t)(SCTP_HEADER_LEN + MIN(offset, len)); + p->l4.vars.sctp.chunk_cnt = chunk_cnt; + p->l4.vars.sctp.tracked_chunk_cnt = tracked_chunk_cnt; + p->l4.vars.sctp.data_chunk_cnt = data_chunk_cnt; + p->l4.vars.sctp.has_init = has_init; + p->l4.vars.sctp.has_init_ack = has_init_ack; + p->l4.vars.sctp.has_data = has_data; + p->l4.vars.sctp.has_abort = has_abort; + + return ret; +} + static int DecodeSCTPPacket(ThreadVars *tv, Packet *p, const uint8_t *pkt, uint16_t len) { DEBUG_VALIDATE_BUG_ON(pkt == NULL); @@ -50,31 +171,531 @@ static int DecodeSCTPPacket(ThreadVars *tv, Packet *p, const uint8_t *pkt, uint1 return -1; } - SCTPHdr *sctph = PacketSetSCTP(p, pkt); - p->sp = SCNtohs(sctph->sh_sport); - p->dp = SCNtohs(sctph->sh_dport); - p->payload = (uint8_t *)pkt + sizeof(SCTPHdr); - p->payload_len = len - sizeof(SCTPHdr); + const SCTPHdr *sctph = PacketSetSCTP(p, pkt); + + p->sp = SCTP_GET_RAW_SRC_PORT(sctph); + p->dp = SCTP_GET_RAW_DST_PORT(sctph); + p->payload = (uint8_t *)pkt + SCTP_HEADER_LEN; + p->payload_len = len - SCTP_HEADER_LEN; p->proto = IPPROTO_SCTP; + + if (p->payload_len > 0) { + if (DecodeSCTPChunks(p, p->payload, p->payload_len) < 0) { + p->payload_len = 0; + return -1; + } + + if (p->l4.vars.sctp.data_chunk_cnt > 0) { + /* Point p->payload at the user data of the first DATA chunk, + * past the SCTP common header and the 16-byte DATA chunk + * header. This is what content will inspect. */ + p->payload = (uint8_t *)pkt + p->l4.vars.sctp.data_offsets[0]; + p->payload_len = p->l4.vars.sctp.data_lens[0]; + } else { + p->payload_len = 0; + } + } else { + p->l4.vars.sctp.hlen = SCTP_HEADER_LEN; + } + return 0; } -int DecodeSCTP(ThreadVars *tv, DecodeThreadVars *dtv, Packet *p, - const uint8_t *pkt, uint16_t len) +int DecodeSCTP(ThreadVars *tv, DecodeThreadVars *dtv, Packet *p, const uint8_t *pkt, uint16_t len) { StatsCounterIncr(&tv->stats, dtv->counter_sctp); - if (unlikely(DecodeSCTPPacket(tv, p,pkt,len) < 0)) { + if (unlikely(DecodeSCTPPacket(tv, p, pkt, len) < 0)) { PacketClearL4(p); return TM_ECODE_FAILED; } SCLogDebug("SCTP sp: %u -> dp: %u", p->sp, p->dp); + if (p->l4.vars.sctp.has_init) { + StatsCounterIncr(&tv->stats, dtv->counter_sctp_init); + } + if (p->l4.vars.sctp.has_init_ack) { + StatsCounterIncr(&tv->stats, dtv->counter_sctp_init_ack); + } + if (p->l4.vars.sctp.has_data) { + StatsCounterIncr(&tv->stats, dtv->counter_sctp_data); + } + if (p->l4.vars.sctp.has_abort) { + StatsCounterIncr(&tv->stats, dtv->counter_sctp_abort); + } + for (uint8_t i = 0; i < p->l4.vars.sctp.tracked_chunk_cnt; i++) { + if (p->l4.vars.sctp.chunk_types[i] == SCTP_CHUNK_TYPE_SHUTDOWN) { + StatsCounterIncr(&tv->stats, dtv->counter_sctp_shutdown); + break; + } + } + FlowSetupPacket(p); return TM_ECODE_OK; } + +#ifdef UNITTESTS + +/** \test Valid SCTP packet with INIT chunk */ +static int SCTPDecodeValidInitTest01(void) +{ + /* SCTP common header: sport=1234 dport=80 vtag=0 checksum=0 + * followed by INIT chunk: type=0x01 flags=0x00 length=20 + * with 16 bytes of INIT-specific data (initiate_tag, a_rwnd, etc.) */ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x00, /* vtag=0 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + 0x01, 0x00, 0x00, 0x14, /* chunk: INIT, flags=0, len=20 */ + 0x00, 0x00, 0x00, 0x01, /* initiate_tag=1 */ + 0x00, 0x01, 0x00, 0x00, /* a_rwnd=65536 */ + 0x00, 0x01, 0x00, 0x01, /* num_outbound=1, num_inbound=1 */ + 0x00, 0x00, 0x00, 0x01, /* initial_tsn=1 */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + + FAIL_IF(p->sp != 1234); + FAIL_IF(p->dp != 80); + FAIL_IF(p->l4.vars.sctp.chunk_types[0] != SCTP_CHUNK_TYPE_INIT); + FAIL_IF(p->l4.vars.sctp.chunk_cnt != 1); + FAIL_IF(!p->l4.vars.sctp.has_init); + FAIL_IF(p->l4.vars.sctp.has_data); + FAIL_IF(p->l4.vars.sctp.has_abort); + + /* no protocol violation events expected */ + FAIL_IF(ENGINE_ISSET_EVENT(p, SCTP_INIT_WITH_NON_ZERO_VTAG)); + FAIL_IF(ENGINE_ISSET_EVENT(p, SCTP_INIT_CHUNK_BUNDLED)); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Packet too small (< 12 bytes) */ +static int SCTPDecodePktTooSmallTest02(void) +{ + uint8_t raw_sctp[] = { 0x04, 0xd2, 0x00, 0x50, 0x00, 0x00 }; + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + int ret = DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF(ret != TM_ECODE_FAILED); + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, SCTP_PKT_TOO_SMALL)); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Chunk too small - header + 2 bytes garbage (not enough for chunk header) */ +static int SCTPDecodeChunkTooSmallTest03(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x01, /* vtag=1 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + 0x01, 0x00, /* only 2 bytes of chunk data */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + int ret = DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF(ret != TM_ECODE_FAILED); + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, SCTP_CHUNK_TOO_SMALL)); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Invalid chunk length (chunk_len < 4) */ +static int SCTPDecodeChunkLenInvalidTest04(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x01, /* vtag=1 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + 0x00, 0x00, 0x00, 0x02, /* chunk: DATA, flags=0, len=2 (invalid < 4) */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + int ret = DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF(ret != TM_ECODE_FAILED); + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, SCTP_CHUNK_LEN_INVALID)); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test INIT with non-zero verification tag */ +static int SCTPDecodeInitNonZeroVtagTest05(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x42, /* vtag=0x42 (non-zero, invalid for INIT) */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + 0x01, 0x00, 0x00, 0x14, /* chunk: INIT, flags=0, len=20 */ + 0x00, 0x00, 0x00, 0x01, /* initiate_tag=1 */ + 0x00, 0x01, 0x00, 0x00, /* a_rwnd=65536 */ + 0x00, 0x01, 0x00, 0x01, /* num_outbound=1, num_inbound=1 */ + 0x00, 0x00, 0x00, 0x01, /* initial_tsn=1 */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, SCTP_INIT_WITH_NON_ZERO_VTAG)); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Multiple chunks: DATA + SACK */ +static int SCTPDecodeMultiChunkTest06(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x01, /* vtag=1 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + /* DATA chunk: type=0x00, flags=0x03, len=20 */ + 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, /* TSN=0 */ + 0x00, 0x01, 0x00, 0x00, /* stream_id=1, stream_seq=0 */ + 0x00, 0x00, 0x00, 0x00, /* PPID=0 */ + 0x41, 0x42, 0x43, 0x44, /* data="ABCD" */ + /* SACK chunk: type=0x03, flags=0x00, len=16 */ + 0x03, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, /* cumulative_tsn_ack=1 */ + 0x00, 0x01, 0x00, 0x00, /* a_rwnd=65536 */ + 0x00, 0x00, 0x00, 0x00, /* num_gap_blocks=0, num_dup_tsns=0 */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + + FAIL_IF(p->l4.vars.sctp.chunk_cnt != 2); + FAIL_IF(p->l4.vars.sctp.chunk_types[0] != SCTP_CHUNK_TYPE_DATA); + FAIL_IF(p->l4.vars.sctp.chunk_types[1] != SCTP_CHUNK_TYPE_SACK); + FAIL_IF(!p->l4.vars.sctp.has_data); + FAIL_IF(p->l4.vars.sctp.has_init); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test INIT bundled with another chunk (violates RFC 4960 sec 6.10) */ +static int SCTPDecodeInitNotAloneTest07(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x00, /* vtag=0 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + /* INIT chunk: type=0x01, flags=0, len=20 */ + 0x01, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, /* initiate_tag=1 */ + 0x00, 0x01, 0x00, 0x00, /* a_rwnd=65536 */ + 0x00, 0x01, 0x00, 0x01, /* num_outbound=1, num_inbound=1 */ + 0x00, 0x00, 0x00, 0x01, /* initial_tsn=1 */ + /* DATA chunk: type=0x00, flags=0, len=16 (bundled illegally) */ + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, SCTP_INIT_CHUNK_BUNDLED)); + FAIL_IF(p->l4.vars.sctp.chunk_cnt != 2); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test DATA chunk payload extraction - p->payload points to user data */ +static int SCTPDecodeDataPayloadTest08(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x01, /* vtag=1 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + /* DATA chunk: type=0x00, flags=0x03, len=20 (16 hdr + 4 data) */ + 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, /* TSN=1 */ + 0x00, 0x01, 0x00, 0x00, /* stream_id=1, stream_seq=0 */ + 0x00, 0x00, 0x00, 0x00, /* PPID=0 */ + 0x41, 0x42, 0x43, 0x44, /* data="ABCD" */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + + FAIL_IF(p->payload_len != 4); + FAIL_IF(memcmp(p->payload, "ABCD", 4) != 0); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test INIT-only packet - payload_len must be 0 (no application data) */ +static int SCTPDecodeNoDataPayloadTest09(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x00, /* vtag=0 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + 0x01, 0x00, 0x00, 0x14, /* chunk: INIT, flags=0, len=20 */ + 0x00, 0x00, 0x00, 0x01, /* initiate_tag=1 */ + 0x00, 0x01, 0x00, 0x00, /* a_rwnd=65536 */ + 0x00, 0x01, 0x00, 0x01, /* num_outbound=1, num_inbound=1 */ + 0x00, 0x00, 0x00, 0x01, /* initial_tsn=1 */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + + FAIL_IF(p->payload_len != 0); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Verify data_offset and data_len in SCTPVars */ +static int SCTPDecodeDataOffsetTest10(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x01, /* vtag=1 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + /* DATA chunk: type=0x00, flags=0x03, len=20 (16 hdr + 4 data) */ + 0x00, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, /* TSN=1 */ + 0x00, 0x01, 0x00, 0x00, /* stream_id=1, stream_seq=0 */ + 0x00, 0x00, 0x00, 0x00, /* PPID=0 */ + 0x41, 0x42, 0x43, 0x44, /* data="ABCD" */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + + /* data_offsets[0] = SCTP_HEADER_LEN(12) + chunk_offset(0) + DATA_CHUNK_HDR_LEN(16) = 28 */ + FAIL_IF(p->l4.vars.sctp.data_chunk_cnt != 1); + FAIL_IF(p->l4.vars.sctp.data_offsets[0] != 28); + FAIL_IF(p->l4.vars.sctp.data_lens[0] != 4); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test Two DATA chunks - both tracked, p->payload is first DATA's user data */ +static int SCTPDecodeMultiDataTest11(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x01, /* vtag=1 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + /* DATA chunk 1: type=0x00, flags=0x02, len=20 (16 hdr + 4 data) */ + 0x00, 0x02, 0x00, 0x14, 0x00, 0x00, 0x00, 0x01, /* TSN=1 */ + 0x00, 0x01, 0x00, 0x00, /* stream_id=1, stream_seq=0 */ + 0x00, 0x00, 0x00, 0x00, /* PPID=0 */ + 0x41, 0x42, 0x43, 0x44, /* data="ABCD" */ + /* DATA chunk 2: type=0x00, flags=0x01, len=20 (16 hdr + 4 data) */ + 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x00, 0x02, /* TSN=2 */ + 0x00, 0x01, 0x00, 0x01, /* stream_id=1, stream_seq=1 */ + 0x00, 0x00, 0x00, 0x00, /* PPID=0 */ + 0x45, 0x46, 0x47, 0x48, /* data="EFGH" */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + + FAIL_IF(p->l4.vars.sctp.chunk_cnt != 2); + FAIL_IF(p->l4.vars.sctp.data_chunk_cnt != 2); + /* first DATA chunk: offset = 12 + 0 + 16 = 28, len = 4 */ + FAIL_IF(p->l4.vars.sctp.data_offsets[0] != 28); + FAIL_IF(p->l4.vars.sctp.data_lens[0] != 4); + /* second DATA chunk: offset = 12 + 20 + 16 = 48, len = 4 */ + FAIL_IF(p->l4.vars.sctp.data_offsets[1] != 48); + FAIL_IF(p->l4.vars.sctp.data_lens[1] != 4); + /* p->payload still points to first DATA's user data */ + FAIL_IF(p->payload_len != 4); + FAIL_IF(memcmp(p->payload, "ABCD", 4) != 0); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +/** \test DATA chunk with chunk_len < 16 - malformed, payload_len must be 0 */ +static int SCTPDecodeSmallDataChunkTest12(void) +{ + // clang-format off + uint8_t raw_sctp[] = { + 0x04, 0xd2, 0x00, 0x50, /* sport=1234, dport=80 */ + 0x00, 0x00, 0x00, 0x01, /* vtag=1 */ + 0x00, 0x00, 0x00, 0x00, /* checksum=0 */ + /* DATA chunk: type=0x00, flags=0x00, len=8 (< 16, malformed) */ + 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x00, /* 4 bytes of value (not enough for DATA header) */ + }; + // clang-format on + + Packet *p = PacketGetFromAlloc(); + FAIL_IF_NULL(p); + ThreadVars tv; + DecodeThreadVars dtv; + + memset(&tv, 0, sizeof(ThreadVars)); + memset(&dtv, 0, sizeof(DecodeThreadVars)); + + FlowInitConfig(FLOW_QUIET); + DecodeSCTP(&tv, &dtv, p, raw_sctp, sizeof(raw_sctp)); + FAIL_IF_NOT(PacketIsSCTP(p)); + + FAIL_IF(p->l4.vars.sctp.data_chunk_cnt != 0); + FAIL_IF(p->payload_len != 0); + + PacketFree(p); + FlowShutdown(); + PASS; +} + +#endif /* UNITTESTS */ + +void DecodeSCTPRegisterTests(void) +{ +#ifdef UNITTESTS + UtRegisterTest("SCTPDecodeValidInitTest01", SCTPDecodeValidInitTest01); + UtRegisterTest("SCTPDecodePktTooSmallTest02", SCTPDecodePktTooSmallTest02); + UtRegisterTest("SCTPDecodeChunkTooSmallTest03", SCTPDecodeChunkTooSmallTest03); + UtRegisterTest("SCTPDecodeChunkLenInvalidTest04", SCTPDecodeChunkLenInvalidTest04); + UtRegisterTest("SCTPDecodeInitNonZeroVtagTest05", SCTPDecodeInitNonZeroVtagTest05); + UtRegisterTest("SCTPDecodeMultiChunkTest06", SCTPDecodeMultiChunkTest06); + UtRegisterTest("SCTPDecodeInitNotAloneTest07", SCTPDecodeInitNotAloneTest07); + UtRegisterTest("SCTPDecodeDataPayloadTest08", SCTPDecodeDataPayloadTest08); + UtRegisterTest("SCTPDecodeNoDataPayloadTest09", SCTPDecodeNoDataPayloadTest09); + UtRegisterTest("SCTPDecodeDataOffsetTest10", SCTPDecodeDataOffsetTest10); + UtRegisterTest("SCTPDecodeMultiDataTest11", SCTPDecodeMultiDataTest11); + UtRegisterTest("SCTPDecodeSmallDataChunkTest12", SCTPDecodeSmallDataChunkTest12); +#endif +} /** * @} */ diff --git a/src/decode-sctp.h b/src/decode-sctp.h index f83a4434ef05..c6e6b45323d5 100644 --- a/src/decode-sctp.h +++ b/src/decode-sctp.h @@ -25,16 +25,75 @@ #define SURICATA_DECODE_SCTP_H /** size of the packet header without any chunk headers */ -#define SCTP_HEADER_LEN 12 - -typedef struct SCTPHdr_ -{ - uint16_t sh_sport; /* source port */ - uint16_t sh_dport; /* destination port */ - uint32_t sh_vtag; /* verification tag, defined per flow */ - uint32_t sh_sum; /* checksum, computed via crc32 */ +#define SCTP_HEADER_LEN 12 + +/** size of a chunk header (type + flags + length) */ +#define SCTP_CHUNK_HDR_LEN 4 + +/** max number of chunks tracked per packet for detection/logging */ +/** value chosen to keep per-packet overhead low while still allowing + * some room to track chunks. + * SCTP has no hard limit on the number of chunks per packet. + * A packet can carry as many chunks as fit within the MTU, + * though in practice most packets contain only a few chunks. */ +#define SCTP_MAX_TRACKED_CHUNKS 16 + +/** max number of DATA chunk payloads tracked per packet */ +#define SCTP_MAX_DATA_CHUNKS 16 + +/** DATA chunk overhead before user data (chunk hdr + TSN + SID + SSN + PPID) */ +#define SCTP_DATA_CHUNK_HDR_LEN 16 + +/* SCTP chunk types (RFC 4960 sec 3.2) */ +#define SCTP_CHUNK_TYPE_DATA 0x00 +#define SCTP_CHUNK_TYPE_INIT 0x01 +#define SCTP_CHUNK_TYPE_INIT_ACK 0x02 +#define SCTP_CHUNK_TYPE_SACK 0x03 +#define SCTP_CHUNK_TYPE_HEARTBEAT 0x04 +#define SCTP_CHUNK_TYPE_HB_ACK 0x05 +#define SCTP_CHUNK_TYPE_ABORT 0x06 +#define SCTP_CHUNK_TYPE_SHUTDOWN 0x07 +#define SCTP_CHUNK_TYPE_SHUTDOWN_ACK 0x08 +#define SCTP_CHUNK_TYPE_ERROR 0x09 +#define SCTP_CHUNK_TYPE_COOKIE_ECHO 0x0A +#define SCTP_CHUNK_TYPE_COOKIE_ACK 0x0B +#define SCTP_CHUNK_TYPE_ECNE 0x0C +#define SCTP_CHUNK_TYPE_CWR 0x0D +#define SCTP_CHUNK_TYPE_SHUTDOWN_COMPLETE 0x0E +#define SCTP_CHUNK_TYPE_FORWARD_TSN 0xC0 + +typedef struct SCTPHdr_ { + uint16_t sh_sport; /* source port */ + uint16_t sh_dport; /* destination port */ + uint32_t sh_vtag; /* verification tag, defined per flow */ + uint32_t sh_sum; /* checksum, computed via crc32 */ } __attribute__((__packed__)) SCTPHdr; +typedef struct SCTPChunkHdr_ { + uint8_t type; + uint8_t flags; + uint16_t length; +} __attribute__((__packed__)) SCTPChunkHdr; + +typedef struct SCTPVars_ { + uint16_t hlen; /**< total header length (common header + chunks) */ + uint8_t chunk_cnt; /**< number of chunks parsed */ + uint8_t tracked_chunk_cnt; /**< number of chunks tracked (capped at SCTP_MAX_TRACKED_CHUNKS) */ + uint8_t chunk_types[SCTP_MAX_TRACKED_CHUNKS]; /**< types of first N chunks */ + uint8_t data_chunk_cnt; /**< number of DATA chunk payloads tracked */ + bool has_init : 1; + bool has_init_ack : 1; + bool has_data : 1; + bool has_abort : 1; + uint16_t data_offsets[SCTP_MAX_DATA_CHUNKS]; /**< offsets of DATA user data from L4 start */ + uint16_t data_lens[SCTP_MAX_DATA_CHUNKS]; /**< lengths of DATA user data */ +} SCTPVars; + +#define SCTP_GET_RAW_SRC_PORT(sctph) SCNtohs((sctph)->sh_sport) +#define SCTP_GET_RAW_DST_PORT(sctph) SCNtohs((sctph)->sh_dport) +#define SCTP_GET_RAW_VTAG(sctph) SCNtohl((sctph)->sh_vtag) +#define SCTP_GET_RAW_SUM(sctph) SCNtohl((sctph)->sh_sum) + void DecodeSCTPRegisterTests(void); #endif /* SURICATA_DECODE_SCTP_H */ diff --git a/src/decode.c b/src/decode.c index ce452930aa21..593f212f6fe5 100644 --- a/src/decode.c +++ b/src/decode.c @@ -656,6 +656,11 @@ void DecodeRegisterPerfCounters(DecodeThreadVars *dtv, ThreadVars *tv) dtv->counter_udp = StatsRegisterCounter("decoder.udp", &tv->stats); dtv->counter_sctp = StatsRegisterCounter("decoder.sctp", &tv->stats); + dtv->counter_sctp_init = StatsRegisterCounter("sctp.init", &tv->stats); + dtv->counter_sctp_init_ack = StatsRegisterCounter("sctp.init_ack", &tv->stats); + dtv->counter_sctp_data = StatsRegisterCounter("sctp.data", &tv->stats); + dtv->counter_sctp_abort = StatsRegisterCounter("sctp.abort", &tv->stats); + dtv->counter_sctp_shutdown = StatsRegisterCounter("sctp.shutdown", &tv->stats); dtv->counter_esp = StatsRegisterCounter("decoder.esp", &tv->stats); dtv->counter_icmpv4 = StatsRegisterCounter("decoder.icmpv4", &tv->stats); dtv->counter_icmpv6 = StatsRegisterCounter("decoder.icmpv6", &tv->stats); diff --git a/src/decode.h b/src/decode.h index 5b6e2a1220a6..b44906d06a5b 100644 --- a/src/decode.h +++ b/src/decode.h @@ -491,6 +491,7 @@ struct PacketL4 { ICMPV4Vars icmpv4; ICMPV6Vars icmpv6; IGMPVars igmp; + SCTPVars sctp; } vars; }; @@ -1025,6 +1026,11 @@ typedef struct DecodeThreadVars_ StatsCounterId counter_raw; StatsCounterId counter_null; StatsCounterId counter_sctp; + StatsCounterId counter_sctp_init; + StatsCounterId counter_sctp_init_ack; + StatsCounterId counter_sctp_data; + StatsCounterId counter_sctp_abort; + StatsCounterId counter_sctp_shutdown; StatsCounterId counter_esp; StatsCounterId counter_ppp; StatsCounterId counter_geneve; diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 1b441fc29675..b37cd61f8466 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -155,6 +155,11 @@ #include "detect-icmpv4hdr.h" #include "detect-igmphdr.h" #include "detect-igmp-type.h" +#include "detect-sctphdr.h" +#include "detect-sctp-chunk-type.h" +#include "detect-sctp-chunk-cnt.h" +#include "detect-sctp-vtag.h" +#include "detect-sctp-chunk-data.h" #include "detect-urilen.h" #include "detect-bsize.h" #include "detect-detection-filter.h" @@ -676,6 +681,11 @@ void SigTableSetup(void) DetectIcmpv4HdrRegister(); DetectIGMPHdrRegister(); DetectIGMPTypeRegister(); + DetectSCTPHdrRegister(); + DetectSCTPChunkTypeRegister(); + DetectSCTPChunkCntRegister(); + DetectSCTPVtagRegister(); + DetectSCTPChunkDataRegister(); DetectTlsRegister(); DetectTlsValidityRegister(); DetectTlsVersionRegister(); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 0538d998a930..822b56a6e407 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -51,6 +51,11 @@ enum DetectKeywordId { DETECT_ICMPV4HDR, DETECT_IGMPHDR, DETECT_IGMP_TYPE, + DETECT_SCTPHDR, + DETECT_SCTP_CHUNK_TYPE, + DETECT_SCTP_CHUNK_CNT, + DETECT_SCTP_VTAG, + DETECT_SCTP_CHUNK_DATA, DETECT_DSIZE, DETECT_FLOW, diff --git a/src/detect-sctp-chunk-cnt.c b/src/detect-sctp-chunk-cnt.c new file mode 100644 index 000000000000..cd54e6fda46f --- /dev/null +++ b/src/detect-sctp-chunk-cnt.c @@ -0,0 +1,122 @@ +/* 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. + */ + +/** + * \file + * + * Implements sctp.chunk_cnt keyword + * + * Author: Giuseppe Longo + */ + +#include "suricata-common.h" +#include "decode.h" + +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine-prefilter-common.h" +#include "detect-engine-build.h" + +#include "detect-sctp-chunk-cnt.h" +#include "detect-engine-uint.h" + +#include "util-byte.h" +#include "util-debug.h" + +static int DetectSCTPChunkCntMatch( + DetectEngineThreadCtx *, Packet *, const Signature *, const SigMatchCtx *); +static int DetectSCTPChunkCntSetup(DetectEngineCtx *, Signature *, const char *); +void DetectSCTPChunkCntFree(DetectEngineCtx *, void *); + +static int PrefilterSetupSCTPChunkCnt(DetectEngineCtx *de_ctx, SigGroupHead *sgh); +static bool PrefilterSCTPChunkCntIsPrefilterable(const Signature *s); + +void DetectSCTPChunkCntRegister(void) +{ + sigmatch_table[DETECT_SCTP_CHUNK_CNT].name = "sctp.chunk_cnt"; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].desc = "match on the SCTP chunk count"; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].url = "/rules/sctp-keywords.html#sctp-chunk-cnt"; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].Match = DetectSCTPChunkCntMatch; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].Setup = DetectSCTPChunkCntSetup; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].Free = DetectSCTPChunkCntFree; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].flags = SIGMATCH_INFO_UINT8; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].SupportsPrefilter = PrefilterSCTPChunkCntIsPrefilterable; + sigmatch_table[DETECT_SCTP_CHUNK_CNT].SetupPrefilter = PrefilterSetupSCTPChunkCnt; +} + +static int DetectSCTPChunkCntMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx) +{ + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + + if (!PacketIsSCTP(p)) { + return 0; + } + + uint8_t val = p->l4.vars.sctp.chunk_cnt; + const DetectU8Data *data = (const DetectU8Data *)ctx; + return DetectU8Match(val, data); +} + +static int DetectSCTPChunkCntSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str) +{ + DetectU8Data *data = DetectU8Parse(str); + if (data == NULL) + return -1; + + if (SCSigMatchAppendSMToList(de_ctx, s, DETECT_SCTP_CHUNK_CNT, (SigMatchCtx *)data, + DETECT_SM_LIST_MATCH) == NULL) { + DetectSCTPChunkCntFree(de_ctx, data); + return -1; + } + s->flags |= SIG_FLAG_REQUIRE_PACKET; + + return 0; +} + +void DetectSCTPChunkCntFree(DetectEngineCtx *de_ctx, void *ptr) +{ + DetectU8Data *data = (DetectU8Data *)ptr; + SCDetectU8Free(data); +} + +static void PrefilterPacketSCTPChunkCntMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const void *pectx) +{ + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + + if (PacketIsSCTP(p)) { + uint8_t val = p->l4.vars.sctp.chunk_cnt; + const PrefilterPacketU8HashCtx *h = pectx; + const SigsArray *sa = h->array[val]; + if (sa) { + PrefilterAddSids(&det_ctx->pmq, sa->sigs, sa->cnt); + } + } +} + +static int PrefilterSetupSCTPChunkCnt(DetectEngineCtx *de_ctx, SigGroupHead *sgh) +{ + return PrefilterSetupPacketHeaderU8Hash(de_ctx, sgh, DETECT_SCTP_CHUNK_CNT, + SIG_MASK_REQUIRE_REAL_PKT, PrefilterPacketU8Set, PrefilterPacketU8Compare, + PrefilterPacketSCTPChunkCntMatch); +} + +static bool PrefilterSCTPChunkCntIsPrefilterable(const Signature *s) +{ + return PrefilterIsPrefilterableById(s, DETECT_SCTP_CHUNK_CNT); +} diff --git a/src/detect-sctp-chunk-cnt.h b/src/detect-sctp-chunk-cnt.h new file mode 100644 index 000000000000..1c9e7620b60b --- /dev/null +++ b/src/detect-sctp-chunk-cnt.h @@ -0,0 +1,27 @@ +/* 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. + */ + +/** + * \file + */ + +#ifndef SURICATA_DETECT_SCTP_CHUNK_CNT_H +#define SURICATA_DETECT_SCTP_CHUNK_CNT_H + +void DetectSCTPChunkCntRegister(void); + +#endif /* SURICATA_DETECT_SCTP_CHUNK_CNT_H */ diff --git a/src/detect-sctp-chunk-data.c b/src/detect-sctp-chunk-data.c new file mode 100644 index 000000000000..3d1cbbb5fe82 --- /dev/null +++ b/src/detect-sctp-chunk-data.c @@ -0,0 +1,220 @@ +/* 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. + */ + +/** + * \file + * + * Implements sctp.chunk_data multi-buffer sticky buffer. + * + * Each SCTP DATA chunk payload in the packet is inspected as a + * separate buffer instance (not reassembled). + * + * Author: Giuseppe Longo + */ + +#include "suricata-common.h" + +#include "detect.h" +#include "detect-engine.h" +#include "detect-engine-buffer.h" +#include "detect-engine-content-inspection.h" +#include "detect-engine-inspect-buffer.h" +#include "detect-engine-mpm.h" +#include "detect-engine-prefilter.h" +#include "detect-sctp-chunk-data.h" +#include "util-mpm.h" +#include "util-profiling.h" + +static int DetectSCTPChunkDataSetup(DetectEngineCtx *, Signature *, const char *); + +static int g_buffer_id = 0; + +/** + * \brief Get a multi-instance inspection buffer for a specific DATA chunk. + * + * \param det_ctx detection engine thread context + * \param transforms transforms to apply + * \param p packet + * \param list_id buffer list id + * \param local_id multi-instance buffer index and index into SCTPVars data_offsets/data_lens + * + * \retval buffer or NULL + */ +static InspectionBuffer *GetBuffer(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Packet *p, const int list_id, + const uint32_t local_id) +{ + DEBUG_VALIDATE_BUG_ON(local_id >= SCTP_MAX_DATA_CHUNKS); + + InspectionBuffer *buffer = InspectionBufferMultipleForListGet(det_ctx, list_id, local_id); + if (buffer == NULL) + return NULL; + if (buffer->initialized) + return buffer; + + const uint16_t offset = p->l4.vars.sctp.data_offsets[(uint8_t)local_id]; + const uint16_t len = p->l4.vars.sctp.data_lens[(uint8_t)local_id]; + if (len == 0) { + InspectionBufferSetupMultiEmpty(buffer); + return NULL; + } + + const uint8_t *data = (const uint8_t *)PacketGetSCTP(p) + offset; + if ((data + (ptrdiff_t)len) > ((uint8_t *)GET_PKT_DATA(p) + (ptrdiff_t)GET_PKT_LEN(p))) { + SCLogDebug("data out of range: %p > %p", (data + (ptrdiff_t)len), + ((uint8_t *)GET_PKT_DATA(p) + (ptrdiff_t)GET_PKT_LEN(p))); + InspectionBufferSetupMultiEmpty(buffer); + return NULL; + } + + InspectionBufferSetupMulti(det_ctx, buffer, transforms, data, len); + return buffer; +} + +/** + * \brief Custom packet inspection callback for sctp.chunk_data. + * + * Loops over all tracked DATA chunks, inspecting each as a separate buffer. + */ +static int DetectEngineInspectSCTPChunkData(DetectEngineThreadCtx *det_ctx, + const DetectEnginePktInspectionEngine *engine, const Signature *s, Packet *p, + uint8_t *_alert_flags) +{ + if (!PacketIsSCTP(p)) + return DETECT_ENGINE_INSPECT_SIG_NO_MATCH; + + const uint8_t cnt = p->l4.vars.sctp.data_chunk_cnt; + if (cnt == 0) + return DETECT_ENGINE_INSPECT_SIG_NO_MATCH; + + const int list_id = engine->sm_list; + const DetectEngineTransforms *transforms = NULL; + if (!engine->mpm) { + transforms = engine->v1.transforms; + } + + for (uint8_t i = 0; i < cnt; i++) { + InspectionBuffer *buffer = GetBuffer(det_ctx, transforms, p, list_id, (uint32_t)i); + if (buffer == NULL || buffer->inspect == NULL) + continue; + + if (DetectEngineContentInspectionBuffer(det_ctx->de_ctx, det_ctx, s, engine->smd, p, + p->flow, buffer, DETECT_ENGINE_CONTENT_INSPECTION_MODE_HEADER)) { + return DETECT_ENGINE_INSPECT_SIG_MATCH; + } + } + + return DETECT_ENGINE_INSPECT_SIG_NO_MATCH; +} + +typedef struct PrefilterMpmSCTPChunkData { + int list_id; + const MpmCtx *mpm_ctx; + const DetectEngineTransforms *transforms; +} PrefilterMpmSCTPChunkData; + +/** + * \brief Prefilter callback: run MPM on each DATA chunk buffer. + */ +static void PrefilterMpmSCTPChunkDataPkt( + DetectEngineThreadCtx *det_ctx, Packet *p, const void *pectx) +{ + if (!PacketIsSCTP(p)) + return; + + const uint8_t cnt = p->l4.vars.sctp.data_chunk_cnt; + if (cnt == 0) + return; + + const PrefilterMpmSCTPChunkData *ctx = (const PrefilterMpmSCTPChunkData *)pectx; + const MpmCtx *mpm_ctx = ctx->mpm_ctx; + const int list_id = ctx->list_id; + + for (uint8_t i = 0; i < cnt; i++) { + InspectionBuffer *buffer = GetBuffer(det_ctx, ctx->transforms, p, list_id, (uint32_t)i); + if (buffer == NULL || buffer->inspect == NULL) + continue; + + if (buffer->inspect_len >= mpm_ctx->minlen) { + (void)mpm_table[mpm_ctx->mpm_type].Search( + mpm_ctx, &det_ctx->mtc, &det_ctx->pmq, buffer->inspect, buffer->inspect_len); + PREFILTER_PROFILING_ADD_BYTES(det_ctx, buffer->inspect_len); + } + } +} + +static void PrefilterMpmSCTPChunkDataFree(void *ptr) +{ + SCFree(ptr); +} + +static int PrefilterSCTPChunkDataRegister(DetectEngineCtx *de_ctx, SigGroupHead *sgh, + MpmCtx *mpm_ctx, const DetectBufferMpmRegistry *mpm_reg, int list_id) +{ + PrefilterMpmSCTPChunkData *pectx = SCCalloc(1, sizeof(*pectx)); + if (pectx == NULL) + return -1; + pectx->list_id = list_id; + pectx->mpm_ctx = mpm_ctx; + pectx->transforms = &mpm_reg->transforms; + + return PrefilterAppendEngine(de_ctx, sgh, PrefilterMpmSCTPChunkDataPkt, 0, + SIGNATURE_HOOK_PKT_NOT_SET, pectx, PrefilterMpmSCTPChunkDataFree, mpm_reg->pname); +} + +void DetectSCTPChunkDataRegister(void) +{ + sigmatch_table[DETECT_SCTP_CHUNK_DATA].name = "sctp.chunk_data"; + sigmatch_table[DETECT_SCTP_CHUNK_DATA].desc = + "sticky buffer to match on each SCTP DATA chunk payload"; + sigmatch_table[DETECT_SCTP_CHUNK_DATA].url = "/rules/sctp-keywords.html#sctp-chunk-data"; + sigmatch_table[DETECT_SCTP_CHUNK_DATA].Setup = DetectSCTPChunkDataSetup; + sigmatch_table[DETECT_SCTP_CHUNK_DATA].flags |= SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER; + + g_buffer_id = DetectBufferTypeRegister("sctp.chunk_data"); + BUG_ON(g_buffer_id < 0); + + DetectBufferTypeSupportsPacket("sctp.chunk_data"); + DetectBufferTypeSupportsMultiInstance("sctp.chunk_data"); + + DetectPktMpmRegister("sctp.chunk_data", 2, PrefilterSCTPChunkDataRegister, NULL); + + DetectPktInspectEngineRegister("sctp.chunk_data", NULL, DetectEngineInspectSCTPChunkData); +} + +/** + * \brief setup sctp.chunk_data sticky buffer + * + * \param de_ctx pointer to the Detection Engine Context + * \param s pointer to the current Signature + * \param _unused unused + * + * \retval 0 on Success + * \retval -1 on Failure + */ +static int DetectSCTPChunkDataSetup(DetectEngineCtx *de_ctx, Signature *s, const char *_unused) +{ + if (!(DetectProtoContainsProto(s->proto, IPPROTO_SCTP))) + return -1; + + s->flags |= SIG_FLAG_REQUIRE_PACKET; + + if (SCDetectBufferSetActiveList(de_ctx, s, g_buffer_id) < 0) + return -1; + + return 0; +} diff --git a/src/detect-sctp-chunk-data.h b/src/detect-sctp-chunk-data.h new file mode 100644 index 000000000000..e66d012ec0b7 --- /dev/null +++ b/src/detect-sctp-chunk-data.h @@ -0,0 +1,27 @@ +/* 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. + */ + +/** + * \file + */ + +#ifndef SURICATA_DETECT_SCTP_CHUNK_DATA_H +#define SURICATA_DETECT_SCTP_CHUNK_DATA_H + +void DetectSCTPChunkDataRegister(void); + +#endif /* SURICATA_DETECT_SCTP_CHUNK_DATA_H */ diff --git a/src/detect-sctp-chunk-type.c b/src/detect-sctp-chunk-type.c new file mode 100644 index 000000000000..dda9d91cca12 --- /dev/null +++ b/src/detect-sctp-chunk-type.c @@ -0,0 +1,140 @@ +/* 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. + */ + +/** + * \file + * + * Implements sctp.chunk_type keyword support + * + * Author: Giuseppe Longo + */ + +#include "suricata-common.h" +#include "decode.h" + +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine-prefilter-common.h" +#include "detect-engine-build.h" + +#include "detect-sctp-chunk-type.h" +#include "detect-engine-uint.h" + +#include "util-byte.h" +#include "util-debug.h" + +static int DetectSCTPChunkTypeMatch( + DetectEngineThreadCtx *, Packet *, const Signature *, const SigMatchCtx *); +static int DetectSCTPChunkTypeSetup(DetectEngineCtx *, Signature *, const char *); +void DetectSCTPChunkTypeFree(DetectEngineCtx *, void *); + +static int PrefilterSetupSCTPChunkType(DetectEngineCtx *de_ctx, SigGroupHead *sgh); +static bool PrefilterSCTPChunkTypeIsPrefilterable(const Signature *s); + +void DetectSCTPChunkTypeRegister(void) +{ + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].name = "sctp.chunk_type"; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].desc = "match on any SCTP chunk type in the packet"; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].url = "/rules/sctp-keywords.html#sctp-chunk-type"; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].Match = DetectSCTPChunkTypeMatch; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].Setup = DetectSCTPChunkTypeSetup; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].Free = DetectSCTPChunkTypeFree; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].flags = + SIGMATCH_INFO_UINT8 | SIGMATCH_INFO_MULTI_UINT | SIGMATCH_INFO_ENUM_UINT; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].SupportsPrefilter = + PrefilterSCTPChunkTypeIsPrefilterable; + sigmatch_table[DETECT_SCTP_CHUNK_TYPE].SetupPrefilter = PrefilterSetupSCTPChunkType; +} + +static int DetectSCTPChunkTypeMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx) +{ + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + + if (!PacketIsSCTP(p)) { + return 0; + } + + const DetectU8Data *data = (const DetectU8Data *)ctx; + const uint8_t cnt = p->l4.vars.sctp.tracked_chunk_cnt; + for (uint8_t i = 0; i < cnt; i++) { + if (DetectU8Match(p->l4.vars.sctp.chunk_types[i], data)) { + return 1; + } + } + return 0; +} + +static int DetectSCTPChunkTypeSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str) +{ + DetectU8Data *data = SCSctpDetectChunkTypeParse(str); + if (data == NULL) + return -1; + + if (SCSigMatchAppendSMToList(de_ctx, s, DETECT_SCTP_CHUNK_TYPE, (SigMatchCtx *)data, + DETECT_SM_LIST_MATCH) == NULL) { + DetectSCTPChunkTypeFree(de_ctx, data); + return -1; + } + s->flags |= SIG_FLAG_REQUIRE_PACKET; + + return 0; +} + +void DetectSCTPChunkTypeFree(DetectEngineCtx *de_ctx, void *ptr) +{ + DetectU8Data *data = (DetectU8Data *)ptr; + SCDetectU8Free(data); +} + +static void PrefilterPacketSCTPChunkTypeMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const void *pectx) +{ + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + + if (!PacketIsSCTP(p)) { + return; + } + + const PrefilterPacketU8HashCtx *h = pectx; + const uint8_t cnt = p->l4.vars.sctp.tracked_chunk_cnt; + /* bitmap to dedup repeated chunk types within a single packet */ + uint32_t seen[8] = { 0 }; + for (uint8_t i = 0; i < cnt; i++) { + const uint8_t val = p->l4.vars.sctp.chunk_types[i]; + if (seen[val >> 5] & (1U << (val & 0x1F))) { + continue; + } + seen[val >> 5] |= 1U << (val & 0x1F); + const SigsArray *sa = h->array[val]; + if (sa) { + PrefilterAddSids(&det_ctx->pmq, sa->sigs, sa->cnt); + } + } +} + +static int PrefilterSetupSCTPChunkType(DetectEngineCtx *de_ctx, SigGroupHead *sgh) +{ + return PrefilterSetupPacketHeaderU8Hash(de_ctx, sgh, DETECT_SCTP_CHUNK_TYPE, + SIG_MASK_REQUIRE_REAL_PKT, PrefilterPacketU8Set, PrefilterPacketU8Compare, + PrefilterPacketSCTPChunkTypeMatch); +} + +static bool PrefilterSCTPChunkTypeIsPrefilterable(const Signature *s) +{ + return PrefilterIsPrefilterableById(s, DETECT_SCTP_CHUNK_TYPE); +} diff --git a/src/detect-sctp-chunk-type.h b/src/detect-sctp-chunk-type.h new file mode 100644 index 000000000000..b3d2a874fbee --- /dev/null +++ b/src/detect-sctp-chunk-type.h @@ -0,0 +1,27 @@ +/* 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. + */ + +/** + * \file + */ + +#ifndef SURICATA_DETECT_SCTP_CHUNK_TYPE_H +#define SURICATA_DETECT_SCTP_CHUNK_TYPE_H + +void DetectSCTPChunkTypeRegister(void); + +#endif /* SURICATA_DETECT_SCTP_CHUNK_TYPE_H */ diff --git a/src/detect-sctp-vtag.c b/src/detect-sctp-vtag.c new file mode 100644 index 000000000000..f0a1983f8fff --- /dev/null +++ b/src/detect-sctp-vtag.c @@ -0,0 +1,129 @@ +/* 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. + */ + +/** + * \file + * + * Implements sctp.vtag keyword + * + * Author: Giuseppe Longo + */ + +#include "suricata-common.h" +#include "decode.h" + +#include "detect.h" +#include "detect-parse.h" +#include "detect-engine.h" +#include "detect-engine-prefilter.h" +#include "detect-engine-prefilter-common.h" +#include "detect-engine-build.h" +#include "detect-engine-uint.h" + +#include "detect-sctp-vtag.h" + +#include "util-debug.h" + +static int DetectSCTPVtagSetup(DetectEngineCtx *, Signature *, const char *); +static int DetectSCTPVtagMatch( + DetectEngineThreadCtx *, Packet *, const Signature *, const SigMatchCtx *); +static void DetectSCTPVtagFree(DetectEngineCtx *, void *); +static int PrefilterSetupSCTPVtag(DetectEngineCtx *de_ctx, SigGroupHead *sgh); +static bool PrefilterSCTPVtagIsPrefilterable(const Signature *s); + +#ifdef UNITTESTS +void DetectSCTPVtagRegisterTests(void); +#endif + +void DetectSCTPVtagRegister(void) +{ + sigmatch_table[DETECT_SCTP_VTAG].name = "sctp.vtag"; + sigmatch_table[DETECT_SCTP_VTAG].desc = "match on the SCTP verification tag"; + sigmatch_table[DETECT_SCTP_VTAG].url = "/rules/sctp-keywords.html#sctp-vtag"; + sigmatch_table[DETECT_SCTP_VTAG].Match = DetectSCTPVtagMatch; + sigmatch_table[DETECT_SCTP_VTAG].Setup = DetectSCTPVtagSetup; + sigmatch_table[DETECT_SCTP_VTAG].Free = DetectSCTPVtagFree; + sigmatch_table[DETECT_SCTP_VTAG].flags = SIGMATCH_INFO_UINT32; + sigmatch_table[DETECT_SCTP_VTAG].SupportsPrefilter = PrefilterSCTPVtagIsPrefilterable; + sigmatch_table[DETECT_SCTP_VTAG].SetupPrefilter = PrefilterSetupSCTPVtag; +} + +static int DetectSCTPVtagMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx) +{ + const DetectU32Data *data = (const DetectU32Data *)ctx; + + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + + if (!(PacketIsSCTP(p))) { + return 0; + } + + return DetectU32Match(SCTP_GET_RAW_VTAG(PacketGetSCTP(p)), data); +} + +static int DetectSCTPVtagSetup(DetectEngineCtx *de_ctx, Signature *s, const char *optstr) +{ + DetectU32Data *data = SCDetectU32Parse(optstr); + if (data == NULL) + return -1; + + if (SCSigMatchAppendSMToList( + de_ctx, s, DETECT_SCTP_VTAG, (SigMatchCtx *)data, DETECT_SM_LIST_MATCH) == NULL) { + DetectSCTPVtagFree(de_ctx, data); + return -1; + } + s->flags |= SIG_FLAG_REQUIRE_PACKET; + return 0; +} + +static void DetectSCTPVtagFree(DetectEngineCtx *de_ctx, void *ptr) +{ + SCDetectU32Free(ptr); +} + +static void PrefilterPacketSCTPVtagMatch( + DetectEngineThreadCtx *det_ctx, Packet *p, const void *pectx) +{ + const PrefilterPacketHeaderCtx *ctx = pectx; + + DEBUG_VALIDATE_BUG_ON(PKT_IS_PSEUDOPKT(p)); + if (!PrefilterPacketHeaderExtraMatch(ctx, p)) + return; + + if (p->proto == IPPROTO_SCTP && PacketIsSCTP(p)) { + DetectU32Data du32; + du32.mode = ctx->v1.u8[0]; + du32.arg1 = ctx->v1.u32[1]; + du32.arg2 = ctx->v1.u32[2]; + if (DetectU32Match(SCTP_GET_RAW_VTAG(PacketGetSCTP(p)), &du32)) { + SCLogDebug("packet matches SCTP vtag %u", ctx->v1.u32[0]); + PrefilterAddSids(&det_ctx->pmq, ctx->sigs_array, ctx->sigs_cnt); + } + } +} + +static int PrefilterSetupSCTPVtag(DetectEngineCtx *de_ctx, SigGroupHead *sgh) +{ + return PrefilterSetupPacketHeader(de_ctx, sgh, DETECT_SCTP_VTAG, SIG_MASK_REQUIRE_REAL_PKT, + PrefilterPacketU32Set, PrefilterPacketU32Compare, PrefilterPacketSCTPVtagMatch); +} + +static bool PrefilterSCTPVtagIsPrefilterable(const Signature *s) +{ + return PrefilterIsPrefilterableById(s, DETECT_SCTP_VTAG); +} diff --git a/src/detect-sctp-vtag.h b/src/detect-sctp-vtag.h new file mode 100644 index 000000000000..a9decaa1224b --- /dev/null +++ b/src/detect-sctp-vtag.h @@ -0,0 +1,27 @@ +/* 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. + */ + +/** + * \file + */ + +#ifndef SURICATA_DETECT_SCTP_VTAG_H +#define SURICATA_DETECT_SCTP_VTAG_H + +void DetectSCTPVtagRegister(void); + +#endif /* SURICATA_DETECT_SCTP_VTAG_H */ diff --git a/src/detect-sctphdr.c b/src/detect-sctphdr.c new file mode 100644 index 000000000000..72ae60dbc87b --- /dev/null +++ b/src/detect-sctphdr.c @@ -0,0 +1,111 @@ +/* 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. + */ + +/** + * \file + * + * Implements sctp.hdr sticky buffer + * + * Author: Giuseppe Longo + */ + +#include "suricata-common.h" + +#include "detect.h" +#include "detect-engine.h" +#include "detect-engine-buffer.h" +#include "detect-engine-mpm.h" +#include "detect-sctphdr.h" +#include "detect-engine-prefilter.h" + +static int DetectSCTPHdrSetup(DetectEngineCtx *, Signature *, const char *); + +static int g_sctphdr_buffer_id = 0; + +static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Packet *p, const int list_id); + +void DetectSCTPHdrRegister(void) +{ + sigmatch_table[DETECT_SCTPHDR].name = "sctp.hdr"; + sigmatch_table[DETECT_SCTPHDR].desc = "sticky buffer to match on the SCTP header"; + sigmatch_table[DETECT_SCTPHDR].url = "/rules/sctp-keywords.html#sctp-hdr"; + sigmatch_table[DETECT_SCTPHDR].Setup = DetectSCTPHdrSetup; + sigmatch_table[DETECT_SCTPHDR].flags |= SIGMATCH_NOOPT | SIGMATCH_INFO_STICKY_BUFFER; + + g_sctphdr_buffer_id = DetectBufferTypeRegister("sctp.hdr"); + BUG_ON(g_sctphdr_buffer_id < 0); + + DetectBufferTypeSupportsPacket("sctp.hdr"); + + DetectPktMpmRegister("sctp.hdr", 2, PrefilterGenericMpmPktRegister, GetData); + + DetectPktInspectEngineRegister("sctp.hdr", GetData, DetectEngineInspectPktBufferGeneric); +} + +/** + * \brief setup sctp.hdr sticky buffer + * + * \param de_ctx pointer to the Detection Engine Context + * \param s pointer to the current Signature + * \param _unused unused + * + * \retval 0 on Success + * \retval -1 on Failure + */ +static int DetectSCTPHdrSetup(DetectEngineCtx *de_ctx, Signature *s, const char *_unused) +{ + if (!(DetectProtoContainsProto(s->proto, IPPROTO_SCTP))) + return -1; + + s->flags |= SIG_FLAG_REQUIRE_PACKET; + + if (SCDetectBufferSetActiveList(de_ctx, s, g_sctphdr_buffer_id) < 0) + return -1; + + return 0; +} + +static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx, + const DetectEngineTransforms *transforms, Packet *p, const int list_id) +{ + SCEnter(); + + if (!PacketIsSCTP(p)) { + SCReturnPtr(NULL, "InspectionBuffer"); + } + + InspectionBuffer *buffer = SCInspectionBufferGet(det_ctx, list_id); + if (buffer->inspect == NULL) { + const SCTPHdr *sctph = PacketGetSCTP(p); + const uint16_t hlen = p->l4.vars.sctp.hlen; + if (((uint8_t *)sctph + (ptrdiff_t)hlen) > + ((uint8_t *)GET_PKT_DATA(p) + (ptrdiff_t)GET_PKT_LEN(p))) { + SCLogDebug("data out of range: %p > %p", ((uint8_t *)sctph + (ptrdiff_t)hlen), + ((uint8_t *)GET_PKT_DATA(p) + (ptrdiff_t)GET_PKT_LEN(p))); + SCReturnPtr(NULL, "InspectionBuffer"); + } + + const uint32_t data_len = hlen; + const uint8_t *data = (const uint8_t *)sctph; + + SCInspectionBufferSetupAndApplyTransforms( + det_ctx, list_id, buffer, data, data_len, transforms); + } + + SCReturnPtr(buffer, "InspectionBuffer"); +} diff --git a/src/detect-sctphdr.h b/src/detect-sctphdr.h new file mode 100644 index 000000000000..02d47157364d --- /dev/null +++ b/src/detect-sctphdr.h @@ -0,0 +1,27 @@ +/* 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. + */ + +/** + * \file + */ + +#ifndef SURICATA_DETECT_SCTPHDR_H +#define SURICATA_DETECT_SCTPHDR_H + +void DetectSCTPHdrRegister(void); + +#endif /* SURICATA_DETECT_SCTPHDR_H */ diff --git a/src/output-json.c b/src/output-json.c index f1a4a29cecc9..b6c6436ec938 100644 --- a/src/output-json.c +++ b/src/output-json.c @@ -925,6 +925,32 @@ SCJsonBuilder *CreateEveHeader(const Packet *p, enum SCOutputJsonLogDirection di } } break; + case IPPROTO_SCTP: + if (PacketIsSCTP(p)) { + SCJbOpenObject(js, "sctp"); + SCJbSetUint(js, "vtag", SCTP_GET_RAW_VTAG(PacketGetSCTP(p))); + SCJbSetUint(js, "chunk_cnt", p->l4.vars.sctp.chunk_cnt); + const uint8_t cnt = p->l4.vars.sctp.tracked_chunk_cnt; + SCJbOpenArray(js, "chunk_types"); + for (uint8_t i = 0; i < cnt; i++) { + const char *name = SCSctpChunkTypeToString(p->l4.vars.sctp.chunk_types[i]); + if (name) { + SCJbAppendString(js, name); + } else { + char unknown[16]; + snprintf(unknown, sizeof(unknown), "unknown(%u)", + p->l4.vars.sctp.chunk_types[i]); + SCJbAppendString(js, unknown); + } + } + SCJbClose(js); + SCJbSetBool(js, "has_init", p->l4.vars.sctp.has_init); + SCJbSetBool(js, "has_init_ack", p->l4.vars.sctp.has_init_ack); + SCJbSetBool(js, "has_data", p->l4.vars.sctp.has_data); + SCJbSetBool(js, "has_abort", p->l4.vars.sctp.has_abort); + SCJbClose(js); + } + break; } SCJbSetString(js, "pkt_src", PktSrcToString(p->pkt_src)); diff --git a/src/runmode-unittests.c b/src/runmode-unittests.c index 4ce39159d9da..e53b79a1e3fa 100644 --- a/src/runmode-unittests.c +++ b/src/runmode-unittests.c @@ -167,6 +167,7 @@ static void RegisterUnittests(void) DecodeTCPRegisterTests(); DecodeUDPV4RegisterTests(); DecodeGRERegisterTests(); + DecodeSCTPRegisterTests(); DecodeESPRegisterTests(); DecodeMPLSRegisterTests(); DecodeNSHRegisterTests();