Feature #8472
openfirewall: Auto-Accept Prior States syntax for firewall mode intent rules
Description
We'd like to propose a syntax addition to Suricata's firewall mode that reduces the rule authoring burden for common firewall use cases while preserving the precision of the state machine.
Currently, writing a firewall rule to allow TLS outbound by SNI requires 13 hand-written rules covering the TCP handshake, all client states, and all server states. The author must know every protocol state name, which state carries which keyword, and the correct direction for each. This pattern repeats for HTTP, DNS, and every other app-layer protocol.
We're exploring a < prefix operator on the hook name that tells the engine to automatically accept all prerequisite states before the specified hook:
accept:tx <tls:client_hello_done $HOME_NET any -> $EXTERNAL_NET any (tls.sni; content:".amazon.com"; endswith; sid:1003;)
This collapses the 13-rule TLS example into a single rule. The author still names the exact hook where the condition is evaluated — the engine handles the transport setup and prior state accepts. Internally this would be a pre-processing step that emits the same state-based rules, so no new architecture is needed.
An alternative expression as a keyword (accept-prior-states;) was also considered for readability. We evaluated several other approaches (full keyword-derived state resolution, keyword-based templates, structured blocks, YAML config) but believe the < operator strikes the right balance between usability and precision.
We also have a few open questions we'd appreciate OISF's perspective on:
- For rules matching keywords at multiple states (e.g., tls.sni at client_hello_done and tls.cert_subject at server_cert_done), should the engine use the latest state, or should such rules be disallowed?
We're looking for feedback on this approach and whether it aligns with the direction OISF envisions for firewall mode. Happy to iterate on the syntax or semantics based on your input.
YD Updated by Yash Datre about 1 month ago
- Tracker changed from Bug to Feature
- Affected Versions deleted (
8.0.4)
We'd like to propose a syntax addition to Suricata's firewall mode that reduces the rule authoring burden for common firewall use cases while preserving the precision of the state machine.
Currently, writing a firewall rule to allow TLS outbound by SNI requires 13 hand-written rules covering the TCP handshake, all client states, and all server states. The author must know every protocol state name, which state carries which keyword, and the correct direction for each. This pattern repeats for HTTP, DNS, and every other app-layer protocol.
We're exploring a < prefix operator on the hook name that tells the engine to automatically accept all prerequisite states before the specified hook:
accept:tx <tls:client_hello_done $HOME_NET any -> $EXTERNAL_NET any (tls.sni; content:".amazon.com"; endswith; sid:1003;)
This collapses the 13-rule TLS example into a single rule. The author still names the exact hook where the condition is evaluated — the engine handles the transport setup and prior state accepts. Internally this would be a pre-processing step that emits the same state-based rules, so no new architecture is needed.
An alternative expression as a keyword (accept-prior-states;) was also considered for readability. We evaluated several other approaches (full keyword-derived state resolution, keyword-based templates, structured blocks, YAML config) but believe the < operator strikes the right balance between usability and precision.
We also have a few open questions we'd appreciate OISF's perspective on:
- For rules matching keywords at multiple states (e.g., tls.sni at client_hello_done and tls.cert_subject at server_cert_done), should the engine use the latest state, or should such rules be disallowed?
We're looking for feedback on this approach and whether it aligns with the direction OISF envisions for firewall mode. Happy to iterate on the syntax or semantics based on your input.
AP Updated by Aneesh Patel about 1 month ago
We also could implement this by making something like pass_prior_hooks be a keyword that customers can add to their rules - either way should work but just wanted to throw an additional idea in case it may make more sense to you all
VJ Updated by Victor Julien about 1 month ago
- Subject changed from Feature Request: Auto-Accept Prior States syntax for firewall mode intent rules to firewall: Auto-Accept Prior States syntax for firewall mode intent rules
YD Updated by Yash Datre 6 days ago
Design Proposal¶
One author-visible rule. The Rule_Loader auto-synthesises the accept chain from the protocol's registered state machine. Two equivalent syntaxes:
accept:tx <tls:client_hello_done $HOME_NET any -> $EXTERNAL_NET any \
(tls.sni; content:".amazon.com"; endswith; sid:1003;)
or, using a keyword form:
accept:tx tls:client_hello_done $HOME_NET any -> $EXTERNAL_NET any \
(tls.sni; content:".amazon.com"; endswith; accept-prior-states; sid:1003;)
Both forms produce the same expansion. All protocol knowledge (state ordinals, directions, registered transports, keyword→state mappings) comes from the existing AppLayerParser* and DetectBufferType registries — no per-protocol code in the loader. Multi-transport protocols are handled by the expander iterating every transport the registry reports for the protocol, so a single DNS rule emits both the TCP handshake block and the UDP transport block.
Design decision 1 — Load-time pre-processor expansion (chosen) vs lazy evaluation¶
The Rule_Loader expands each Prior_State_Rule into concrete accept-rule strings before DetectFirewallRuleAppendNew sees them. Runtime sees ordinary Signature objects.
Pros (chosen approach).
- Zero per-packet cost: equivalent hand-written rules = identical hot path.
- Round-trip equivalence with hand-written rules is structural, not behavioural — same code path from parsing onward.
- No cross-table coupling: the TCP handshake rules must live in
packet_filterand the state rules inapp_filter; load-time expansion handles this naturally. - Pure loader-side change, no reach into
detect.c/ alert / tagging — easier upstream review surface.
Cons.
- Engine-internal rule count grows (10-13× per Prior_State_Rule). Invisible to customers but real in MPM compilation budget. Measured in microseconds per rule for assembly; MPM compilation dominates the load-time budget either way.
Alternative considered — native lazy evaluation. Keep the Prior_State_Rule as a single Signature with a cached prerequisite bitmap, and check "does this packet's current state satisfy the bitmap?" lazily on every detection pass.
Pros (alternative).
- No rule-count explosion in the detection engine.
- Expansion is a runtime concept, not a load-time artefact.
Cons (alternative — why rejected).
- Permanent per-packet branch on every Signature evaluation (is this Prior_State? does current state satisfy?). Adds non-zero cost to the hot path.
- Round-trip equivalence becomes a behavioural claim that two different evaluators have to keep producing identical verdicts across every future engine change.
packet_filter ↔ app_filtercoupling for the TCP handshake side either requires an O(N-rules) per-packet scan or a load-time shadow intopacket_filter— which is effectively pre-processor expansion at a different layer.- Touches
detect.c,detect-engine-alert.c, possiblydetect-engine-tag.c— larger patch, more upstream review, longer path to merge.
Net: lazy evaluation is workable but pays a permanent runtime cost and a recurring correctness burden. Pre-processor expansion costs nothing at runtime and is a single loader-side change.
Design decision 2 — Customer-facing SID abstraction¶
Every customer-visible record (eve.alert, fast.log, syslog, any SID-keyed tooling) shows only the author's Parent_SID. Derived Sub_SIDs exist, but only as engineer-facing attribution in --dump-expanded-rules output and --engine-analysis JSON.
Two structural pieces enforce this:
- Every loaded
Signaturekeeps a uniqueuint32_truntime SID. Suricata's existing uniqueness invariant (detect-parse.c:DetectEngineSignatureIsDuplicate, relied on by thresholding / tagging / dedup) stays intact. Auto-accepted rules get derived runtime SIDs under the deterministic formula0x80000000 | (fnv1a32(file) ^ parent_sid ^ sub_index) & 0x7FFFFFFF. - Every auto-accepted Expanded_Rule carries
noalert;. Todayaccept:*is already silent at the alert layer (PacketAlertFinalizeskips(action & (ACTION_ALERT | ACTION_PASS)) == 0), butnoalert;makes the silence a durable contract instead of a default that could change upstream.
The Decision_Hook rule is not marked noalert; — it is the single rule carrying the Parent_SID and is the only place customer-facing output from the expansion is allowed to originate.
Alternative considered — expose Sub_SIDs via optional eve metadata (firewall.log-expanded-from: yes, metadata.expanded_from: { parent_sid, sub_index } on each alert record).
Pros. Operators debugging a dropped flow can tell which sub-rule matched without --dump-expanded-rules.
Cons (why rejected). Leaks an implementation detail to a customer-facing surface the feature otherwise keeps invisible. Every operator tool keyed on sid would have to decide whether to group on sid or on metadata.expanded_from.parent_sid. Rejected; kept as engineer-facing only.
--dump-expanded-rules — testing tool, not a hard requirement¶
The spec defines a RUNMODE_DUMP_EXPANDED_RULES run mode that loads the ruleset (classifier → parser → validator → expander) and dumps the post-expansion concrete rules to stdout. It's an engineer-facing inspection tool with three uses:
- Demo-able visibility: the 12-rule TLS expansion and 10-rule DNS expansion are observable on stdout before any packets are processed, so reviewers can see what the pre-processor produces.
- Golden-file CI fixture: the Suricata-verify test
fw-prior-state-dump-expandeddiffs the dump against a checked-indump.expectedso rule-shape regressions surface on every build. - Debugging handle for authors: paste the output back into a rule file to verify round-trip equivalence with hand-written rules.
This is not a hard user-facing requirement — no customer needs it to use the feature. It's scaffolding that makes the pre-processor approach auditable and keeps the engineer-inspection deliverables (--engine-analysis integration) cheap. Based on the preference of a different engineer-tooling surface, the expansion work stands on its own and the dump run mode can be dropped or replaced.
VJ Updated by Victor Julien about 3 hours ago
- Status changed from New to In Review
- Assignee set to Victor Julien
- Target version changed from TBD to 9.0.0-beta1
https://github.com/OISF/suricata/pull/15402
Implemented this using stateful rules. It uses tls:<client_hello_done and http1:<request_headers as notation.