Feature #8408
openTask #8388: firewall: support protocol hooks for all app-layer protocols
firewall: support FTP-data hook states for firewall rule evaluation
Description
FTP is a critical protocol for network firewall deployments. In Suricata 8.0.4, FTP app-layer hook states are not registered for firewall mode. Attempting to use any ftp:* hook in a firewall rule (e.g., accept:hook ftp:request_command ) fails with the error: protocol ftp does not support hook.
This means FTP traffic cannot be inspected or controlled at the application layer in firewall mode. Even when packet-layer rules accept the TCP handshake on port 21, the FTP control channel is dropped by default_app_policy as soon as the app-layer parser engages, because no FTP hooks exist for the firewall engine to evaluate.
FTP-DATA has its own lifecycle:- Connection requested
- Connection established
- Transfer initiated
- Data transfer in progress
- Transfer complete
- Transfer aborted
- Connection closed
These states should be mapped to firewall hook points that allow rules to make accept/drop decisions at meaningful protocol transitions — for example, during data transfer, etc.
The expectation engine should also be integrated with firewall mode so that PASV/PORT-negotiated data channel ports are automatically allowed when the control channel is accepted. (Optional)
VJ Updated by Victor Julien about 1 month ago
- Copied from Feature #8392: firewall: support FTP hook states for firewall rule evaluation added
VJ Updated by Victor Julien 16 days ago
The FTP data state machine is very simple. There are 2 states: in progress and finished. There is no protocol data to parse, just data. I'm not sure how to distinguish between transfer complete and aborted, and what would be different between finished and connection closed. The transfer is still in progress as long as the tcp connection is active.
There is currently one dedicated ftp data command: ftpdata_command. This is set by the control channel to either RETR or STOR. Additionally the file.* keywords work, but these haven't been tested with firewall mode yet. They have their own state machine that needs some review. Perhaps a dedicated ftpdata_filename keyword could be added as this info is set up in the connection state by the control channel.
@yashda@amazon.com do you have a ruleset example we can use to reason about this?
YD Updated by Yash Datre 1 day ago
Hi Victor,
Here are the ruleset examples. Three scenarios, then thoughts on hook design.
Scenario 1: Read-only FTP (block uploads)¶
Access control happens on the control channel via ftp:request_command_complete. The data channel is handled with broad TCP port-range rules since the passive mode port is dynamically negotiated.
# --- Control channel (port 21) --- accept:hook tcp:all $HOME_NET any -> 10.0.5.100/32 21 (flow:not_established; sid:1;) accept:hook tcp:all $HOME_NET any <> 10.0.5.100/32 21 (flow:established; sid:2;) accept:hook ftp:request_started $HOME_NET any -> 10.0.5.100/32 any (sid:3;) # Allow read-only commands — STOR, STOU, APPE, DELE not listed → dropped by default accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.100/32 any (ftp.command; content:"RETR"; sid:4;) accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.100/32 any (ftp.command; content:"LIST"; sid:5;) accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.100/32 any (ftp.command; content:"PASV"; sid:6;) accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.100/32 any (ftp.command; content:"EPSV"; sid:7;) accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.100/32 any (ftp.command; content:"USER"; sid:8;) accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.100/32 any (ftp.command; content:"PASS"; sid:9;) accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.100/32 any (ftp.command; content:"TYPE"; sid:10;) accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.100/32 any (ftp.command; content:"PWD"; sid:11;) accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.100/32 any (ftp.command; content:"CWD"; sid:12;) accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.100/32 any (ftp.command; content:"QUIT"; sid:13;) accept:hook ftp:response_started 10.0.5.100/32 any -> $HOME_NET any (sid:14;) accept:hook ftp:response_complete 10.0.5.100/32 any -> $HOME_NET any (sid:15;) # --- Data channel (passive mode) --- accept:hook tcp:all 10.0.5.100/32 1024:65535 -> $HOME_NET any (flow:not_established; sid:16;) accept:hook tcp:all 10.0.5.100/32 1024:65535 <> $HOME_NET any (flow:established; sid:17;)
Scenario 2: Bounce attack prevention¶
Same control channel rules as above, but PORT and EPRT are simply not in the accept list → dropped by default. Only PASV/EPSV are accepted. No rules for the server connecting outbound (active mode), so bounce attacks are blocked by default.
Scenario 3: Directory-scoped uploads¶
Allow STOR only to /incoming/ — this is where request_command_complete earns its keep, since both the command and its argument are available:
accept:hook ftp:request_command_complete $HOME_NET any -> 10.0.5.51/32 any \
(ftp.command; content:"STOR"; ftp.command_data; content:"/incoming/"; startswith; sid:20;)
Thoughts on FTP-data hooks¶
You're right that the data state machine is trivially simple. The control channel hooks do the heavy lifting — every accept/drop decision about the transfer is made at request_command_complete where we know the command and target file. By the time data flows, the policy decision is already made.
For the data channel, a simple two-state model ( request_started / request_complete ) is fine. The main value would be defense-in-depth using keywords set by the control channel:
- ftpdata_command — e.g. accept:hook ftp-data:request_started ... (ftpdata_command:retr; sid:X;) to confirm only downloads on the data channel
- file.name — e.g. accept:hook ftp-data:request_started ... (file.name; content:".pdf"; endswith; sid:X;) to restrict by file type
No need to distinguish transfer complete vs. aborted for firewall purposes.
Priority from our side:- FTP control channel hooks — request_started , request_command_complete , response_started , response_complete . This is where all policy decisions happen.
- FTP-data hooks — request_started and request_complete with ftpdata_command and file.name validated for firewall mode. Defense-in-depth.
Let me know if you'd like me to flesh out any specific scenario further.