A modern Perl-based automatic IP blocking system that monitors Snort IDS alerts and dynamically blocks offending IP addresses using OpenBSD's Packet Filter (PF).
snortsentry operates as a highly resilient, stateful daemon that provides real-time, tiered blacklisting of malicious source IP addresses detected by Snort, utilizing the high-performance OpenBSD PF (Packet Filter) firewall tables.
It reads Snort alert logs in real-time, robustly parses and normalizes network addresses (handling IPv4, IPv6, zone indices, and optional IPv4-mapped IPv6 translation), and atomically adds them to PF tables for immediate blocking. It features configurable block durations, priority filtering, keyword matching, whitelisting, and automatic block escalation for repeat offenders. All state (blocked IPs, expiration, and hit counts) is persisted securely using atomic file operations and checksums for reliability across reboots or crashes.
- Location-neutral: All paths configurable via config file or command-line
- Real-time monitoring: Continuously monitors Snort alert logs
- PF integration: Automatically manages OpenBSD PF tables for both IPv4 and IPv6
- Priority filtering: Block only alerts meeting priority thresholds
- Keyword matching: Filter alerts by specific patterns (e.g., "Portscan", "Exploit")
- Whitelist support: Protect trusted IPs from blocking
- Block escalation: Repeat offenses extend block duration up to configurable maximum
- State persistence: Dumps current blocked IPs to file for monitoring, ensuring atomic and reliable state save
- Daemon mode: Runs as background service with proper signal handling
- Flexible logging: Syslog integration or stdout/stderr output
- OpenBSD (tested on OpenBSD 7.x)
- Perl 5.x (included in base system)
- Snort IDS configured with
alert_fastoutput - Root privileges (for PF table manipulation)
- Perl Modules:
NetAddr::IP(any version, v3.x and later supported),JSON
# Copy script
cp snortsentry /usr/local/sbin/snortsentry
chmod +x /usr/local/sbin/snortsentry
# Copy config file and configure options
cp snortsentry.conf /etc/snort/snortsentry.conf
chmod 640 /etc/snort/snortsentry.conf
# Create necessary directories
mkdir -p /var/{logs,run,db,tmp}/snortsentryThe primary configuration file is /etc/snort/snortsentry.conf. All settings in this file can be overridden by corresponding command-line arguments, following this hierarchy:
Command-Line Arguments > Config File > Default Values
| Directive | Description | Default Value |
|---|---|---|
AlertFile |
Full path to the Snort alert_fast log file. |
/var/log/snort/alert |
StateFile |
Path for the atomic state persistence file. | /var/db/snortsentry.state |
BlockDuration |
Initial block time in seconds. | 60 |
MaxBlockDuration |
Maximum time a persistent offender can be blocked (in seconds). | 86400 (1 day) |
Priority |
Maximum Snort priority to block (1 is highest). | 1 |
Keyword "..." |
Case-insensitive regex pattern required in the alert message. Can be specified multiple times. | N/A (None) |
EscalationTiers |
Comma-separated list of durations (in seconds) for subsequent blocks. | 60, 300, 1800, 3600 |
PfAnchor |
Name of the PF anchor. | snort_block |
UseAnchor |
yes or no. Toggles between Anchor Mode (script manages rules) and Table Only Mode (recommended). |
yes |
AutoWhitelistRFC1918 |
Automatically whitelist RFC 1918 (private) and link-local addresses. | yes |
UnblockOnExit |
Remove all IPs from the PF table when the script gracefully stops. | no |
CheckInterval |
How often (in seconds) to check the alert log file. | 1 |
Add alert_fast output to /etc/snort/snort.conf:
output alert_fast: /var/log/snort/alert
For robust performance and to prevent conflicts with other dynamically updated tables (like bogons or blacklists), snortsentry is best run in its Table Only Mode. This mode requires you to manually define the table and the block rules in your main /etc/pf.conf.
Add the following to /etc/pf.conf:
# 1. Define the table and ensure it persists across reloads
table <snort_block> persist
# 2. Block for IPv4
block drop quick from <snort_block> to any
# 3. Block for IPv6
block drop quick inet6 from <snort_block> to any
Note: If using an anchor, ensure both inet and inet6 block rules are defined within that anchor.
Apply configuration:
pfctl -f /etc/pf.confUsage: ./snortsentry.pl [OPTIONS]
-f FILE Config file (default: /etc/snort/snortsentry.conf)
-a FILE Alert file (default: /var/log/snort/alert)
-p FILE PID file (default: /var/run/snortsentry.pid)
-l FILE Lock file (default: /var/run/snortsentry.lock)
-s FILE State file (default: /var/db/snortsentry.state)
--log FILE Log file (optional)
-t NAME PF table (default: snort_block)
-v Verbose (debug logging)
-D No daemon
-n Dry run
--version Show version
--status Show current status
-h Help
Manual start (foreground for testing):
/usr/local/sbin/snortsentry -f /etc/snort/snortsentry.conf -D -vDaemon mode:
/usr/local/sbin/snortsentry -f /etc/snort/snortsentry.confFrom rc.local:
# Add to /etc/rc.local
if [ -x /usr/local/sbin/snortsentry ]; then
/usr/local/sbin/snortsentry -f /etc/snort/snortsentry.conf
fiFrom rcctl Create /etc/rc.d/snortsentry
#!/bin/ksh
daemon="/usr/local/sbin/snortsentry"
daemon_user="root"
daemon_flags="-f /etc/snort/snortsentry.conf"
. /etc/rc.d/rc.subr
# Set pexp to match the interpreter and the script name.
# The rc.subr script will intelligently match this pattern.
pexp="/usr/bin/perl -T ${daemon} ${daemon_flags}"
rc_cmd $1
Make it executable, enable, start, check status, and stop by executing:
# Make script execuable
chmod +x /etc/rc.d/snortsentry
# Enable snortsentry
rcctl enable snortsentry
# Start snortsentry
rcctl start snortsentry
# Status check snortsentry
rcctl check snortsentry
# Stop snortsentry
rcctl stop snortsentry
When Perl-based daemons are started through OpenBSD's rc subsystem, they often cannot be reliably found or killed using pgrep or pkill commands, even when specifying the script name. Unlike compiled binaries that become the process themselves, Perl scripts run under the Perl interpreter. The kernel registers perl as the process name, not snortsentry. The script path is merely an argument.
You can find the pid of snortsentry post start by running:
pgrep -f snortsentry
You can terminate the running process by executing
pkill -f snortsentry
To test block escalation and ensure IPv4/IPv6 handling is correct, you must add alerts from both families.
To test block escalation and ensure IPv4 and IPv6 handling are correct, you will use a dedicated simulation script. This script rapidly generates a mixture of unique and repeated alerts to trigger all stages of block escalation.
Save the following shell script contents into a file named test.sh and ensure it has executable permissions.
chmod +x test.sh
** IMPORTANT:** Verify that the ALERT_FILE path in this script matches the AlertFile directive in your /etc/snort/snortsentry.conf.
# Start snortsentry in debug mode
/usr/local/sbin/snortsentry -f /etc/snort/snortsentry.conf -D -v
From another console execute test.sh
./test.sh
Monitor the snortsentry output in the first console. You should see logs showing queued block for new IPs, extended for repeat offenders, and whitelisted for the fe80:: address.
Verify Results: After the script finishes, run the following command to check the list of blocked IPs in the PF table:
pfctl -t snort_block -T showThe output should show a list of unique IPv4 and IPv6 addresses blocked during the test, including the persistent attackers: 203.0.113.99, 2001:db8:dead::beef, 198.51.100.99, and 2001:db8:persist::1. The link-local address fe80::dead:beef should not be present due to the auto-whitelisting.
Expected behavior:
- Separate entries for
198.51.100.99and2001:db8:ffff::dead:beefwill be added to the PF table. - Both IPs will show duration extension up to MaxBlockDuration.
Watch in real-time:
# Terminal 1: Watch alerts
tail -f /var/log/snort/alert
# Terminal 2: Watch snortsentry (confirming IPv6 addresses are added cleanly)
/usr/local/sbin/snortsentry -f /etc/snort/snortsentry.conf -D -v
# Terminal 3: Watch PF table (confirming both IPv4 and IPv6 addresses are present)
watch 'pfctl -t snort_block -T show -v'Generate actual Snort alerts:
# IPv4 Port scan from another machine
nmap -sS -p 1-1000 <your_openbsd_ip>
# for port in {20..100}; do
nc -zv -w1 <your_openbsd_ip> $port 2>&1 | grep succeeded
sleep 0.5
done
# IPv6 Scan (ensure Snort is configured to inspect IPv6 traffic)
nmap -6 -sS -p 1-100 <your_openbsd_ipv6_address>
# Check if running
pgrep -f snortsentry
# View daemon logs (if running as daemon)
grep snortsentry /var/log/messages | tail -20
# View custom log (if configured in syslog and newsyslog). Please see doc file
tail -f /var/log/snortsentry.log# Current blocked IPs (shows both IPv4 and IPv6)
pfctl -t snort_block -T show
# Detailed state
cat /var/db/snortsentry/snortsentry
# Count of blocked IPs
pfctl -t snort_block -T show | wc -l# Manually add IPv4
pfctl -t snort_block -T add 1.2.3.4
# Manually add IPv6
pfctl -t snort_block -T add 2001:db8::1
# Manually remove IPv4
pfctl -t snort_block -T delete 1.2.3.4
# Manually remove IPv6
pfctl -t snort_block -T delete 2001:db8::1
# Flush all blocked IPs
pfctl -t snort_block -T flush- Alert Monitoring: snortsentry continuously reads the Snort alert_fast log file
- Alert Parsing: Extracts priority, message/keyword, and source IP from each alert. Uses NetAddr::IP for IPv4/IPv6 normalisation. IPv4-mapped IPv6 addresses (::ffff:x.x.x.x) are transparently rewritten to native IPv4 using pure regex β compatible with NetAddr::IP v3.x and later without requiring v4.x methods..
- Filtering: Checks against priority threshold, keyword patterns, and whitelist
- Blocking: Adds matching IPs to PF table using
pfctl -t <table> -T add <ip>. The process includes robust retry and requeueing logic for transient PF errors. - Escalation: Repeat offenses from same IP extend block duration (capped at MaxBlockDuration)
- Expiration: After block duration expires, IP is automatically removed from PF table
- State Dump: Periodically writes current blocked IPs to dump file for monitoring using atomic writes and checksum validation to prevent data corruption.
snortsentry parses Snort's alert_fast format:
MM/DD-HH:MM:SS.UUUUUU [**] [GID:SID:REV] Message [**] [Classification: class] [Priority: N] {PROTO} SRC_IP:PORT -> DST_IP:PORT
Example (IPv4):
11/26-10:45:24.234567 [**] [1:1421:11] SNORT STREAM TCP Portscan [**] [Classification: Attempted Information Leak] [Priority: 1] {TCP} 198.51.100.99:54321 -> 192.168.1.1:80
Example (IPv6):
11/26-10:45:24.234567 [**] [1:1421:11] SNORT STREAM UDP DNS Query [**] [Classification: Attempted Denial of Service] [Priority: 1] {UDP} [2001:db8:a::1%if0]:5353 -> [ff02::fb]:5353
- Check config file exists and is readable
- Verify AlertFile path is correct and file exists
- Check PidFile directory exists and is writable
- Run with
-D -vto see error messages
- Verify Snort is generating alerts:
tail -f /var/log/snort/alert - Check priority threshold in config (Priority 1 = highest)
- Verify keywords match alert messages
- Check if IPs are whitelisted
- Run snortsentry with
-vflag for verbose output
- Verify table exists:
pfctl -t snort_block -T show - Check PF rules for both
inet(IPv4) andinet6(IPv6) block rules:pfctl -sr | grep snort_block - Ensure table is defined in
/etc/pf.confastable <snort_block> persist - Reload PF:
pfctl -f /etc/pf.conf
- snortsentry must run as root (needs pfctl access)
- Ensure alert file is readable:
ls -l /var/log/snort/alert - Check PID file directory permissions
βββββββββββββββββββββββββββββββ
β Snort β
β Intrusion Detection System β
β - Generates alerts for β
β IPv4 and IPv6 traffic β
βββββββββββββββ¬ββββββββββββββββ
β Writes to alert_fast-formatted log
βΌ
βββββββββββββββββββββββββββββββ
β Alert β
β (alert_fast log format) β
β - Records detailed alerts β
β from Snort β
βββββββββββββββ¬ββββββββββββββββ
β Monitored and parsed by
βΌ
βββββββββββββββββββββββββββββββ
β SnortSentry β
β Alert Management Daemon β
β - Parses alerts, normalizesβ
β IP addresses (IPv4/IPv6) β
β - Filters alerts by β
β priority, keywords, and β
β whitelist criteria β
β - Manages duration of IP β
β block entries β
β - Executes pfctl commands β
β to update firewall rules β
βββββββββββββββ¬ββββββββββββββββ
β Updates block list
βΌ
βββββββββββββββββββββββββββββββ
β PF Table: snort_block β
β - Contains blocked IPs β
β (IPv4 and IPv6) β
β - Entries expire β
β automatically β
βββββββββββββββ¬ββββββββββββββββ
β Enforced by
βΌ
βββββββββββββββββββββββββββββββ
β OpenBSD Packet Filter β
β (pf) firewall controlling β
β - Blocks network traffic β
β based on snort_block IPs β
β - Applies to net/internet6 β
βββββββββββββββββββββββββββββββ
/usr/local/sbin/snortsentry- Main executable/etc/snort/snortsentry.conf- Configuration file/var/log/snort/alert- Snort alerts (configurable)/var/run/snortsentry/snortsentry.pid- PID file (configurable)/var/db/snortsentry/snortsentry- State dump (configurable)
- Minimal CPU usage (sleeps between checks)
- Memory footprint: ~5-10MB
- Handles thousands of blocked IPs efficiently
- Configurable check interval (default: 1 second)
- Whitelist critical IPs: Always whitelist your management IPs to avoid lockout
- Test in non-daemon mode first: Verify configuration before production deployment
- Monitor false positives: Review blocked IPs regularly
- Adjust thresholds: Tune Priority and MaxBlockDuration for your environment
- Backup access: Ensure console/KVM access in case of lockout
This implementation was specifically designed for Tangent Networks Securty Lab demonstrations:
- Self-contained: All paths configurable, no hardcoded dependencies
- Observable: Atomic state saves and verbose mode for educational visibility
- Testable: Easy to generate fake alerts for demonstration and IPv6 testing
- Documented: Comprehensive README for experimenting
- Flexible: Priority/keyword filtering demonstrates security policy concepts
BSD 3-Clause license
David Peter, Tangent Networks
Contributions welcome! Please test thoroughly on OpenBSD before submitting.
- snort(8) - Network Intrusion Detection System
- pf(4) - Packet Filter
- pf.conf(5) - Packet Filter configuration
- pfctl(8) - Control the packet filter