Project

General

Profile

Actions

Feature #8393

open
YD OD

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 4 months ago. Updated about 16 hours ago.

Status:
In Review
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.


Related issues 1 (1 open0 closed)

Related to Suricata - Bug #8715: smtp: subsequent helo/ehlo should be treated like a rsetIn ReviewJason IshActions

VJ Updated by Victor Julien 4 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 4 months ago Actions #2

  • Parent task set to #8388

VJ Updated by Victor Julien 3 months 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 3 months 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 3 months 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 3 months 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 2 months 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;)

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

  • Status changed from New to Assigned
  • Assignee set to OISF Dev

JI Updated by Jason Ish 3 days ago Actions #9

  • Status changed from Assigned to In Review

JI Updated by Jason Ish about 16 hours ago Actions #10

Some follow-up implementation notes.

Unlike FTP, where each request/response pair is a transaction, in SMTP the whole session until the end of data is a transaction, and I do think this makes sense after working on it for a while. But it does mean scenarios require xbits to maintain state as we progress through the transaction.

The following progress states have been added:

  • smtp:request_started
  • smtp:request_data
  • smtp:request_complete
  • smtp:response_started
  • smtp:response_data
  • smtp:response_complete

We are in requested_started while in the envelope phase. on DATA we move to request_data, then at end of data (before quit), we move to request_complete. Perhaps the names could be better, but the progression would be more or less the same. As we complete the TX and end of data, response_data mainly exists only for symmetry and may not actually be needed.

SMTP also makes it hard to step through various states as the ordering is somewhat arbitrary, you can easily do something like:

  • helo
  • vrfy
  • mail from
  • vrfy
  • rcpt to
  • vrfy

etc...

So mapping these to progression states doesn't work that well. Even sending a rcpt to before mail from is tolerated with an error message allowing you to fix it up in the current session. As such, use of xbits is the way to to express allow and deny lists from the examples above.

So sender validation looks something like:

accept:hook smtp:request_started any any (smtp.mail_from; content: "@corp.example.com>"; endswith; xbits:set,fw.smtp.mail_from_ok; sid:1;)

# When we enter data started, validate the sender was OK.
accept:hook smtp:request_data any any (xbits:isset,fw.smtp.mail_from_ok; sid:2;)

For the inbound gateway:

# Only accept RCPT TO for our domains
accept:hook smtp:request_started any any (smtp.rcpt_to; content: "@example.com>"; endswith; xbits:set,fw.smtp.rcpt_to_ok; sid:1;)
accept:hook smtp:request_started any any (smtp.rcpt_to; content: "@example.org>"; endswith; xbits:set,fw.smtp.rcpt_to_ok; sid:2;)

# Only allow OK recipients to send data.
accept:hook smtp:request_data any any (xbits:isset,fw.smtp.rcpt_to_ok; sid; 3;)

Of course the sender and recipient validation above could be combined, and addresses could be used to apply difference policies per sub-net.

JI Updated by Jason Ish about 14 hours ago Actions #11

  • Related to Bug #8715: smtp: subsequent helo/ehlo should be treated like a rset added
Actions

Also available in: PDF Atom