Project

General

Profile

Actions

Feature #8393

open
YD

Task #8388: firewall: support protocol hooks for all app-layer protocols

firewall: support SMTP hook states for firewall rule evaluation

Feature #8393: firewall: support SMTP hook states for firewall rule evaluation

Added by Yash Datre about 2 months ago. Updated 5 days ago.

Status:
New
Priority:
High
Assignee:
-
Target version:
Effort:
Difficulty:
Label:

Description

SMTP is a widely deployed protocol that network firewalls commonly need to inspect and control. In Suricata 8.0.4, SMTP app-layer hook states are not registered for firewall mode. Attempting to use any smtp:* hook in a firewall rule fails with the error: "protocol smtp does not support hook" .

Without SMTP hooks, SMTP traffic on port 25/587 cannot be inspected at the application layer in firewall mode. Packet-layer rules can accept the TCP handshake, but once the SMTP app-layer parser engages, the flow is dropped by default_app_policy because no hooks exist for the firewall engine to evaluate.

This prevents common firewall use cases such as:
  • Allowing or blocking SMTP based on sender/recipient commands
  • Inspecting MAIL FROM / RCPT TO for policy enforcement
  • Controlling DATA transfer based on content inspection
  • Enforcing STARTTLS requirements
Potential SMTP states:
  • Connected
  • HELO/EHLO sent
  • Server greeting received
  • MAIL FROM sent
  • RCPT TO sent
  • DATA command sent
  • Message body transfer
  • Message accepted
  • QUIT sent
  • Connection closed
  • STARTTLS initiated
  • Authentication in progress

These states should be mapped to firewall hook points that allow rules to make accept/drop decisions at meaningful protocol transitions — for example, after EHLO, after MAIL FROM/RCPT TO, during DATA transfer, after STARTTLS negotiation, etc.

VJ Updated by Victor Julien about 2 months ago Actions #1

  • Tracker changed from Bug to Feature
  • Subject changed from Firewall mode: Register SMTP hook states for firewall rule evaluation to firewall: support SMTP hook states for firewall rule evaluation
  • Priority changed from Normal to High
  • Target version changed from TBD to 9.0.0-beta1
  • Affected Versions deleted (8.0.4)

VJ Updated by Victor Julien about 2 months ago Actions #2

  • Parent task set to #8388

VJ Updated by Victor Julien about 1 month ago Actions #3

Could you provide a mock up of what a real world SMTP ruleset could look like? This is the first protocol that is a bit less structured in terms of order of commands than the other protocols supported, so it would be helpful to look at some examples.

VJ Updated by Victor Julien about 1 month ago Actions #4

I'm thinking most or even all steps can be done by maintaining a relatively simple state machine, where we'd track all command-reponse pairs as transactions. These would be covered by the normal request_started and request_complete states.

Per transaction there would be mode variable to indicate where it fits in the connection state (thinking command, data, closed).

The rules would then have the ability to match on the mode, the command, command data, response code, response string.

A mock up of what a ruleset could look like:

accept:hook smtp:request_started any any -> any 25 (smtp.mode:command;)
accept:hook smtp:request_complete any any -> any 25 (smtp.command; content:"MAIL FROM"; smtp.command_data; dataset:isset,mail-from-allow-list,type string,load mfal.csv;)
reject:flow smtp:request_complete any any -> any 25 (smtp.command; content:"MAIL FROM"; smtp.command_data; content:"ceo@example.com";)
accept:hook smtp:request_complete any any -> any 25 (smtp.command; content:"MAIL FROM"; smtp.command_data; content:"example.com";)
accept:hook smtp:request_complete any any -> any 25 (smtp.command; content:"RCPT TO"; smtp.command_data; content:"example.com";)
accept:hook smtp:request_complete any any -> any 25 (smtp.command; pcre:/^(HELO|EHLO)$/; smtp.command_data; content:"mydomain.lan";)

# DATA command with options
drop:flow smtp:request_complete any any -> any 25 (smtp.command; content:"DATA"; smtp.command_data; bsize:>0;)
# accept DATA command
accept:hook smtp:request_complete any any -> any 25 (smtp.command; content:"DATA";)

accept:hook smtp:response_started any 25 -> any any ()
accept:hook smtp:response_complete any 25 -> any any (smtp.command; content:"MAIL_FROM"; smtp.response_code; content:"250";)
accept:hook smtp:response_complete any 25 -> any any (smtp.command; smtp.response_code; content:"220";)

drop:flow smtp:request_started .. (smtp.mode:data; smtp.data; content:"Buy my crypto";)
accept:hook smtp:request_started .. (smtp.mode:data;)
accept:hook smtp:request_complete .. (smtp.mode:data;)

accept:hook smtp:response_complete .. (smtp.mode:data; smtp.response_code:"250";)

The _started hooks are used to match on a partial command/response line. E.g. when a command like MAIL FROM would be sent in multiple packets.

For this to work the transaction handling would have to be redesigned and several keywords will have to be created. I would imagine that we'd store the helo/ehlo, mail from, rcpt to in the global state to add it to the data mode as well, so keywords can use it there too.

JI Updated by Jason Ish about 1 month ago Actions #5

Victor Julien wrote in #note-4:

I'm thinking most or even all steps can be done by maintaining a relatively simple state machine, where we'd track all command-reponse pairs as transactions.

Do you see these transactions rippling into the logging as well? While it might make sense from a firewall state perspective, when it comes to logging, I think it makes more sense for a transaction to contain all the info from HELO to BYE/RSET?

VJ Updated by Victor Julien about 1 month ago Actions #6

Jason Ish wrote in #note-5:

Victor Julien wrote in #note-4:

I'm thinking most or even all steps can be done by maintaining a relatively simple state machine, where we'd track all command-reponse pairs as transactions.

Do you see these transactions rippling into the logging as well? While it might make sense from a firewall state perspective, when it comes to logging, I think it makes more sense for a transaction to contain all the info from HELO to BYE/RSET?

I guess it kind of creates 2 classes of transactions. One is essentially the existing tx with the current logging, although the existing logic can also create a tx for something like a single QUIT command. The 2nd is a bit lower level, a per command tx, so I would imagine we'd disable it by default for logging.

I suppose the whole setup is a bit like the smb/nfs concept of transactions, where there are special "file tx" for tracking files.

YD Updated by Yash Datre 5 days ago Actions #7

Hi Victor,

Here are the real-world SMTP firewall scenarios we need, with rulesets using your per-command transaction design. Our original request_command_data hook proposal doesn't hold up — SMTP has a sequence of commands (EHLO → MAIL FROM → RCPT TO → DATA) and a single hook can't cleanly handle cases that need decisions at multiple points. Your smtp.mode + per-command request_started / request_complete model maps much better.

Use case 1: Outbound relay with sender validation

Restrict SMTP to a designated relay, allow only corporate senders.


accept:hook tcp:all $HOME_NET any -> 10.0.10.25/32 25 (flow:not_established; sid:1;)
accept:hook tcp:all $HOME_NET any <> 10.0.10.25/32 25 (flow:established; sid:2;)

accept:hook smtp:request_started any any -> any 25 (smtp.mode:command; sid:3;)
accept:hook smtp:request_complete any any -> any 25 (smtp.command; content:"MAIL FROM"; smtp.command_data; content:"@corp.example.com"; endswith; sid:4;)
accept:hook smtp:request_complete any any -> any 25 (smtp.command; content:"RCPT TO"; sid:5;)
accept:hook smtp:request_complete any any -> any 25 (smtp.command; pcre:/^(HELO|EHLO|DATA|QUIT)$/; sid:6;)

accept:hook smtp:request_started any any -> any 25 (smtp.mode:data; sid:7;)
accept:hook smtp:request_complete any any -> any 25 (smtp.mode:data; sid:8;)

accept:hook smtp:response_started any 25 -> any any (sid:9;)
accept:hook smtp:response_complete any 25 -> any any (sid:10;)

Use case 2: Inbound gateway — open relay prevention

Accept inbound SMTP but only for mail addressed to our domains. This is where per-command transactions shine — RCPT TO is a separate transaction from MAIL FROM, so we can accept/drop per-recipient independently.


accept:hook tcp:all $EXTERNAL_NET any -> 10.0.10.50/32 25 (flow:not_established; sid:1;)
accept:hook tcp:all $EXTERNAL_NET any <> 10.0.10.50/32 25 (flow:established; sid:2;)

accept:hook smtp:request_started any any -> any 25 (smtp.mode:command; sid:3;)
accept:hook smtp:request_complete any any -> any 25 (smtp.command; content:"MAIL FROM"; sid:4;)
# Only accept RCPT TO for our domains — everything else dropped by default
accept:hook smtp:request_complete any any -> any 25 (smtp.command; content:"RCPT TO"; smtp.command_data; content:"@example.com"; endswith; sid:5;)
accept:hook smtp:request_complete any any -> any 25 (smtp.command; content:"RCPT TO"; smtp.command_data; content:"@example.org"; endswith; sid:6;)
accept:hook smtp:request_complete any any -> any 25 (smtp.command; pcre:/^(HELO|EHLO|DATA|QUIT)$/; sid:7;)

accept:hook smtp:request_started any any -> any 25 (smtp.mode:data; sid:8;)
accept:hook smtp:request_complete any any -> any 25 (smtp.mode:data; sid:9;)

accept:hook smtp:response_started any 25 -> any any (sid:10;)
accept:hook smtp:response_complete any 25 -> any any (sid:11;)

Use case 3: Sender-recipient pairing (different policies per subnet)

Compliance team (10.0.40.0/24) can send externally; everyone else is internal-only. Because MAIL FROM and RCPT TO are separate transactions, the RCPT TO rule for non-compliance subnets simply restricts to internal recipients — no need to combine both values in a single rule.

# --- Compliance team: corporate sender, any recipient ---
accept:hook smtp:request_complete 10.0.40.0/24 any -> any 25 (smtp.command; content:"MAIL FROM"; smtp.command_data; content:"@corp.example.com"; endswith; sid:4;)
accept:hook smtp:request_complete 10.0.40.0/24 any -> any 25 (smtp.command; content:"RCPT TO"; sid:5;)
# (TCP handshake, mode, response rules omitted for brevity)

# --- Everyone else: corporate sender AND internal recipient only ---
accept:hook smtp:request_complete $HOME_NET any -> any 25 (smtp.command; content:"MAIL FROM"; smtp.command_data; content:"@corp.example.com"; endswith; sid:23;)
accept:hook smtp:request_complete $HOME_NET any -> any 25 (smtp.command; content:"RCPT TO"; smtp.command_data; content:"@corp.example.com"; endswith; sid:24;)
Actions

Also available in: PDF Atom