]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
detect: add vlan.id keyword
authorAlice Akaki <akakialice@gmail.com>
Thu, 7 Nov 2024 20:46:33 +0000 (16:46 -0400)
committerVictor Julien <victor@inliniac.net>
Tue, 14 Jan 2025 14:22:50 +0000 (15:22 +0100)
vlan.id matches on Virtual Local Area Network IDs
It is an unsigned 16-bit integer
Valid range = [0-4095]
Supports prefiltering

Ticket: #1065

doc/userguide/rules/index.rst
doc/userguide/rules/vlan-keywords.rst [new file with mode: 0644]
rust/src/detect/mod.rs
rust/src/detect/vlan.rs [new file with mode: 0644]
src/Makefile.am
src/detect-engine-register.c
src/detect-engine-register.h
src/detect-vlan.c [new file with mode: 0644]
src/detect-vlan.h [new file with mode: 0644]

index c8b586fecaa7bdd20b4f968184d5aa348a9e09e8..415a6c2c381c872a1080fd387fd6d2d19e51ffc2 100644 (file)
@@ -49,3 +49,4 @@ Suricata Rules
    differences-from-snort
    multi-buffer-matching
    tag
+   vlan-keywords
diff --git a/doc/userguide/rules/vlan-keywords.rst b/doc/userguide/rules/vlan-keywords.rst
new file mode 100644 (file)
index 0000000..1ae40f8
--- /dev/null
@@ -0,0 +1,85 @@
+VLAN Keywords
+=============
+
+.. role:: example-rule-action
+.. role:: example-rule-header
+.. role:: example-rule-options
+.. role:: example-rule-emphasis
+
+vlan.id
+-------
+
+Suricata has a ``vlan.id`` keyword that can be used in signatures to identify
+and filter network packets based on Virtual Local Area Network IDs. By default,
+it matches all layers if a packet contains multiple VLAN layers. However, if a
+specific layer is defined, it will only match that layer.
+
+Syntax::
+
+ vlan.id: [op]id[,layer];
+
+The id can be matched exactly, or compared using the ``op`` setting::
+
+ vlan.id:300    # exactly 300
+ vlan.id:<300,0   # smaller than 300 at layer 0
+ vlan.id:>=200,1  # greater or equal than 200 at layer 1
+
+vlan.id uses :ref:`unsigned 16-bit integer <rules-integer-keywords>`.
+
+The valid range for VLAN id values is ``0 - 4095``.
+
+This keyword also supports ``all`` and ``any`` as arguments for ``layer``.
+``all`` matches only if all VLAN layers match and ``any`` matches with any layer.
+
+.. table:: **Layer values for vlan.id keyword**
+
+    ===============  ================================================
+    Value            Description
+    ===============  ================================================
+    [default]        Match with any layer
+    0 - 2            Match specific layer
+    ``-3`` - ``-1``  Match specific layer with back to front indexing
+    all              Match only if all layers match
+    any              Match with any layer
+    ===============  ================================================
+
+This small illustration shows how indexing works for vlan.id::
+
+ [ethernet]
+ [vlan 666 (index 0 and -2)]
+ [vlan 123 (index 1 and -1)]
+ [ipv4]
+ [udp]
+
+Examples
+^^^^^^^^
+
+Example of a signature that would alert if any of the VLAN IDs is equal to 300:
+
+.. container:: example-rule
+
+  alert ip any any -> any any (msg:"Vlan ID is equal to 300"; :example-rule-emphasis:`vlan.id:300;` sid:1;)
+
+Example of a signature that would alert if the VLAN ID at layer 1 is equal to 300:
+
+.. container:: example-rule
+
+  alert ip any any -> any any (msg:"Vlan ID is equal to 300 at layer 1"; :example-rule-emphasis:`vlan.id:300,1;` sid:1;)
+
+Example of a signature that would alert if the VLAN ID at the last layer is equal to 400:
+
+.. container:: example-rule
+
+  alert ip any any -> any any (msg:"Vlan ID is equal to 400 at the last layer"; :example-rule-emphasis:`vlan.id:400,-1;` sid:1;)
+
+Example of a signature that would alert only if all the VLAN IDs are greater than 100:
+
+.. container:: example-rule
+
+  alert ip any any -> any any (msg:"All Vlan IDs are greater than 100"; :example-rule-emphasis:`vlan.id:>100,all;` sid:1;)
+
+It is also possible to use the vlan.id content as a fast_pattern by using the ``prefilter`` keyword, as shown in the following example.
+
+.. container:: example-rule
+
+  alert ip any any -> any any (msg:"Vlan ID is equal to 200 at layer 1"; :example-rule-emphasis:`vlan.id:200,1; prefilter;` sid:1;)
index 1857c22ee2b20fac547c43d3aeaba79c6954259f..c00f0dfdeb18e873aa446e6128f3535fa9275080 100644 (file)
@@ -29,6 +29,7 @@ pub mod transforms;
 pub mod uint;
 pub mod uri;
 pub mod tojson;
+pub mod vlan;
 
 use crate::core::AppProto;
 use std::os::raw::{c_int, c_void};
diff --git a/rust/src/detect/vlan.rs b/rust/src/detect/vlan.rs
new file mode 100644 (file)
index 0000000..fb3abbc
--- /dev/null
@@ -0,0 +1,200 @@
+/* Copyright (C) 2024 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use super::uint::{detect_parse_uint, DetectUintData};
+use std::ffi::CStr;
+use std::str::FromStr;
+
+pub const DETECT_VLAN_ID_ANY: i8 = i8::MIN;
+pub const DETECT_VLAN_ID_ALL: i8 = i8::MAX;
+pub static VLAN_MAX_LAYERS: i8 = 3;
+
+#[repr(C)]
+#[derive(Debug, PartialEq)]
+/// This data structure is also used in detect-vlan.c
+pub struct DetectVlanIdData {
+    /// Vlan id
+    pub du16: DetectUintData<u16>,
+    /// Layer can be DETECT_VLAN_ID_ANY to match with any vlan layer
+    /// DETECT_VLAN_ID_ALL to match if all layers match, or an integer
+    /// within the range -VLAN_MAX_LAYERS to VLAN_MAX_LAYERS-1 for indexing.
+    /// Negative values represent back to front indexing.
+    pub layer: i8,
+}
+
+pub fn detect_parse_vlan_id(s: &str) -> Option<DetectVlanIdData> {
+    let parts: Vec<&str> = s.split(',').collect();
+    let du16 = detect_parse_uint(parts[0]).ok()?.1;
+    if parts.len() > 2 {
+        return None;
+    }
+    if du16.arg1 > 0xFFF || du16.arg2 > 0xFFF {
+        // vlan id is encoded on 12 bits
+        return None;
+    }
+    let layer = if parts.len() == 2 {
+        if parts[1] == "all" {
+            DETECT_VLAN_ID_ALL
+        } else if parts[1] == "any" {
+            DETECT_VLAN_ID_ANY
+        } else {
+            let u8_layer = i8::from_str(parts[1]).ok()?;
+            if !(-VLAN_MAX_LAYERS..=VLAN_MAX_LAYERS - 1).contains(&u8_layer) {
+                return None;
+            }
+            u8_layer
+        }
+    } else {
+        DETECT_VLAN_ID_ANY
+    };
+    return Some(DetectVlanIdData { du16, layer });
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCDetectVlanIdParse(
+    ustr: *const std::os::raw::c_char,
+) -> *mut DetectVlanIdData {
+    let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe
+    if let Ok(s) = ft_name.to_str() {
+        if let Some(ctx) = detect_parse_vlan_id(s) {
+            let boxed = Box::new(ctx);
+            return Box::into_raw(boxed) as *mut _;
+        }
+    }
+    return std::ptr::null_mut();
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn SCDetectVlanIdFree(ctx: &mut DetectVlanIdData) {
+    // Just unbox...
+    std::mem::drop(Box::from_raw(ctx));
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::detect::uint::DetectUintMode;
+
+    #[test]
+    fn test_detect_parse_vlan_id() {
+        assert_eq!(
+            detect_parse_vlan_id("300").unwrap(),
+            DetectVlanIdData {
+                du16: DetectUintData {
+                    arg1: 300,
+                    arg2: 0,
+                    mode: DetectUintMode::DetectUintModeEqual,
+                },
+                layer: DETECT_VLAN_ID_ANY
+            }
+        );
+        assert_eq!(
+            detect_parse_vlan_id("300,any").unwrap(),
+            DetectVlanIdData {
+                du16: DetectUintData {
+                    arg1: 300,
+                    arg2: 0,
+                    mode: DetectUintMode::DetectUintModeEqual,
+                },
+                layer: DETECT_VLAN_ID_ANY
+            }
+        );
+        assert_eq!(
+            detect_parse_vlan_id("300,all").unwrap(),
+            DetectVlanIdData {
+                du16: DetectUintData {
+                    arg1: 300,
+                    arg2: 0,
+                    mode: DetectUintMode::DetectUintModeEqual,
+                },
+                layer: DETECT_VLAN_ID_ALL
+            }
+        );
+        assert_eq!(
+            detect_parse_vlan_id("200,1").unwrap(),
+            DetectVlanIdData {
+                du16: DetectUintData {
+                    arg1: 200,
+                    arg2: 0,
+                    mode: DetectUintMode::DetectUintModeEqual,
+                },
+                layer: 1
+            }
+        );
+        assert_eq!(
+            detect_parse_vlan_id("200,-1").unwrap(),
+            DetectVlanIdData {
+                du16: DetectUintData {
+                    arg1: 200,
+                    arg2: 0,
+                    mode: DetectUintMode::DetectUintModeEqual,
+                },
+                layer: -1
+            }
+        );
+        assert_eq!(
+            detect_parse_vlan_id("!200,2").unwrap(),
+            DetectVlanIdData {
+                du16: DetectUintData {
+                    arg1: 200,
+                    arg2: 0,
+                    mode: DetectUintMode::DetectUintModeNe,
+                },
+                layer: 2
+            }
+        );
+        assert_eq!(
+            detect_parse_vlan_id(">200,2").unwrap(),
+            DetectVlanIdData {
+                du16: DetectUintData {
+                    arg1: 200,
+                    arg2: 0,
+                    mode: DetectUintMode::DetectUintModeGt,
+                },
+                layer: 2
+            }
+        );
+        assert_eq!(
+            detect_parse_vlan_id("200-300,0").unwrap(),
+            DetectVlanIdData {
+                du16: DetectUintData {
+                    arg1: 200,
+                    arg2: 300,
+                    mode: DetectUintMode::DetectUintModeRange,
+                },
+                layer: 0
+            }
+        );
+        assert_eq!(
+            detect_parse_vlan_id("0xC8,2").unwrap(),
+            DetectVlanIdData {
+                du16: DetectUintData {
+                    arg1: 200,
+                    arg2: 0,
+                    mode: DetectUintMode::DetectUintModeEqual,
+                },
+                layer: 2
+            }
+        );
+        assert!(detect_parse_vlan_id("200abc").is_none());
+        assert!(detect_parse_vlan_id("4096").is_none());
+        assert!(detect_parse_vlan_id("600,abc").is_none());
+        assert!(detect_parse_vlan_id("600,100").is_none());
+        assert!(detect_parse_vlan_id("123,-4").is_none());
+        assert!(detect_parse_vlan_id("1,2,3").is_none());
+    }
+}
index 82155d7f825d8188fdf9328d44873d6f4f6d38af..61581695354228a074fe635a484fef1cb1fdb829 100755 (executable)
@@ -313,6 +313,7 @@ noinst_HEADERS = \
        detect-urilen.h \
        detect-within.h \
        detect-xbits.h \
+       detect-vlan.h \
        device-storage.h \
        feature.h \
        flow-bit.h \
@@ -877,6 +878,7 @@ libsuricata_c_a_SOURCES = \
        detect-urilen.c \
        detect-within.c \
        detect-xbits.c \
+       detect-vlan.c \
        device-storage.c \
        feature.c \
        flow-bit.c \
index e19cf7b0ffa6124f8c18cbd4ca397a3a02816cbc..0ca554149f663660b68ed66004ae6f664ef6543f 100644 (file)
 #include "detect-ike-nonce-payload-length.h"
 #include "detect-ike-nonce-payload.h"
 #include "detect-ike-key-exchange-payload.h"
+#include "detect-vlan.h"
 
 #include "action-globals.h"
 #include "tm-threads.h"
@@ -726,6 +727,8 @@ void SigTableSetup(void)
 
     DetectFileHandlerRegister();
 
+    DetectVlanIdRegister();
+
     ScDetectSNMPRegister();
     ScDetectDHCPRegister();
     ScDetectWebsocketRegister();
index 3060a875167f3237cd24579637336e6948873c6d..37ffd0b3455b73f62ad7e2dcbc75deb565147421 100644 (file)
@@ -333,6 +333,8 @@ enum DetectKeywordId {
 
     DETECT_AL_JA4_HASH,
 
+    DETECT_VLAN_ID,
+
     /* make sure this stays last */
     DETECT_TBLSIZE_STATIC,
 };
diff --git a/src/detect-vlan.c b/src/detect-vlan.c
new file mode 100644 (file)
index 0000000..41a7434
--- /dev/null
@@ -0,0 +1,142 @@
+/* Copyright (C) 2024 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+#include "detect-vlan.h"
+#include "detect-engine-uint.h"
+#include "detect-parse.h"
+#include "rust.h"
+
+static int DetectVlanIdMatch(
+        DetectEngineThreadCtx *det_ctx, Packet *p, const Signature *s, const SigMatchCtx *ctx)
+{
+    const DetectVlanIdData *vdata = (const DetectVlanIdData *)ctx;
+
+    if (p->vlan_idx == 0) {
+        return 0;
+    }
+
+    switch (vdata->layer) {
+        case DETECT_VLAN_ID_ANY:
+            for (int i = 0; i < p->vlan_idx; i++) {
+                if (DetectU16Match(p->vlan_id[i], &vdata->du16)) {
+                    return 1;
+                }
+            }
+            return 0;
+        case DETECT_VLAN_ID_ALL:
+            for (int i = 0; i < p->vlan_idx; i++) {
+                if (!DetectU16Match(p->vlan_id[i], &vdata->du16)) {
+                    return 0;
+                }
+            }
+            return 1;
+        default:
+            if (vdata->layer < 0) { // Negative layer values for backward indexing.
+                if (((int16_t)p->vlan_idx) + vdata->layer < 0) {
+                    return 0;
+                }
+                return DetectU16Match(p->vlan_id[p->vlan_idx + vdata->layer], &vdata->du16);
+            } else {
+                if (p->vlan_idx < vdata->layer) {
+                    return 0;
+                }
+                return DetectU16Match(p->vlan_id[vdata->layer], &vdata->du16);
+            }
+    }
+}
+
+static void DetectVlanIdFree(DetectEngineCtx *de_ctx, void *ptr)
+{
+    SCDetectVlanIdFree(ptr);
+}
+
+static int DetectVlanIdSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr)
+{
+    DetectVlanIdData *vdata = SCDetectVlanIdParse(rawstr);
+    if (vdata == NULL) {
+        SCLogError("vlan id invalid %s", rawstr);
+        return -1;
+    }
+
+    if (SigMatchAppendSMToList(
+                de_ctx, s, DETECT_VLAN_ID, (SigMatchCtx *)vdata, DETECT_SM_LIST_MATCH) == NULL) {
+        DetectVlanIdFree(de_ctx, vdata);
+        return -1;
+    }
+    s->flags |= SIG_FLAG_REQUIRE_PACKET;
+
+    return 0;
+}
+
+static void PrefilterPacketVlanIdMatch(DetectEngineThreadCtx *det_ctx, Packet *p, const void *pectx)
+{
+    const PrefilterPacketHeaderCtx *ctx = pectx;
+
+    DetectVlanIdData vdata;
+    vdata.du16.mode = ctx->v1.u8[0];
+    vdata.layer = ctx->v1.u8[1];
+    vdata.du16.arg1 = ctx->v1.u16[2];
+    vdata.du16.arg2 = ctx->v1.u16[3];
+
+    if (p->vlan_idx == 0)
+        return;
+
+    if (DetectVlanIdMatch(det_ctx, p, NULL, (const SigMatchCtx *)&vdata)) {
+        PrefilterAddSids(&det_ctx->pmq, ctx->sigs_array, ctx->sigs_cnt);
+    }
+}
+
+static void PrefilterPacketVlanIdSet(PrefilterPacketHeaderValue *v, void *smctx)
+{
+    const DetectVlanIdData *a = smctx;
+    v->u8[0] = a->du16.mode;
+    v->u8[1] = a->layer;
+    v->u16[2] = a->du16.arg1;
+    v->u16[3] = a->du16.arg2;
+}
+
+static bool PrefilterPacketVlanIdCompare(PrefilterPacketHeaderValue v, void *smctx)
+{
+    const DetectVlanIdData *a = smctx;
+    if (v.u8[0] == a->du16.mode && v.u8[1] == a->layer && v.u16[2] == a->du16.arg1 &&
+            v.u16[3] == a->du16.arg2)
+        return true;
+    return false;
+}
+
+static int PrefilterSetupVlanId(DetectEngineCtx *de_ctx, SigGroupHead *sgh)
+{
+    return PrefilterSetupPacketHeader(de_ctx, sgh, DETECT_VLAN_ID, SIG_MASK_REQUIRE_REAL_PKT,
+            PrefilterPacketVlanIdSet, PrefilterPacketVlanIdCompare, PrefilterPacketVlanIdMatch);
+}
+
+static bool PrefilterVlanIdIsPrefilterable(const Signature *s)
+{
+    return PrefilterIsPrefilterableById(s, DETECT_VLAN_ID);
+}
+
+void DetectVlanIdRegister(void)
+{
+    sigmatch_table[DETECT_VLAN_ID].name = "vlan.id";
+    sigmatch_table[DETECT_VLAN_ID].desc = "match vlan id";
+    sigmatch_table[DETECT_VLAN_ID].url = "/rules/vlan-keywords.html#vlan-id";
+    sigmatch_table[DETECT_VLAN_ID].Match = DetectVlanIdMatch;
+    sigmatch_table[DETECT_VLAN_ID].Setup = DetectVlanIdSetup;
+    sigmatch_table[DETECT_VLAN_ID].Free = DetectVlanIdFree;
+    sigmatch_table[DETECT_VLAN_ID].SupportsPrefilter = PrefilterVlanIdIsPrefilterable;
+    sigmatch_table[DETECT_VLAN_ID].SetupPrefilter = PrefilterSetupVlanId;
+}
\ No newline at end of file
diff --git a/src/detect-vlan.h b/src/detect-vlan.h
new file mode 100644 (file)
index 0000000..301fcff
--- /dev/null
@@ -0,0 +1,23 @@
+/* Copyright (C) 2024 Open Information Security Foundation
+ *
+ * You can copy, redistribute or modify this Program under the terms of
+ * the GNU General Public License version 2 as published by the Free
+ * Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * version 2 along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+#ifndef SURICATA_DETECT_VLAN_H
+#define SURICATA_DETECT_VLAN_H
+
+void DetectVlanIdRegister(void);
+
+#endif /* SURICATA_DETECT_VLAN_H */