Two small Perl daemons that keep production servers standing under real-world attack traffic.
Marian (HackMan) Marinov
mm@yuhu.biz
tail -F instead of Perl I/O (and why)Load spikes. Site is unresponsive. What does a sysadmin do?
ss -ntp | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head
iptables -I ..., repeatBoth tools automate exactly this reflex. Different data sources, same goal: identify a bad IP and block it in less than a second.
Automatic TCP connection monitoring and blocking
/proc/net/tcp every second/proc/loadavg every 5 secondsSYN flood
Attacker opens many half-open connections; the server's SYN backlog fills and legitimate clients cannot complete a handshake.
→ Detected as too many connections in SYN_RECV from one IP.
Distributed SYN flood (small scale)
Same, spread across a handful of source IPs. Fortress checks SYN_RECV counts regardless of load, so it catches this early.
Connection exhaustion / slow-loris style abuse
An IP opens dozens of ESTABLISHED sockets and sits on them, starving Apache/Nginx worker slots.
→ Detected as too many ESTABLISHED conns to a monitored port.
Application-layer flood
Legitimate handshake, hammering HTTP/HTTPS/SMTP. Same signature; thresholds tighten as load rises.
Bot / scraper storms
Same signature again; excludes files whitelist Google, Bing, Yahoo, Cloudflare, Yandex, Baidu so real crawlers don't get blocked.
It solves one problem:
too many concurrent TCP sockets from one source.
That's it. That's the whole product.
Linux exposes every TCP socket in /proc/net/tcp:
sl local_address rem_address st tx_queue rx_queue tr ...
0: 00000000:0016 00000000:0000 0A 00000000:00000000 ...
Column st is the TCP state as a hex byte. Fortress only cares about two of them:
| Hex | State | Why it matters |
|---|---|---|
01 | ESTABLISHED | Handshake done → app is serving this conn |
03 | SYN_RECV | Half-open → SYN backlog pressure |
TIME_WAIT, CLOSE_WAIT, FIN_WAIT_*, LISTEN — skipped. Those don't consume the resources we're protecting.
SYN+ACK, waiting for final ACKnet.ipv4.tcp_max_syn_backlognet.core.somaxconn. listen(fd, backlog) is effectively min(backlog, net.core.somaxconn)25 53 80 110 143 443 993 995high_load = 5
low_conns = 50 # ESTABLISHED cap when load < 5
high_conns = 20 # ESTABLISHED cap when load >= 5
low_syn_recv_conns = 50
high_syn_recv_conns = 25
Low load → be permissive. A legitimate power user or a small office NAT can easily open 30-40 concurrent sockets.
High load → be strict. The box is already hurting; anything abusive gets cut off fast.
Same IP, same behavior, different verdict depending on server pressure.
PATRICIA = Practical Algorithm To Retrieve Information Coded In Alphanumeric — Morrison, 1968.
Same data structure BSD has used for the kernel routing table since forever.
0 left, 1 rightPrefixes stored:
10.0.0.0/8
10.1.0.0/16
192.168.0.0/16
(root)
/ \
0 / \ 1
/ \
10.0.0.0/8 192.168.0.0/16
|
[bits 8..15] <-- compressed edge
|
10.1.0.0/16
Lookup 10.1.2.3:
root -> 0 branch (bit 0 = 0)
-> match 10.0.0.0/8 (remember)
-> follow compressed edge
-> match 10.1.0.0/16 (remember, deeper)
-> return 10.1.0.0/16
Fortress must never block Googlebot, Cloudflare, or the office IP.
Net::Patricia trie at startupmy $excludes = Net::Patricia->new;
$excludes->add_string('66.249.64.0/19', 'google');
...
next if ($excludes->match_string($ip));
Local IPs auto-detected from ip -4 a l at startup — you can never accidentally block yourself.
/24, /23, /22) to catch more distributed attacks that fly under the per-IP threshold/proc/net/udp — UDP support
Patches welcome — github.com/hackman/Fortress
Log-driven bruteforce detection and IP blocking
hawk-web.pl) for attack historySupported out of the box:
sshd, dovecot (POP3/IMAP), courier, pure-ftpd, proftpd, postfix, exim, cPanel, DirectAdmin, WHM
Hawk does not read log files from Perl.
It opens a pipe to GNU tail:
my $log_list = "/usr/bin/sudo /usr/bin/tail -s 1.00 -F "
. "--max-unchanged-stats=30 $monitor_list |";
$tail_pid = open LOGS, $log_list or die "open $log_list failed: $!\n";
while (<LOGS>) {
# parse the line
}
That's the entire log ingestion layer. One open, one while. Rotation, truncation, multi-file follow — all handled by tail -F.
tail -F and not AnyEvent?Hawk was written when AnyEvent was brand new.
At the time, benchmarking on real production mail servers showed:
AnyEvent::Handle watching 6 log files → measurable CPU overhead per file, dominated by event loop bookkeepingtail -F file1 file2 ... | perl → the kernel does the epoll, tail does the buffering, Perl just reads a pipeThe C-implemented tail was cheaper than the Perl event loop it was supposed to replace. On a shared hosting box running dozens of services, that difference mattered.
Simplicity as a feature — one fewer moving part, one fewer CPAN dep, behaviour identical to what any sysadmin already understands.
tail has been debugged for 30+ years-F reopens on rename/truncate)strace the pipe, doneopen '-|', one while (<LOGS>)Would we rewrite it today with AnyEvent or Mojo::IOLoop? Probably not. It still doesn't win.
Each service has a tiny parser returning (ip, attempts, service_id, user):
sub postfix_broot {
# Dec 30 09:04:16 host postfix/smtpd[14147]: warning:
# unknown[46.148.40.150]: SASL LOGIN authentication failed: ...
if ($_ =~ /\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\]: /) {
return ($1, 1, 7, 'unknown');
}
}
The main loop tries each enabled service in turn. First parser that matches wins. Adding a new service = one sub + one if in the dispatcher.
broot_time = 180 # sliding window (seconds)
broot_number = 10 # failures in window -> "bruteforce"
max_attempts = 2 # bruteforce records -> actual block
block_expire = 86400 # unblock after 24h
Two counters:
broot_time — resets periodically.max_attempts, the IP is added to iptables/ipset.Everything is also written to SQLite/MySQL/PgSQL for the web UI to query later.
hawk-web.pl — a small CGI/FastCGI script that reads the same DB and shows:
Not fancy. Useful. Sysadmins tend to like it more than command-line iptables -L.
| Fortress | Hawk | |
|---|---|---|
| Data source | /proc/net/tcp | Log files (via tail -F) |
| Threat model | Connection floods, SYN attacks | Credential bruteforce |
| Time to detect | ≤ 1 second | Seconds to minutes |
| Sees attacker before login? | Yes | No — only after failed auth |
| Sees which account is targeted? | No | Yes |
| Storage | In-memory + periodic Storable | SQL database |
Run both. They cover different layers and don't conflict — Fortress kills the flood, Hawk kills the patient guesser.
/proc/net/tcp and tail -F are cheaper than any library you can layer on top.Fortress:
https://github.com/hackman/Fortress
Hawk:
https://github.com/hackman/Hawk-IDS-IPS
Both GPLv2. Patches welcome.
Marian Marinov