From 393bee21001b349d3f99d47f421d57be069031c8 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Wed, 19 Aug 2026 15:49:18 -0300 Subject: [PATCH 01/15] scripts: check-doc-rules tries local bin first If the point is to check the docs for added changes, it makes sense to that used binary is the one that comes with the doc changes. Thus, try using the local binary first, before falling back to usr/bin installed Suricata. --- scripts/check-doc-rules.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/check-doc-rules.py b/scripts/check-doc-rules.py index 2785039c539f..59c97a608864 100644 --- a/scripts/check-doc-rules.py +++ b/scripts/check-doc-rules.py @@ -90,6 +90,14 @@ def iter_rst_files(path: Path) -> Iterable[Path]: def resolve_suricata_bin(repo_root: Path, configured: Optional[str]) -> Path: + candidates = [repo_root / "src" / "suricata", repo_root / "suricata"] + + # First check for local repo, as there may be patches to test. + # Then look for the Path option. + for candidate in candidates: + if candidate.exists(): + return candidate + if configured: return Path(configured) @@ -97,11 +105,6 @@ def resolve_suricata_bin(repo_root: Path, configured: Optional[str]) -> Path: if in_path: return Path(in_path) - candidates = [repo_root / "src" / "suricata", repo_root / "suricata"] - for candidate in candidates: - if candidate.exists(): - return candidate - raise SystemExit( "Unable to find Suricata binary. Use --suricata-bin to provide it." ) From 2370357c9b508d8edb0ba38cbab845c15fa69433 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Wed, 19 Aug 2026 16:44:46 -0300 Subject: [PATCH 02/15] scripts: check doc rules as TD and FW rules Previously, a rule that had firewall-only syntax or keywords would fail the script check. Since we can't guarantee that a firewall rule will look different than a detection one, run rule examples against both scenarios before failing them. --- scripts/check-doc-rules.py | 47 +++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/scripts/check-doc-rules.py b/scripts/check-doc-rules.py index 59c97a608864..3124c1383286 100644 --- a/scripts/check-doc-rules.py +++ b/scripts/check-doc-rules.py @@ -121,24 +121,39 @@ def check_rule_with_suricata( shutil.copytree(data_dir, tmpdir, dirs_exist_ok=True) rule_file.write_text(rule + "\n", encoding="utf-8") - cmd = [ - str(suricata_bin), - "-T", - "-c", str(suricata_yaml), - "--data-dir="+tmpdir, - "-S", str(rule_file), - '--strict-rule-keywords=all', - "-l", tmpdir, - ] - proc = subprocess.run( - cmd, - check=False, - capture_output=True, - text=True, + load_modes = ( + ("detection", ["-S", str(rule_file)]), + ("firewall", ["--firewall-rules-exclusive=" + str(rule_file)]), ) - combined = proc.stderr.strip() - return proc.returncode == 0, combined + # Check against both Threat Detection and Firewall rule parsers + # before failing the example rules + failures: List[str] = [] + for label, load_args in load_modes: + cmd = [ + str(suricata_bin), + "-T", + "-c", str(suricata_yaml), + "--data-dir="+tmpdir, + *load_args, + '--strict-rule-keywords=all', + "-l", tmpdir, + ] + proc = subprocess.run( + cmd, + check=False, + capture_output=True, + text=True, + ) + + if proc.returncode == 0: + return True, "" + + failures.append( + f"--- rejected as {label} rule ---\n{proc.stderr.strip()}" + ) + + return False, "\n\n".join(failures) def main() -> int: From d5969af1ee0f714af905a5281bd02607de1ff75b Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Fri, 17 Jul 2026 16:38:47 -0300 Subject: [PATCH 03/15] detect: allow banning kw from td in firewall mode In case a keyword should work in firewall mode, with firewall rules only. The engine errors out if threat detection rules use the given keyword. Part of Ticket #8459 --- rust/sys/src/sys.rs | 1 + src/detect-engine-register.c | 6 ++++++ src/detect-engine-register.h | 2 ++ src/detect-parse.c | 10 +++++++++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/rust/sys/src/sys.rs b/rust/sys/src/sys.rs index 0f0e7d3d65c8..6f30bbec1337 100644 --- a/rust/sys/src/sys.rs +++ b/rust/sys/src/sys.rs @@ -24,6 +24,7 @@ pub const SIGMATCH_INFO_ENUM_UINT: u32 = 524288; pub const SIGMATCH_INFO_BITFLAGS_UINT: u32 = 1048576; pub const SIGMATCH_BAN_FIREWALL_RULE: u32 = 2097152; pub const SIGMATCH_BAN_FIREWALL_MODE: u32 = 4194304; +pub const SIGMATCH_BAN_TD_FIREWALL_MODE: u32 = 8388608; pub type __intmax_t = ::std::os::raw::c_long; pub type intmax_t = __intmax_t; #[repr(u32)] diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index 845521acac1e..d8024eab0fe9 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -343,6 +343,12 @@ static void PrintFeatureList(const SigTableElmt *e, char sep) printf("banned from firewall mode"); prev = 1; } + if (flags & SIGMATCH_BAN_TD_FIREWALL_MODE) { + if (prev == 1) + printf("%c", sep); + printf("banned from threat detection rules in firewall mode"); + prev = 1; + } if (e->Transform) { if (prev == 1) printf("%c", sep); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 221b25ef6997..96e539d05017 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -356,6 +356,8 @@ extern int DETECT_TBLSIZE_IDX; #define SIGMATCH_BAN_FIREWALL_RULE (1UL << (21)) /** keyword cannot be used in firewall mode */ #define SIGMATCH_BAN_FIREWALL_MODE (1UL << (22)) +/** keyword cannot be used in td rules with firewall mode */ +#define SIGMATCH_BAN_TD_FIREWALL_MODE (1UL << (23)) int SigTableList(const char *keyword); void SigTableCleanup(void); diff --git a/src/detect-parse.c b/src/detect-parse.c index 8829b6859cab..2e4b9ed766d8 100644 --- a/src/detect-parse.c +++ b/src/detect-parse.c @@ -973,11 +973,19 @@ static int SigParseOptions(DetectEngineCtx *de_ctx, Signature *s, char *optstr, goto error; } - if (EngineModeIsFirewall() && (st->flags & SIGMATCH_BAN_FIREWALL_MODE) != 0) { + /* For non-firewall rules */ + if (EngineModeIsFirewall() && !s->init_data->firewall_rule && + (st->flags & SIGMATCH_BAN_FIREWALL_MODE) != 0) { SCLogError("keyword \'%s\' is not allowed in firewall mode", optname); goto error; } + if (EngineModeIsFirewall() && !s->init_data->firewall_rule && + (st->flags & SIGMATCH_BAN_TD_FIREWALL_MODE) != 0) { + SCLogError("keyword \'%s\' is not allowed in threat detection rules with firewall mode", + optname); + goto error; + } int setup_ret = 0; /* Validate double quoting, trimming trailing white space along the way. */ From 7aeb89b912326bb9d1d35f8602c723623a5d76ae Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Fri, 17 Jul 2026 16:40:20 -0300 Subject: [PATCH 04/15] detect: ban bypass keyword from td w/ firewall mode The bypass keyword should work in firewall mode, with firewall rules, only. The engine errors out if threat detection rules use said keyword. Ticket #8459 --- src/detect-bypass.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/detect-bypass.c b/src/detect-bypass.c index 61f93c05648c..d97c84b1a9e6 100644 --- a/src/detect-bypass.c +++ b/src/detect-bypass.c @@ -64,7 +64,8 @@ void DetectBypassRegister(void) sigmatch_table[DETECT_BYPASS].Match = DetectBypassMatch; sigmatch_table[DETECT_BYPASS].Setup = DetectBypassSetup; sigmatch_table[DETECT_BYPASS].Free = NULL; - sigmatch_table[DETECT_BYPASS].flags = SIGMATCH_NOOPT | SIGMATCH_BAN_FIREWALL_MODE; + sigmatch_table[DETECT_BYPASS].flags = + SIGMATCH_NOOPT | SIGMATCH_SUPPORT_FIREWALL | SIGMATCH_BAN_TD_FIREWALL_MODE; } static int DetectBypassSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str) From 690a96da20288f5d483abf3c9dbf3280d0c1757c Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Wed, 29 Jul 2026 11:10:11 -0300 Subject: [PATCH 05/15] detect/parse: extract fn to validate fw rules opts If we add more firewall-related rule options, we can keep this opaque to SigParseOptions. Part of Ticket #8459 --- src/detect-parse.c | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/detect-parse.c b/src/detect-parse.c index 2e4b9ed766d8..dcd8bd5e9bbd 100644 --- a/src/detect-parse.c +++ b/src/detect-parse.c @@ -868,6 +868,27 @@ static int DetectSetupDirection(Signature *s, char **str, bool only_dir) return 0; } +/** + * \brief called only with firewall rules, to validate options + * + * It is valid to call this before the keyword's value, if any, has been parsed. + * + * \retval false if the keyword is not allowed for this rule's action and/or action scope + */ +static bool SigParseFirewallRuleAllowed(uint32_t sig_flags, const char *optname) +{ + if ((sig_flags & SIGMATCH_BAN_FIREWALL_RULE) != 0) { + SCLogError("keyword \'%s\' is not allowed with firewall rules", optname); + return false; + } + if ((sig_flags & SIGMATCH_BAN_FIREWALL_MODE) != 0) { + SCLogError("keyword \'%s\' is not allowed in firewall mode", optname); + return false; + } + /* for most cases, firewall rules should be allowed */ + return true; +} + static int SigParseOptions(DetectEngineCtx *de_ctx, Signature *s, char *optstr, char *output, size_t output_size, bool requires) { @@ -968,8 +989,8 @@ static int SigParseOptions(DetectEngineCtx *de_ctx, Signature *s, char *optstr, #undef URL } - if (s->init_data->firewall_rule && (st->flags & SIGMATCH_BAN_FIREWALL_RULE) != 0) { - SCLogError("keyword \'%s\' is not allowed with firewall rules", optname); + if (EngineModeIsFirewall() && s->init_data->firewall_rule && + !SigParseFirewallRuleAllowed(st->flags, optname)) { goto error; } From a5a0f09b4e8843ca2ddc3755a73839253dc2ae48 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Tue, 28 Jul 2026 11:12:23 -0300 Subject: [PATCH 06/15] detect: add action/scope ban mechanism for fw/rules This allows banning variations of `action`:`scope` for specific keywords. Mostly having firewall rules in mind. Done by introducing several SIGMATCH flags, to cover: actions: - config - drop - reject action scopes: - packet - tx - hook `accept` and `flow` were left out as they would not be used for the work at hand. Part of Ticket #8459 --- rust/sys/src/sys.rs | 6 ++++++ src/detect-engine-register.c | 36 ++++++++++++++++++++++++++++++++++++ src/detect-engine-register.h | 8 ++++++++ src/detect-parse.c | 36 ++++++++++++++++++++++++++++++++++-- 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/rust/sys/src/sys.rs b/rust/sys/src/sys.rs index 6f30bbec1337..d6096b58ef4a 100644 --- a/rust/sys/src/sys.rs +++ b/rust/sys/src/sys.rs @@ -25,6 +25,12 @@ pub const SIGMATCH_INFO_BITFLAGS_UINT: u32 = 1048576; pub const SIGMATCH_BAN_FIREWALL_RULE: u32 = 2097152; pub const SIGMATCH_BAN_FIREWALL_MODE: u32 = 4194304; pub const SIGMATCH_BAN_TD_FIREWALL_MODE: u32 = 8388608; +pub const SIGMATCH_BAN_FIREWALL_SCOPE_PACKET: u32 = 16777216; +pub const SIGMATCH_BAN_FIREWALL_SCOPE_TX: u32 = 33554432; +pub const SIGMATCH_BAN_FIREWALL_SCOPE_HOOK: u32 = 67108864; +pub const SIGMATCH_BAN_ACTION_CONFIG: u32 = 134217728; +pub const SIGMATCH_BAN_ACTION_DROP: u32 = 268435456; +pub const SIGMATCH_BAN_ACTION_REJECT: u32 = 536870912; pub type __intmax_t = ::std::os::raw::c_long; pub type intmax_t = __intmax_t; #[repr(u32)] diff --git a/src/detect-engine-register.c b/src/detect-engine-register.c index d8024eab0fe9..c74174d644ec 100644 --- a/src/detect-engine-register.c +++ b/src/detect-engine-register.c @@ -349,6 +349,42 @@ static void PrintFeatureList(const SigTableElmt *e, char sep) printf("banned from threat detection rules in firewall mode"); prev = 1; } + if (flags & SIGMATCH_BAN_FIREWALL_SCOPE_TX) { + if (prev == 1) + printf("%c", sep); + printf("banned from firewall rules with \'tx\' scope"); + prev = 1; + } + if (flags & SIGMATCH_BAN_FIREWALL_SCOPE_HOOK) { + if (prev == 1) + printf("%c", sep); + printf("banned from firewall rules with \'hook\' scope"); + prev = 1; + } + if (flags & SIGMATCH_BAN_ACTION_CONFIG) { + if (prev == 1) + printf("%c", sep); + printf("banned from firewall rules with \'config\' action"); + prev = 1; + } + if (flags & SIGMATCH_BAN_ACTION_DROP) { + if (prev == 1) + printf("%c", sep); + printf("banned from firewall rules with \'drop\' action"); + prev = 1; + } + if (flags & SIGMATCH_BAN_ACTION_REJECT) { + if (prev == 1) + printf("%c", sep); + printf("banned from firewall rules with \'reject\' action"); + prev = 1; + } + if (flags & SIGMATCH_BAN_FIREWALL_SCOPE_PACKET) { + if (prev == 1) + printf("%c", sep); + printf("banned from firewall rules with \'packet\' scope"); + prev = 1; + } if (e->Transform) { if (prev == 1) printf("%c", sep); diff --git a/src/detect-engine-register.h b/src/detect-engine-register.h index 96e539d05017..41d15a2e438d 100644 --- a/src/detect-engine-register.h +++ b/src/detect-engine-register.h @@ -358,6 +358,14 @@ extern int DETECT_TBLSIZE_IDX; #define SIGMATCH_BAN_FIREWALL_MODE (1UL << (22)) /** keyword cannot be used in td rules with firewall mode */ #define SIGMATCH_BAN_TD_FIREWALL_MODE (1UL << (23)) +/** keyword cannot be used in combination with indicated action scope */ +#define SIGMATCH_BAN_FIREWALL_SCOPE_PACKET (1UL << (24)) +#define SIGMATCH_BAN_FIREWALL_SCOPE_TX (1UL << (25)) +#define SIGMATCH_BAN_FIREWALL_SCOPE_HOOK (1UL << (26)) +/** keyword cannot be unsed in combination with indicated action */ +#define SIGMATCH_BAN_ACTION_CONFIG (1UL << (27)) +#define SIGMATCH_BAN_ACTION_DROP (1UL << (28)) +#define SIGMATCH_BAN_ACTION_REJECT (1UL << (29)) int SigTableList(const char *keyword); void SigTableCleanup(void); diff --git a/src/detect-parse.c b/src/detect-parse.c index dcd8bd5e9bbd..e5d1cd48fbf2 100644 --- a/src/detect-parse.c +++ b/src/detect-parse.c @@ -875,7 +875,8 @@ static int DetectSetupDirection(Signature *s, char **str, bool only_dir) * * \retval false if the keyword is not allowed for this rule's action and/or action scope */ -static bool SigParseFirewallRuleAllowed(uint32_t sig_flags, const char *optname) +static bool SigParseFirewallRuleAllowed( + uint8_t action, uint8_t action_scope, uint32_t sig_flags, const char *optname) { if ((sig_flags & SIGMATCH_BAN_FIREWALL_RULE) != 0) { SCLogError("keyword \'%s\' is not allowed with firewall rules", optname); @@ -885,6 +886,37 @@ static bool SigParseFirewallRuleAllowed(uint32_t sig_flags, const char *optname) SCLogError("keyword \'%s\' is not allowed in firewall mode", optname); return false; } + if ((action & ACTION_CONFIG) != 0 && (sig_flags & SIGMATCH_BAN_ACTION_CONFIG) != 0) { + SCLogError("keyword \'%s\' cannot be used in combination with \'config\' action", optname); + return false; + } + if ((action & ACTION_REJECT_ANY) != 0 && (sig_flags & SIGMATCH_BAN_ACTION_REJECT) != 0) { + SCLogError("keyword \'%s\' cannot be used in combination with \'reject\' action", optname); + return false; + } + if ((action & ACTION_DROP) != 0 && (sig_flags & SIGMATCH_BAN_ACTION_DROP) != 0) { + SCLogError("keyword \'%s\' cannot be used in combination with \'drop\' action", optname); + return false; + } + if (action_scope == (uint8_t)ACTION_SCOPE_PACKET) { + if ((sig_flags & SIGMATCH_BAN_FIREWALL_SCOPE_PACKET) != 0) { + SCLogError( + "keyword \'%s\' cannot be used in combination with \'packet\' scope", optname); + return false; + } + } + if (action_scope == (uint8_t)ACTION_SCOPE_TX) { + if ((sig_flags & SIGMATCH_BAN_FIREWALL_SCOPE_TX) != 0) { + SCLogError("keyword \'%s\' cannot be used in combination with \'tx\' scope", optname); + return false; + } + } + if (action_scope == (uint8_t)ACTION_SCOPE_HOOK) { + if ((sig_flags & SIGMATCH_BAN_FIREWALL_SCOPE_HOOK) != 0) { + SCLogError("keyword \'%s\' cannot be used in combination with \'hook\' scope", optname); + return false; + } + } /* for most cases, firewall rules should be allowed */ return true; } @@ -990,7 +1022,7 @@ static int SigParseOptions(DetectEngineCtx *de_ctx, Signature *s, char *optstr, } if (EngineModeIsFirewall() && s->init_data->firewall_rule && - !SigParseFirewallRuleAllowed(st->flags, optname)) { + !SigParseFirewallRuleAllowed(s->action, s->action_scope, st->flags, optname)) { goto error; } From 94fadabeaf64df326b782fc306ca44e3e9904310 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Fri, 7 Aug 2026 21:10:01 -0300 Subject: [PATCH 07/15] detect/bypass: apply action & scope bans to bypass A firewall rule only accepts the `bypass` keyword with the combination of `accept:flow`. Thus, ban: `drop`, `reject`, `config`, `hook`, `tx` and `packet` from firewall usage for this keyword. Part of Ticket #8459 --- src/detect-bypass.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/detect-bypass.c b/src/detect-bypass.c index d97c84b1a9e6..b18e9d408487 100644 --- a/src/detect-bypass.c +++ b/src/detect-bypass.c @@ -65,7 +65,10 @@ void DetectBypassRegister(void) sigmatch_table[DETECT_BYPASS].Setup = DetectBypassSetup; sigmatch_table[DETECT_BYPASS].Free = NULL; sigmatch_table[DETECT_BYPASS].flags = - SIGMATCH_NOOPT | SIGMATCH_SUPPORT_FIREWALL | SIGMATCH_BAN_TD_FIREWALL_MODE; + SIGMATCH_NOOPT | SIGMATCH_SUPPORT_FIREWALL | SIGMATCH_BAN_TD_FIREWALL_MODE | + SIGMATCH_BAN_FIREWALL_SCOPE_PACKET | SIGMATCH_BAN_FIREWALL_SCOPE_TX | + SIGMATCH_BAN_FIREWALL_SCOPE_HOOK | SIGMATCH_BAN_ACTION_CONFIG | + SIGMATCH_BAN_ACTION_DROP | SIGMATCH_BAN_ACTION_REJECT; } static int DetectBypassSetup(DetectEngineCtx *de_ctx, Signature *s, const char *str) From ad91f99c899cd31f6771cb2b4b1f6c84954fc980 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Wed, 29 Jul 2026 11:01:38 -0300 Subject: [PATCH 08/15] detect/parse: document certain functions Especially related to firewall mode. As part of Ticket #8459 --- src/detect-parse.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/detect-parse.c b/src/detect-parse.c index e5d1cd48fbf2..b8a03efb8e84 100644 --- a/src/detect-parse.c +++ b/src/detect-parse.c @@ -2772,6 +2772,7 @@ static bool DetectRuleValidateTable(const Signature *s) return true; } +/** \brief validates firewall rule action scope */ static bool DetectFirewallRuleValidate(const DetectEngineCtx *de_ctx, const Signature *s) { if (s->init_data->hook.type == SIGNATURE_HOOK_TYPE_NOT_SET) { From 71206a4c625fbacac2b1f503c4388362df91faf6 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Tue, 4 Aug 2026 17:18:18 -0300 Subject: [PATCH 09/15] detect/alert: firewall bypass is immediate If a firewall rule sets a flow to be bypassed, the triggering packet could still be inspected by a threat detection rule with a drop. Avoid that the `accept` from the firewall rule would still allow a TD `drop` to be applied to the first packet. This also implies that the stats for accept in such cases will now differ between firewall and IPS, as the firewall accepted+bypassed packet is never seen by ips (so can't be accepted). Related to Ticket #8459 --- src/detect-engine-alert.c | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/detect-engine-alert.c b/src/detect-engine-alert.c index 38d262c364ea..185f7871de49 100644 --- a/src/detect-engine-alert.c +++ b/src/detect-engine-alert.c @@ -600,6 +600,35 @@ static struct DetectFirewallPolicy HandleFirewallRule( return pol; } +/** + * \brief see if a firewall rule in the queue bypassed the flow + * + * `bypass` is applied from the postmatch list at match time. By the time the queue + * is processed the flow is already out of inspection. So, Threat detection must + * not be consulted for this packet. Especially when a TD rule sorts ahead + * of the firewall rule, as packet:td does relative to app:filter. + * + * Independent of the rule's action: `bypass` is a keyword, not an action, and is not tied to + * `accept` + */ +static inline bool AlertQueueHasFirewallBypass( + const DetectEngineThreadCtx *det_ctx, const Packet *p) +{ + if (p->flow == NULL || PKT_IS_PSEUDOPKT(p) || !FlowIsBypassed(p->flow)) + return false; + + for (uint16_t i = 0; i < det_ctx->alert_queue_size; i++) { + const Signature *s = det_ctx->alert_queue[i].s; + if ((s->flags & (SIG_FLAG_FIREWALL | SIG_FLAG_BYPASS)) == + (SIG_FLAG_FIREWALL | SIG_FLAG_BYPASS)) { + SCLogDebug("packet %" PRIu64 ": fw sid %u bypassed the flow, skipping td", + PcapPacketCntGet(p), s->id); + return true; + } + } + return false; +} + /* * Queue order after sorting: * @@ -650,6 +679,11 @@ static inline void PacketAlertFinalizeProcessQueue( #endif /* DEBUG */ uint8_t skip_table_id = 0; bool skip_table = false; + + if (AlertQueueHasFirewallBypass(det_ctx, p)) { + skip_td = true; + } + for (uint16_t i = 0; i < det_ctx->alert_queue_size; i++) { PacketAlert *pa = &det_ctx->alert_queue[i]; const Signature *s = pa->s; @@ -789,7 +823,9 @@ static inline void PacketAlertFinalizeProcessQueue( fw_dropped: /* after threat detection has been handled, see if the fw intended to accept (drop is handled - * immediately by the fw), as fw accept can be overruled by td drop. */ + * immediately by the fw), as fw accept can be overruled by td drop. + * (this is not the case if a flow is accepted _and_ bypassed with a firewall rule + * (accept:flow+bypass)) */ if (have_fw_rules) { if (p->action & ACTION_DROP) { SCLogDebug("packet %" PRIu64 ": dropped by TD", PcapPacketCntGet(p)); From cdab18895eb4ef4ee4309e4a7b48b013d02b1b7b Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Sat, 8 Aug 2026 17:33:11 -0300 Subject: [PATCH 10/15] decode: firewall bypass won't increment ips stats A packet bypassed by the firewall can't lead to ips stats counters increments. For a accept+bypass from the firewall, this implies that the stats for accept in such cases will now differ between firewall and IPS, as the firewall accepted+bypassed packet is never seen by ips (so can't be counted as accepted). Part of Ticket #8459 --- src/decode.c | 11 ++++++++++- src/decode.h | 5 ++++- src/detect-engine-alert.c | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/decode.c b/src/decode.c index e6800d80063a..ca3a56fde9b7 100644 --- a/src/decode.c +++ b/src/decode.c @@ -1063,6 +1063,13 @@ static bool VerdictByFirewall(const Packet *p) return false; } +static bool PacketBypassed(const Packet *p) +{ + if ((p->flags & PKT_FW_BYPASSED) != 0) + return true; + return false; +} + void CaptureStatsUpdate(ThreadVars *tv, const Packet *p) { if (!EngineModeIsIPS() || PKT_IS_PSEUDOPKT(p)) @@ -1096,7 +1103,9 @@ void CaptureStatsUpdate(ThreadVars *tv, const Packet *p) } } else if (PacketCheckAction(p, ACTION_ACCEPT)) { StatsCounterIncr(&tv->stats, s->counter_fw_accepted); - StatsCounterIncr(&tv->stats, s->counter_ips_accepted); + /* A packet bypassed by the firewall isn't seen by IPS */ + if (!PacketBypassed(p)) + StatsCounterIncr(&tv->stats, s->counter_ips_accepted); } } else { if (unlikely(PacketCheckAction(p, ACTION_REJECT_ANY))) { diff --git a/src/decode.h b/src/decode.h index c7e8e31d9869..40f8f6ea368b 100644 --- a/src/decode.h +++ b/src/decode.h @@ -1321,7 +1321,10 @@ void DecodeUnregisterCounters(void); depth reached. */ #define PKT_STREAM_NOPCAPLOG BIT_U32(12) -// vacancy 2x +/** Packet was bypassed by a (firewall) rule */ +#define PKT_FW_BYPASSED BIT_U32(13) + +// vacancy /** Packet checksum is not computed (TX packet for example) */ #define PKT_IGNORE_CHECKSUM BIT_U32(15) diff --git a/src/detect-engine-alert.c b/src/detect-engine-alert.c index 185f7871de49..2f16b77d83f4 100644 --- a/src/detect-engine-alert.c +++ b/src/detect-engine-alert.c @@ -682,6 +682,8 @@ static inline void PacketAlertFinalizeProcessQueue( if (AlertQueueHasFirewallBypass(det_ctx, p)) { skip_td = true; + /* a bypass verdict won't be changed at this point */ + p->flags |= PKT_FW_BYPASSED; } for (uint16_t i = 0; i < det_ctx->alert_queue_size; i++) { From 663046367712aaf4e87d61d6ba4e36a29ff574a4 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Fri, 17 Jul 2026 16:40:26 -0300 Subject: [PATCH 11/15] docs: clarify bypass keyword usage w firewall mode Part of Ticket #8459 --- doc/userguide/firewall/firewall-design.rst | 6 ++- doc/userguide/rules/bypass-keyword.rst | 43 +++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/doc/userguide/firewall/firewall-design.rst b/doc/userguide/firewall/firewall-design.rst index ffee070fe25c..aa68719159ea 100644 --- a/doc/userguide/firewall/firewall-design.rst +++ b/doc/userguide/firewall/firewall-design.rst @@ -31,12 +31,16 @@ counted as ``ips.accepted``. If it was dropped by firewall, only ``firewall.bloc will be incremented. No ``ips.*`` counter will be updated as conceptually the TD instance won't have seen the packet. +.. note:: If a firewall rule uses the :ref:`bypass keyword`, an + accepted packet will not be passed along to the TD step of the pipeline. + Tables ------ A ``table`` is a collection of rules with different properties. These tables are built-in. No custom tables can be created. Tables are available within the scope of packet layer -and application layer (if available). Each rule can define its own :ref:`action scope`. +and application layer (if available). Each rule can define its own +:ref:`action scope`. Packet layer tables ~~~~~~~~~~~~~~~~~~~ diff --git a/doc/userguide/rules/bypass-keyword.rst b/doc/userguide/rules/bypass-keyword.rst index 6572f72f0152..d26d73028850 100644 --- a/doc/userguide/rules/bypass-keyword.rst +++ b/doc/userguide/rules/bypass-keyword.rst @@ -1,3 +1,5 @@ +.. _bypass-keyword: + Bypass Keyword ============== @@ -13,8 +15,11 @@ The ``bypass`` keyword is considered a post-match keyword. .. note:: - ``bypass`` cannot be used in firewall mode, not even with Threat Detection - rules, as this could lead to bypassing the firewall altogether. + In firewall mode, ``bypass`` can only be used in firewall rules. If a threat + detection rule uses the ``bypass`` keyword and you want to run Suricata in + the offending rule will produce an error and won't be loaded. This is to + prevent a threat detection rule from bypassing the firewall altogether. + (To make the engine error out in such a case, use ``--init-errors-fatal``). bypass ------ @@ -26,3 +31,37 @@ Bypass a flow on matching http traffic. alert http any any -> any any (http.host; \ content:"suricata.io"; :example-rule-emphasis:`bypass;` \ sid:10001; rev:1;) + +Firewall mode +------------- + +``bypass`` is only accepted with a specific combination of `action` and `scope`: +``accept:flow``. + +Not accepted: + - Action: ``config`` + - Action: ``reject`` + - Action: ``drop`` + - Scope: ``packet`` + - Scope: ``tx`` + - Scope: ``hook`` + +.. attention:: `bypass` on a firewall rule is a terminating action. Threat + detection rules are not evaluated for the matching packet, respecting the + premise of what would happen if Firewall and IPS were two separate devices. + +.. note:: The type of bypass will depend on whether the engine is configured + for local or capture bypass: offloading is not guaranteed by a firewall + bypass rule. + +.. note:: If `bypass` is used in a rule together with thresholding, the bypass + could be silent, if the alert is suppressed. (This can be checked with the + stats counter: ``detect.alerts_suppressed``). + +Valid firewall rule with bypass: + +.. container:: example-rule + + :example-rule-emphasis:`accept:flow,alert` http1:request_headers any any -> \ + any any (http.host; content:"suricata.io"; :example-rule-emphasis:`bypass;` \ + sid:10001; rev:1;) From 701b334a4cc0239b6fad8a8702a653116cfaf414 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Fri, 7 Aug 2026 20:44:00 -0300 Subject: [PATCH 12/15] userguide: clarify bypass stats with firewall mode Part of Ticket #8459 --- doc/userguide/firewall/firewall-stats.rst | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/doc/userguide/firewall/firewall-stats.rst b/doc/userguide/firewall/firewall-stats.rst index d8561e34dcbe..4fb80c4344a4 100644 --- a/doc/userguide/firewall/firewall-stats.rst +++ b/doc/userguide/firewall/firewall-stats.rst @@ -14,14 +14,25 @@ Statistics counters for the firewall mode cover: These will be present in the stats logs if the engine is run in firewall mode, only. +Bypassed packets +================ + +As the firewall bypass does not happen as the primary action in a firewall +policy/rule, the stats counters for bypassed packets continue to be the ones +that already exist. A `bypassed` packet will be counted as an `accepted` packet +in firewall stats. + Drop reasons ============ -If a drop was caused by the firewall, the corresponding counter will be incremented. The existing ones are: +If a drop was caused by the firewall, the corresponding counter will be +incremented. The existing ones are: - ``rules``: a firewall rule triggered the drop - - ``default_packet_policy``: drop caused by the default fail closed firewall behavior, on the packet hook level - - ``default_app_policy``: drop caused by the default fail close firewall behavior, on the app-layer hook level + - ``default_packet_policy``: drop caused by the default fail closed firewall + behavior, on the packet hook level + - ``default_app_policy``: drop caused by the default fail close firewall + behavior, on the app-layer hook level - ``pre_flow_hook``: drop caused by the pre-flow hook - ``pre_stream_hook``: drop caused by the pre-stream hook - ``flow_drop``: the whole flow was dropped after a firewall action. From 5acfa0e3f3523649893ab9720cb5df37a69febe1 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Sun, 9 Aug 2026 20:36:41 -0300 Subject: [PATCH 13/15] doc: note on bypass and pre_flow hook interactions Related to Ticket #8459 --- doc/userguide/rules/bypass-keyword.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/userguide/rules/bypass-keyword.rst b/doc/userguide/rules/bypass-keyword.rst index d26d73028850..eeae2f0b1b9c 100644 --- a/doc/userguide/rules/bypass-keyword.rst +++ b/doc/userguide/rules/bypass-keyword.rst @@ -65,3 +65,17 @@ Valid firewall rule with bypass: :example-rule-emphasis:`accept:flow,alert` http1:request_headers any any -> \ any any (http.host; content:"suricata.io"; :example-rule-emphasis:`bypass;` \ sid:10001; rev:1;) + + +Special hooks +~~~~~~~~~~~~~ + +``pre_flow`` hook +^^^^^^^^^^^^^^^^^ + +If the ``bypass`` is applied locally, ``pre_flow`` rules will still be processed +and invoked, due to the fact that the engine can't apply nor control a flow +bypass at a stage where the packet hasn't been tied to its flow yet. + +This won't happen in the case of an offloaded bypass, as there won't be +anything for the engine to inspect against. From 8d836f209593aee1db06a2276af50e770c86c83a Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Fri, 21 Aug 2026 19:30:37 -0300 Subject: [PATCH 14/15] detect/alert: incr alert.suppressed w/ fw threshold As a firewall rule skips the TD branch during PacketAlertQueue finalizing, we must account for supressed alerts elsewhere, for firewall rules. --- src/detect-engine-alert.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/detect-engine-alert.c b/src/detect-engine-alert.c index 2f16b77d83f4..db4d010abdd1 100644 --- a/src/detect-engine-alert.c +++ b/src/detect-engine-alert.c @@ -597,6 +597,11 @@ static struct DetectFirewallPolicy HandleFirewallRule( } } } + + /* threshold removed the alert; account for it as the TD path does */ + if ((res == 0 || res == 2) && (s->action & ACTION_ALERT)) { + p->alerts.suppressed++; + } return pol; } From 6b649c518a94ea81f74986cff026d15ff564135c Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Fri, 21 Aug 2026 19:33:41 -0300 Subject: [PATCH 15/15] detect/alert: don't queue fw alert if thresholded PacketAlertHandle returns 2 if the alert is to be suppressed by threshold, but actions should be applied. But the FirewallRule check was adding rules to the alert queue if results were > 0. --- src/detect-engine-alert.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/detect-engine-alert.c b/src/detect-engine-alert.c index db4d010abdd1..40d09b89e70b 100644 --- a/src/detect-engine-alert.c +++ b/src/detect-engine-alert.c @@ -589,7 +589,7 @@ static struct DetectFirewallPolicy HandleFirewallRule( } } /* add the alert for logging if required. */ - if (s->action & ACTION_ALERT) { + if ((s->action & ACTION_ALERT) && res != 2) { if (p->alerts.cnt < packet_alert_max) { p->alerts.alerts[p->alerts.cnt++] = *pa; } else {