Rocking iptables like a DJ

What is ipturntables.sh?

ipturntables.sh is a bash script controlling a set of abstract firewall rules. Mainly it uses iptables and ip command to configure a firewall for both IPv4 and IPv6 protocols simultaneosly. Most of it’s functions are reentrant and can be invoked dynamically without reseting the whole firewall but it can also work with static configurations.
The ruleset is highly optimized to keep rule count small and works in a “Deny All, Allow explicit only” fashion as a stateful firewall and can handle multihome scenarios very well.
Often a functionality of allowing and filtering packets spans multiple chains and can not be limited to a single rule therefore this script’s approach is to combine (not necessarily multiple) rules into an easy understandable, comprehensive name. Maybe it can be called an abstract iptables function or call. A full firewall is then assembled by a set of these most common cases as calls like BASE_RULE_SET …, ALLOW_SUBNET …, FORWARD_SUBNET …, etc.

Although some kind of syntax simplification is used this script doesn’t try to achieve this by simplifying iptables arguments itself like eg. UFW does therefore it finally just outputs a rules file by calling  iptables-save command. The idea behind this is to avoid iptables micro management by using a combination of a set of well defined firewall filter functions, like a DJ who uses a mix of records for his art instead of composing the music, ipturntables is an approach to handle a firewall the same way.
Most serious admins won’t trust a “script” when it comes to firewall security but that’s ok. You need to be familiar with iptables anyway in order to understand what it does or you can just study the source code therefore it can be used as a good starting point for customized configurations without going too much into implementation details.

But there’s more ipturntables.sh maintains some kind of simple resource tracking for rules created by an abstract call therefore it can remove all rules of such a call cleanly without the need to reconfigure the whole firewall. This makes it possible to tune the firewall dynamically during runtime for different scenarios like multi-homed hosts with multiple interfaces coming up and going down which eases configuration complexity where usally multiple different profiles are used for such a task. Here’s a subset of features:

  • ALLOW_DHCP_CLIENT
  • ALLOW_SUBNETS
  • ALLOW_LINK_LOCAL
  • ALLOW_SERVICE_DISCOVERY
  • ALLOW_TUNNEL
  • FORWARD_SUBNET (FORWARD_SUBNET_PROTECTIVE)
  • FORWARD_PORT|FORWARD_ROUTING
  • MAC_FILTER
  • POSTROUTING_MASQUERADE

At first glance most of this stuff might look simple but in fact some of these functions do really complex things although this is not a documentation i would like to explain the rules in detail for at least some of them to give you an insight of the inner mechanics and pricinples of the rules setup used.

Thanks to Mark Oellermann and VRT Systems for providing a nice set of shapes for the graphics.

Creating a base config

Let’s start with the scenario used. A typical linux server used as a router with 3 network interfaces with one interface offering public internet connection through a cable modem or DSL or whatever.

Figure A: Example scenario used.

Although adding rules dynamically offers fexibility most of the base configuration on a router are long living rules which can be created statically eg. with a simple Makefile:

#!/usr/bin/make

all: IPv4.rules IPv6.rules

IPv4.rules:
    ./ipturntables.sh -4 \
    KERNEL_PARAMS accept_source_route,accept_redirects,rp_filter,ip_forward \
    PROBE_KERNEL_MODS ip_tables,nf_conntrack \
    RESET \
    BASE_RULE_SET \
    ALLOW_SERVICE_DISCOVERY eth0 \
    ALLOW_SUBNETS eth0 \
    FORWARD_SUBNET_PROTECTIVE 192.168.0.0/16 ppp0 \
    POSTROUTING_MASQUERADE 192.168.0.0/16 ppp0 \
    >IPv4.rules

IPv6.rules:
    ./ipturntables.sh -6 \
    KERNEL_PARAMS accept_source_route,accept_redirects,accept_ra,forwarding \
    PROBE_KERNEL_MODS ip6_tables,nf_conntrack \
    RESET \
    BASE_RULE_SET \
    ALLOW_SERVICE_DISCOVERY eth0 \
    ALLOW_SUBNETS eth0 \
    ALLOW_DHCP_CLIENT ppp0 \
    >IPv6.rules

[...]

The resulting IPv?.rules files are created by iptables-save command. The KERNEL_PARAM and PROBE_KERNEL_MODS just ensure that specific features of iptables are available eg. connection tracking is a feature used in most stateful firewalls today have a look at the manual for details. The RESET function is pretty obvious, it flushes all chains of any table, deletes any user-customized rule, chain and resets all packet counters on any rule left.

BASE_RULE_SET

Creates a minimal firewall setup with a new set of default configurations like BLOCK, ANTI-FLOOD, ICMP, USER-IN & USER-OUT chains which are wired by the iptables standard chains. Most of these are reentrant and other calls may use them too to keep the footprint small. Access to local loopback (lo) is allowed. New packets are allowed to leave and related and established connections are allowed to return. Any invalid packet is blocked. Most ICMP types are allowed except the dangerous ones. Syn-flooding and port scanners are blocked by the ANTI-FLOOD chain and as a security measure (to avoid locking out yourself) ssh is allowed. Custom rules can be configured in the USER-(IN|OUT) chains.
The FORWARD chain is left empty but will be used when calling additional FORWARD_SUBNET or FORWARD_SUBNET_PROTECTIVE calls. OUTPUT allows local loopback and only new, related or established packets of any protocol to be sent but only after USER-OUT chain has been passed. Setting up a firewall with BASE_RULE_SET only without any proper additional rules will mostly end up in completely blocked traffic as the default policy setting of INPUT. OUTPUT and FORWARD is set to DROP.

ipturntables_base_rule_set

Figure B: Structural overview of a base ruleset. Click to enlarge.

All these chains are used by the INPUT or OUTPUT chain, to protect the router host. A closer look of a base rule set reveals:

Chain INPUT (policy DROP 27083 packets, 1419K bytes)
 pkts bytes target     prot opt in     out     source               destination         
  ...   ... ACCEPT      all  --  lo     *       0.0.0.0/0            0.0.0.0/0           
  ...   ... ACCEPT      all  --  *      *       0.0.0.0/0            0.0.0.0/0  state RELATED,ESTABLISHED
  ...   ... ICMP        icmp --  *      *       0.0.0.0/0            0.0.0.0/0           
  ...   ... BLOCK       all  --  *      *       0.0.0.0/0            0.0.0.0/0  state INVALID
  ...   ... ANTI-FLOOD  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0  tcp flags:0x17/0x02
  ...   ... ANTI-FLOOD  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0  tcp flags:0x17/0x04
  ...   ... LOCAL       all  --  *      *       0.0.0.0/0            0.0.0.0/0           
  ...   ... USER-IN     all  --  *      *       0.0.0.0/0            0.0.0.0/0  /* add your custom INPUT rules in the USER-IN chain! */
  ...   ... LOG         all  --  *      *       0.0.0.0/0            0.0.0.0/0  limit: avg 8/min burst 16 LOG flags 0 level 4 prefix "[IN4-DROP] "

Chain FORWARD (policy DROP 1338 packets, 168K bytes)
 pkts bytes target     prot opt in     out     source               destination         
  ...   ... LOG         all  --  *      *       0.0.0.0/0            0.0.0.0/0  limit: avg 8/min burst 16 LOG flags 0 level 4 prefix "[FW4-DROP] "

Chain OUTPUT (policy DROP 2 packets, 80 bytes)
 pkts bytes target     prot opt in     out     source               destination         
  ...   ... ACCEPT      all  --  *      lo      0.0.0.0/0            0.0.0.0/0           
  ...   ... ACCEPT      all  --  *      *       0.0.0.0/0            0.0.0.0/0  state RELATED,ESTABLISHED
  ...   ... USER-OUT    all  --  *      *       0.0.0.0/0            0.0.0.0/0  /* add your custom OUTPUT rules in the USER-OUT chain! */
  ...   ... ACCEPT      all  --  *      *       0.0.0.0/0            0.0.0.0/0  state NEW
  ...   ... LOG         all  --  *      *       0.0.0.0/0            0.0.0.0/0  limit: avg 8/min burst 16 LOG flags 0 level 4 prefix "[OU4-DROP] "

Notice that any of the default chains will log only some of the dropped packets to avoid flooding the logs with useless information. Any explicitly allowed packet won’t be logged the remaining dropped packets are still sufficient to give precious information about a pattern of an attack.
This optimized order of rules carries a fragile structure which breaks easily when modified inappropriately, for that reason the INPUT chains calls at last the USER-IN chain likewise the OUTPUT chains has a USER-OUT chain for user customized rules.

Some chains can be used in multiple scenarios because they aren’t limited to a protocol or port eg. the ANTI-FLOOD chain just consists of:

Chain ANTI-FLOOD (5 references)
 pkts bytes target     prot opt in     out     source               destination         
  ...   ... RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0            limit: avg 2/sec burst 5
  ...   ... LOG        all  --  *      *       0.0.0.0/0            0.0.0.0/0            LOG flags 0 level 4 prefix "[BLOCK] (ANTIFLOOD) "
  ...   ... DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0

First it checks if the time frame limit has been exceeded. If yes it’s considered as packet flooding and will be logged and finally dropped. If not it just returns. A rule can limit the call of ANTI-FLOOD by giving a precise filter criteria like tcp flags of 0x17/0x02 (syn-flood protection) or 0x17/0x04 (furtive port scanner) which can be found in the INPUT chain. Another example is the ICMP chain where ANTI-FLOOD is called to reduce echo-request packets.

Logging

Notice the LOG statement at the end of the ANTI-FLOOD chain which gives useful information about forwarded traffic which isn’t allowed!
The INPUT, OUTPUT, FORWARD and BLOCK chains also log unwanted packets. This is the default as it helps to detect different attack patterns when any blocked packet is logged but on the other hand it can result in a talkative log.
The BLOCK chain is called for any known unwanted packet (eg: invalid packets) but ANTIFLOOD is an exception as it uses a different log prefix to be able to distinguish the chain the packet got dropped at. Removing the LOG statements on these will reduce log size and just unknown packets on INPUT, OUTPUT and FORWARD will be logged but can make diagnostics harder when not any packet blocked is shown. So removing LOG statements is only recommended when your firewall setup has proofed to be rock solid but the logged packets on the default chains shouldn’t be removed anyway as they can give a good indication about still unknown attack scenarios or for traffic that got blocked unintended therefore the log prefixes for the standard chains differ from those intended to be blocked.
For the standard chains they look like IN(4|6)-DROP, OU(4|6)-DROP and FW(4|6)-DROP and they are only reached if the packet passed any chain and there was no explicit rule available to allow the packet. Here’s an example of a log output in which it’s easy to distinguish among intended packet drops and those which just fall through and get dropped at the end:

... router kernel: [...] [BLOCK] (INVALID) IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=57 ID=5170 PROTO=TCP SPT=443 DPT=43581 WINDOW=0 RES=0x00 RST URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=46 ID=1396 PROTO=TCP SPT=37915 DPT=23 WINDOW=34315 RES=0x00 SYN URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=242 ID=13673 PROTO=TCP SPT=59874 DPT=10778 WINDOW=1024 RES=0x00 SYN URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=109 ID=256 PROTO=TCP SPT=77 DPT=8088 WINDOW=16384 RES=0x00 SYN URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=239 ID=12533 PROTO=TCP SPT=60259 DPT=23 WINDOW=14600 RES=0x00 SYN URGP=0 
... router kernel: [...] [BLOCK] (INVALID) IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=46 ID=41048 DF PROTO=TCP SPT=22416 DPT=22 WINDOW=0 RES=0x00 RST URGP=0 
... router kernel: [...] [BLOCK] (INVALID) IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=46 ID=41055 DF PROTO=TCP SPT=22416 DPT=22 WINDOW=0 RES=0x00 RST URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=43 ID=18744 PROTO=TCP SPT=35002 DPT=7547 WINDOW=57567 RES=0x00 SYN URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=48 ID=1220 PROTO=TCP SPT=18156 DPT=23 WINDOW=56738 RES=0x00 SYN URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=44 TOS=0x00 PREC=0x00 TTL=45 ID=18329 PROTO=TCP SPT=20562 DPT=23 WINDOW=33178 RES=0x00 SYN URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=45 ID=64405 PROTO=TCP SPT=43383 DPT=23 WINDOW=55047 RES=0x00 SYN URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=43 ID=47391 PROTO=TCP SPT=63184 DPT=23 WINDOW=60872 RES=0x00 SYN URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=108 ID=256 PROTO=TCP SPT=77 DPT=3128 WINDOW=16384 RES=0x00 SYN URGP=0 
... router kernel: [...] [BLOCK] (INVALID) IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=55 ID=29709 PROTO=TCP SPT=443 DPT=47359 WINDOW=0 RES=0x00 RST URGP=0 
... router kernel: [...] [BLOCK] (INVALID) IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=55 ID=29711 PROTO=TCP SPT=443 DPT=47359 WINDOW=0 RES=0x00 RST URGP=0 
... router kernel: [...] [BLOCK] (INVALID) IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=55 ID=29712 PROTO=TCP SPT=443 DPT=47359 WINDOW=0 RES=0x00 RST URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=43 ID=47391 PROTO=TCP SPT=63184 DPT=23 WINDOW=60872 RES=0x00 SYN URGP=0 
... router kernel: [...] [IN4-DROP] IN=eth2 OUT= MAC=... SRC=... DST=... LEN=40 TOS=0x00 PREC=0x00 TTL=243 ID=54321 PROTO=TCP SPT=40154 DPT=1099 WINDOW=65535 RES=0x00 SYN URGP=0