BGP Blackhole (RTBH) with Juniper SRX firewall
How to use your Juniper SRX firewall and BGP RTBH to fight some of the spam/bad trafficI own a small (free) web/mail hosting solution for personal and close friends' websites. It is unbelievable how much junk even hosting ~10 domains can attract... about 2000 spam emails / week.
Up until recently, big IP blocks (/24) that were used to deliver spam were added to firewall's blacklist security policy. Due to big amount of spam and new IPs, the configuration started to grow and this method passed the scallability point.
Luckily latest version of SpamAssassin with a few tweaks, ClamAV with Sanesecurity Spam signatures + few custom Clamav signatures filter ~99% of the spam for all domains. Based on this result, Dovecot IMAP server will copy them to the administrator (me

) in one big and welcoming mail directory (like a quarantine).
I still believe that most inteligent and expensive method is to allow all traffic and perform layer 7 inspection ( Intrussion prevention, anti-virus scanning, NG firewalls and so on), but SA/ClamAV/SaneSecurity are very efficient (and free) guards.
But what if you think this way: "Most of the IP addresses / subnets that are used to deliver spam are also probably used to flood, scan (VoIP, SSH, Web, ftp) vulnerabilities, bruteforcing, host phishing (fishing) domains and other types of malware traffic ? What if I want to block them dynamically from reaching my network altogether ?"
Since my network is single homed and no BGP peering is required with my ISP, the BGP blackhole community and FlowSpec solutions to stop the traffic in the ISP network is not something that I am looking for.
Blackhole communities are used to export a /32 route (an IP inside your network that is under attack) to the ISP via eBGP with the ISP's blackhole community to avoid paying for DDoS traffic and to decongestion the line. This does not block based on source IP/subnets, but rather on destination.
Flowspec is newer technology. Juniper and other vendors have been supporting it for some time (folks at CloudFlare use it very efficiently) but still is not scalable enough for my needs.
A hybrid BGP Remote Triggered Blackhole solution between the above two is something that I need. RTBH on my network's edge - bad traffic still reaches my network, but I can inject dynamicallly tens of thousands of offending IPs and block them.
This example is based on RFC5635 (
http://tools.ietf.org/html/rfc5635#appendix-B), but uses slightly different approach than Unicast Reverse Path Filtering as the SRX firewall has anti-spoofing IP screening feature that can be enabled in a security zone (Checkout "Understanding Juniper SRX security zones":
https://www.juniper.net/techpubs/software/junos-security/junos-security10.2/junos-security-swconfig-security/topic-41185.html).
Using IP Spoofing screen over Unicast Reverse Path Filtering has the advantage that it is easier to syslog the offending IPs and see details of the blocked traffic (timestamp, destination IP, source IP). This can also be achieved with uRPF using fail policy, but it is just a little more complex.
Hoow does it work ?- Extract IP addresses used to deliver spam, sort and prepare.
- Edge SRX firewall is configured for a BGP peering connection not to the ISP, but to an internal PERL script. The SRX firewall is configured to set the next hop of all routes to a Reject (send ICMP Dest NET unreachable) or Discard (drop) next hop. It is also configured to reject internal RFC1918 prefixes and to accept only prefix lengths in the range between /24 and /32.
- The perl script uses Perl's NET::BGP::Update module to inject routes into the SRX. Based on bgpsimple, but altered to inject prefixes dynamically (on reciving SIGINT signal). It is also configured to daemonize.
- When new prefixes are added, they are added into a file and a SIGINT signal is sent to the script's daemon PID (kill -2 <PID of script child/daemon).
This guide starts of with extracting the IP addresses used to deliver spam to the network.In the Maildir (IMAP mail directory format that, contrary to MBOX, keeps each individual email in a separate file. Advantages and disadvantages...), it is easy to extract the offending IPs using a simple command line (works on Linux and BSD).
Code:
nobody@mail-server:/var/home/vmail/domain/user/Maildir/.Junk/cur# grep -l '^Subject: \*\*\*\*\*SPAM' * | xargs grep '^Received: from .* by mail.ivorde.ro' | awk '{print $3}' | sort -n | uniq -c | sort -n
...
2 94.242.203
3 115.68.23
3 5.9.76
3 89.40.214
4 31.14.8.226
4 50.31.35.
4 82.208.162.
5 31.14.8.
8 31.14.8.
8 31.14.8.
8 31.14.8.
11 31.14.8.
Output is masked (last octet removed).
This can be very well altered for different analysis of the offending IPaddresses.
Next part: BGP configuration on edge SRX firewall to peer with the perl script.Code:
# show routing-instances ISP protocols bgp group iBGP-block-Spam
type internal;
multihop {
ttl 2;
}
local-address 172.16.2.3; --> Loopback IP address
passive;
log-updown;
import iBGP-block-Spam-import;
family inet {
unicast {
prefix-limit {
maximum 10000;
teardown;
}
}
}
export iBGP-block-Spam-export;
neighbor 10.1.20.2; --> Perl script is running on this IP address
# show policy-options policy-statement iBGP-block-Spam-export
then reject;
[edit]
# show policy-options policy-statement iBGP-block-Spam-import
term reject-internal {
from {
route-filter 10.0.0.0/8 orlonger reject;
route-filter 192.168.0.0/16 orlonger reject;
route-filter 172.16.0.0/12 orlonger reject;
}
}
term 1 {
from {
route-filter 0.0.0.0/0 prefix-length-range /24-/32;
}
then {
community add no-export;
next-hop 192.0.2.1;
accept;
}
}
term 2 {
then reject;
}
# show routing-instances ISP routing-options static route 192.0.2.101/32
reject;
retain;
no-readvertise;
# show security screen ids-option internet | display set | match spoof
set security screen ids-option internet ip spoofing
# show security zones security-zone internet screen
screen internet;
# show security log | display set
set security log mode stream
set security log rate-cap 2
set security log source-address 172.16.2.2
set security log stream homeserv format sd-syslog
set security log stream homeserv category all
set security log stream homeserv host 10.1.20.2
set security log stream homeserv host port 514
Some failsafe measures in the srx firewall:- accepts only prefix ranges between /24 and /32
- accepts a maximum of 10k prefixes and tears down the BGP session with the script in case this limit is reached ( in Junos this limit is applied to received prefixes, not accepted prefixes)
- authentication would also be good.
The perl script is structured this way:- trap SIGINT signal and execute a subroutine named "check". When this subroutine is executed it triggers a chain reaction that involves checking a file on disk for new IPs, constructs the BGP update object, moves these new IP addresses to another file (that is loaded at execute time) and empties the old one.
The script is very basic, without prefix validation - which is probably done in Net::BGP::Update module of perl - and it's intended for proof of concept. It is not supported by me, nor my employer.
Defining some of the interesting variables...
Code:
#!/usr/bin/perl
#
#
#
#
use strict;
use warnings;
use Getopt::Long;
use Net::BGP;
use Net::BGP::Process;
#$SIG{INT} = sub { my $check = 1; }
my $check = 0;
my $end_loop = 5;
my $cur = 1;
my $newupdate;
my $pid = fork();
$SIG{INT} = \✓
Some interesting subroutines
Code:
sub check() {
$check = 1;
}
sub on_the_fly() {
if ($check == 1) {
my $peer = shift(@_);
my $input_file = 'bad_ips.txt';
my $input_size = -s $input_file;
my $update;
my $line;
if($input_size == 0 ) {
print "WTF ?!\r\n";
$check = 0;
return;
}
# Open file with badroutes
open (BADROUTES, '>>bad_routes.txt');
# Open file with new IPs
open BADIPS, '<',$input_file;
while (<BADIPS>) {
chomp($_);
if ($_ !~ /\/(24)|(25)|(26)|(27)|(32)$/) {
$line = $_.'/32';
} else {
$line = $_;
}
if ($line =~ s/^-(.*)/\1/g) {
$update = Net::BGP::Update->new(
Withdraw => [ $line ],
NextHop => '10.1.20.2',
Origin => 'INCOMPLETE',
AsPath => '' ,
LocalPref => 100,
MED => 200
);
} else {
$update = Net::BGP::Update->new(
NLRI => [ $line ],
NextHop => '10.1.20.2',
Origin => 'INCOMPLETE',
AsPath => '' ,
LocalPref => 100,
MED => 200
);
print BADROUTES "$line\r\n";
}
$peer->update($update);
}
close(BADIPS);
open BADIPS, '+>', $input_file;
close BADIPS;
close(BADROUTES);
$check = 0;
}
}
Now, the daemonizing part (it has to run in background)Code:
if($pid!=0){
# Parent exiting
exit;
} else {
## CHILD
if ($dry) {
die "Prefix file (-f) required for dry run!\n" if not ($infile);
sub_debug ("m", "Starting dry run.\n");
sub_update_from_file();
sub_debug ("m", "Dry run done, exiting.\n");
exit;
}
$bgp->add_peer($peer);
$peer->add_timer(\&load_on_execute, 5);
$peer->add_timer(\&on_the_fly, 1);
$bgp->event_loop();
}
The script uses fork() to create a child process that does all the work. The parent dies and child is adopted by INIT (basic Unix).
Next, it opens up the BGP connection to the peer (SRX edge firewall) and creates two callback methods that are executed inside the bgp event loop.
First callback, "load_on_execute" injects all the routes from the "bad_routes.txt" file into BGP when the script is initialized (not covered).
The second callback, "on_the_fly", is executed only when $check variable is 1. This variable is enabled when the "check" subroutine is called. The "check" subroutine is called when SIGINT is received.
Next, the callback method checks if the "bad_ips.txt" file size is not zero ( no point in going further, right ?).
It opens the "bad_ips.txt" file for reading, strips new line characters and if the line starts with a dash, it creates a withdraw event. Otherwise, it creates an update one.
At the end, it creates the bgp update object cleans the input file, remembers all new IPs and disables the $check variable. All these are done inside the bgp event loop.
Once the script has been started, let's do some Verification:
Code:
> show bgp summary
Groups: 2 Peers: 3 Down peers: 0
Table Tot Paths Act Paths Suppressed History Damp State Pending
inet.0 0 0 0 0 0 0
Peer AS InPkt OutPkt OutQ Flaps Last Up/Dwn State|#Active/Received/Accepted/Damped...
10.1.20.2 64819 3285 2487 0 8 12:26:21 Establ
ISP.inet.0: 1031/1032/1031/0
Good. I inject another prefix:
Code:
user@server$ echo 192.254.65.85>>bad_ips.txt
user@server$ kill -2 97855
user@server$ tcpdump <...>
10.1.20.2.41396 > 172.16.2.3.179: Flags [P.], cksum 0xa0bd (correct), seq 1:57, ack 19, win 514, options [nop,nop,TS val 4066994726 ecr 3436391629], length 56: BGP, length: 56
Update Message (2), length: 56
Origin (1), length: 1, Flags [T]: IGP
AS Path (2), length: 0, Flags [T]: empty
Next Hop (3), length: 4, Flags [T]: 10.1.20.2
Multi Exit Discriminator (4), length: 4, Flags [O]: 200
Local Preference (5), length: 4, Flags [T]: 100
Updated routes:
192.254.65.85/32
> show bgp summary
Groups: 2 Peers: 3 Down peers: 0
Table Tot Paths Act Paths Suppressed History Damp State Pending
inet.0 0 0 0 0 0 0
Peer AS InPkt OutPkt OutQ Flaps Last Up/Dwn State|#Active/Received/Accepted/Damped...
10.1.20.2 64819 3290 2492 0 8 12:27:44 Establ
ISP.inet.0: 1032/1033/1032/0
> show route 192.254.65.85 table ISP.inet.0 terse
ISP.inet.0: 4094 destinations, 7134 routes (4092 active, 0 holddown, 3 hidden)
+ = Active Route, - = Last Active, * = Both
A Destination P Prf Metric 1 Metric 2 Next hop AS path
* 192.254.65.85/32 B 170 100 200 Reject I
> show route 192.254.65.85 table ISP.inet.0 extensive | match age
Age: 38 Metric: 200 Metric2: 0
Route was added 38 seconds ago.
Code:
user@server$ ping 192.254.65.85
PING 192.254.65.85 (192.254.65.85): 56 data bytes
36 bytes from 10.1.20.1: Destination Net Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 d409 0 0000 40 01 8649 10.1.20.2 192.254.65.85
user@attacker$ telnet 82.78.227.181 80
Trying 82.78.227.181...
telnet: Unable to connect to remote host: Connection timed out
So for packets coming from the internet, packet is dropped by the anti-spoofing feature. Packets coming from the LAN side towards the blocked IP, are rejected (ICMP type 3) as spoofing is not performed on this side.
Here is a screen syslog message generated by the srx:
Code:
RT_IDS - RT_SCREEN_IP [junos@2636.1.1.1.2.36 attack-name="IP spoofing!" source-address="192.254.65.85" destination-address="82.78.227.181" protocol-id="6" source-zone-name="internet" interface-name="ae1.5" action="drop"]
On Juniper routing platforms without multi services cards, the anti-spoof feature is not present (it's a firewall feature), but instead the uRPF feature can be used (described in RFC 5635).
Note: Both anti-spoof and unicast Reverse Path Filtering features work on the same routing table as the ingress interface and, to my knowledge, it cannot be changed.
There are beter tools out there (like exaBGP) that are ready to do this job much easier, but for me, this is better learning experience.