Blocking traffic flows selectively with a timeout from Bro IDS
I needed to block some flows on OpenWRT from the Bro IDS. One option was to install the recent
module for expiring iptables
rules which sounded like an overkill. After some tinkering around I landed on using bash
and at
to expire the firewall rules after timeouts (luckily the at daemon was available on OpenWRT which made my job easier).
There are three parts to the process:
The bash
script
First, a script which:
- Constructs and adds the
iptables
rule to theFORWARD
chain. - Constructs the corresponding deletion rule.
- Creates a temporary
bash
script, writes the rule to it, makes the new script self-deleting. - Schedules a launch of the temporary script with
at
command.
Here’s the script:
#!/bin/sh if [ $# -le 5 ] ; then echo "usage: $0 proto src sport dst dport timeout" exit 1 fi proto=$1 src=$2 sport=$3 dest=$4 dport=$5 timeout=$6 echo " proto: $1" echo " src: $2" echo " sport: $3" echo " dest: $4" echo " dport: $5" echo "timeout: $6" rule="" if [ "$proto" != "any" ]; then rule="$rule --protocol $proto" fi if [ "$src" != "0.0.0.0" ]; then rule="$rule --source $src" fi if [ "$sport" != "0" ]; then rule="$rule --sport $sport" fi if [ "$dest" != "0.0.0.0" ]; then rule="$rule --destination $dest" fi if [ "$dport" != "0" ]; then rule="$rule --dport $dport" fi rule="$rule -j DROP" echo "rule: $rule" addcmd="iptables -I FORWARD $rule" delcmd="iptables -D FORWARD $rule" delscript=`mktemp` echo "delscript: $delscript" echo "#!/bin/sh" >>$delscript echo $delcmd >>$delscript echo "rm \"${delscript}\"" >>$delscript chmod 755 $delscript echo "adding iptable rule:" echo $addcmd `$addcmd` atcmd="at -M -f $delscript now + $timeout min" echo "creating at job for deletion:" echo $atcmd `$atcmd` |
Given below is an example run. First, let’s print the default FORWARD
chain:
# iptables -nL FORWARD |
Chain FORWARD (policy ACCEPT) target prot opt source destination ACCEPT all -- anywhere 10.42.0.0/24 state RELATED,ESTABLISHED ACCEPT all -- 10.42.0.0/24 anywhere ACCEPT all -- anywhere anywhere REJECT all -- anywhere anywhere reject-with icmp-port-unreachable REJECT all -- anywhere anywhere reject-with icmp-port-unreachable REJECT all -- anywhere anywhere reject-with icmp-host-prohibited
Block a flow for 2 minutes:
# sh blockflow.sh tcp 50.50.50.50 50 60.60.60.60 60 2 |
proto: tcp src: 50.50.50.50 sport: 50 dest: 60.60.60.60 dport: 60 timeout: 2 rule: --protocol tcp --source 50.50.50.50 --sport 50 --destination 60.60.60.60 --dport 60 -j DROP delscript: /tmp/tmp.SAREJvtsK0 adding iptable rule: iptables -I FORWARD --protocol tcp --source 50.50.50.50 --sport 50 --destination 60.60.60.60 --dport 60 -j DROP creating at job for deletion: at -M -f /tmp/tmp.SAREJvtsK0 now + 2 min job 79 at Sun Jun 30 14:37:00 2013
Let’s check if the new rule was added:
# iptables -nL FORWARD |
Chain FORWARD (policy ACCEPT) target prot opt source destination DROP tcp -- 50.50.50.50 60.60.60.60 tcp spt:50 dpt:60 ACCEPT all -- anywhere 10.42.0.0/24 state RELATED,ESTABLISHED ACCEPT all -- 10.42.0.0/24 anywhere ACCEPT all -- anywhere anywhere REJECT all -- anywhere anywhere reject-with icmp-port-unreachable REJECT all -- anywhere anywhere reject-with icmp-port-unreachable REJECT all -- anywhere anywhere reject-with icmp-host-prohibited
After 2 minutes, the temporary bash script shall remove the rule and then delete itself. To confirm:
# iptables -nL FORWARD |
Chain FORWARD (policy ACCEPT) target prot opt source destination ACCEPT all -- anywhere 10.42.0.0/24 state RELATED,ESTABLISHED ACCEPT all -- 10.42.0.0/24 anywhere ACCEPT all -- anywhere anywhere REJECT all -- anywhere anywhere reject-with icmp-port-unreachable REJECT all -- anywhere anywhere reject-with icmp-port-unreachable REJECT all -- anywhere anywhere reject-with icmp-host-prohibited
The Bro module
A simple module which exports just one function, i.e., BlockFlow::block
which takes a conn_id
and a count
and calls the bash
script with appropriate parameters:
module BlockFlow; export { global block: function(id: conn_id, t: count); } function block(id: conn_id, t: count) { print fmt("blocking %s:%d -> %s:%d for %d minutes", id$orig_h, id$orig_p, id$resp_h, id$resp_p, t); local protocol = get_port_transport_proto(id$resp_p); print fmt("protocol is: %s", protocol); local cmd: string = fmt("sh blockflow.sh %s %s %d %s %d %d", protocol , id$orig_h, id$orig_p , id$resp_h, id$resp_p, t); print fmt("executing: %s", cmd); system(cmd); } |
Bro module usage
And finally, using the module from a Bro script:
@load ./blockflow event bro_init() { local id: conn_id; id$orig_h = 10.10.10.10; id$orig_p = 10/tcp; id$resp_h = 20.20.20.20; id$resp_p = 20/tcp; BlockFlow::block(id, 2); } |
And the flow will be blocked for 2 minutes. Unfortunately, due to the way at
command works the granularity of timeouts is limited to minutes. If you really want to block flows for only a few seconds a quick solution would be to use sleep
in place of at
before expiring the rule.