I've always wanted to learn how to set up IPTables properly, and this weekend I finally learned. In my studies, I wrote a script that looks after my new network setup. Since I was just learning, I commented the heck out of the script, so I figured it might be useful for somebody else.
Typically, a firewall has three areas:
Internet -- The untrusted, world-wide Internet that we all love
LAN -- The totally trusted network where the PCs live
DMZ -- Demilitarized Zone -- The semi-trusted area where servers live
The idea of setting up a DMZ is to protect the LAN from attacks on the servers. Here is a brief summary of connections:
LAN -> DMZ -- Allowed
LAN -> Internet -- Allowed
LAN -> Firewall -- Allowed
Internet -> LAN -- Denied
Internet -> DMZ -- Allowed, but restricted
Internet -> Firewall -- Denied
DMZ -> LAN -- Denied
DMZ -> Internet -- Denied (although I allow NTP out to a specific server)
DMZ -> Firewall -- Denied
I eventually intend to make this script a little more flexible with DMZ -> Internet connections, so it'll be possible to install software. I'd also like to add a blacklist for SSH bruteforcers and stuff.
Anyways, here is the script I wrote to look after those connections. It seems to be working quite well:
#!/usr/bin/perl -w
use strict;
# start|stop|restart|status
my $ACTION = shift;
if($ACTION ne 'start' && $ACTION ne 'stop' && $ACTION ne 'restart' && $ACTION ne 'status')
{
die("Usage: $0 start|stop|restart|status\n");
}
# The program
my $IPTABLES = '/usr/sbin/iptables';
if($ACTION eq 'status')
{
print `$IPTABLES -L`;
exit(0);
}
# Hosts to block
my @BLOCKED;
push(@BLOCKED, "4.2.2.4");
# The mapping of DMZ ports to IPs. Since there's only one external IP,
# A single port can have a single address, but a single address can have
# multiple ports.
my %DMZ_ALLOWED_INCOMING;
$DMZ_ALLOWED_INCOMING{'TCP/80'} = '192.168.2.11'; # x86 Website
$DMZ_ALLOWED_INCOMING{'TCP/81'} = '192.168.2.10'; # x86 Forums
# Allows internal hosts to initiate a connection to an external host.
# Format: protocol/internal ip/external ip:port
# Any of the values can be replaced with '*' for a wildcard
my @DMZ_ALLOWED_OUTGOING;
push(@DMZ_ALLOWED_OUTGOING, "UDP/*/212.85.158.10:123"); # NTP Server
# The internal trusted network
my $LAN_IP = '192.168.1.1';
my $LAN_RANGE = '192.168.1.0/24';
my $LAN_INTERFACE = 'eth1';
# The external, untrusted network
my $INET_IP = '192.168.0.20';
my $INET_RANGE = '192.168.0.0/24';
my $INET_INTERFACE = 'eth0';
# The internal, untrusted network
my $DMZ_IP = '192.168.2.1';
my $DMZ_RANGE = '192.168.2.0/24';
my $DMZ_INTERFACE = 'vmnet0';
# The loopback interface
my $LO_IP = '127.0.0.1';
my $LO_RANGE = '255.0.0.0';
my $LO_INTERFACE = 'lo';
my $FROM_LAN = "-i $LAN_INTERFACE -s $LAN_RANGE";
my $FROM_DMZ = "-i $DMZ_INTERFACE -s $DMZ_RANGE";
my $FROM_INET = "-i $INET_INTERFACE";
my $TO_LAN = "-o $LAN_INTERFACE -d $LAN_RANGE";
my $TO_DMZ = "-o $DMZ_INTERFACE -d $DMZ_RANGE";
my $TO_INET = "-o $INET_INTERFACE";
# These three just simplify rules
my $TCP = '-p TCP';
my $UDP = '-p UDP';
my $ICMP = '-p ICMP';
my $ALL = '-p ALL';
my $NEW = '-m state --state NEW';
my $NEW_EST = '-m state --state NEW,ESTABLISHED';
my $EST_REL = '-m state --state ESTABLISHED,RELATED';
my $NEW_EST_REL = '-m state --state NEW,ESTABLISHED,RELATED';
my $SYNACK = '--tcp-flags SYN,ACK SYN,ACK';
my $ONLYSYN = '--tcp-flags ALL SYN';
my $DROP = '-j DROP';
my $ACCEPT = '-j ACCEPT';
my $LOG = '-j LOG --log-level DEBUG --log-prefix ';
my $CHECK_BLOCKED = '-j blocked';
my $CHECK_BAD_TCP = '-j bad_tcp';
# Ensure that ip forwarding is enabled
`echo '1' > /proc/sys/net/ipv4/ip_forward`;
if($ACTION eq 'stop' || $ACTION eq 'restart')
{
print "* Stopping IPTables\n";
print " -> Setting filter policies to ACCEPT\n";
print `$IPTABLES -t filter -P INPUT ACCEPT`;
print `$IPTABLES -t filter -P OUTPUT ACCEPT`;
print `$IPTABLES -t filter -P FORWARD ACCEPT`;
print " -> Setting nat policies to ACCEPT\n";
print `$IPTABLES -t nat -P PREROUTING ACCEPT`;
print `$IPTABLES -t nat -P POSTROUTING ACCEPT`;
print `$IPTABLES -t nat -P OUTPUT ACCEPT`;
print " -> Setting mangle policies to ACCEPT\n";
print `$IPTABLES -t mangle -P INPUT ACCEPT`;
print `$IPTABLES -t mangle -P FORWARD ACCEPT`;
print `$IPTABLES -t mangle -P PREROUTING ACCEPT`;
print `$IPTABLES -t mangle -P POSTROUTING ACCEPT`;
print `$IPTABLES -t mangle -P OUTPUT ACCEPT`;
print " -> Flushing rules\n";
print `$IPTABLES -t filter -F`;
print `$IPTABLES -t nat -F`;
print `$IPTABLES -t mangle -F`;
print " -> Deleting custom chains\n";
print `$IPTABLES -t filter -X`;
print `$IPTABLES -t nat -X`;
print `$IPTABLES -t mangle -X`;
print "* IPTables stopped\n\n";
}
if($ACTION eq 'start' || $ACTION eq 'restart')
{
print "\n";
print "* Setting up default policies\n";
print " -> INPUT policy -> DROP\n";
print `$IPTABLES -t filter -P INPUT DROP`;
print " -> OUTPUT policy -> DROP\n";
print `$IPTABLES -t filter -P OUTPUT DROP`;
print " -> FILTER policy -> DROP\n";
print `$IPTABLES -t filter -P FORWARD DROP`;
print "* Done setting up default policies\n\n";
print "* Creating chain to find invalid TCP packets\n";
print `$IPTABLES -t filter -N bad_tcp`;
print " -> Dropping all packets that don't have just 'SYN' set\n";
print `$IPTABLES -t filter -A bad_tcp $TCP ! $ONLYSYN $NEW $LOG 'New not SYN: '`;
print `$IPTABLES -t filter -A bad_tcp $TCP ! $ONLYSYN $NEW $DROP`;
print "* Bad TCP chain complete\n\n";
print "* Creating chain to find banned hosts\n";
print `$IPTABLES -t filter -N blocked`;
foreach my $blocked(@BLOCKED)
{
print " -> Blocking packets coming from $blocked\n";
`$IPTABLES -t filter -A blocked -s $blocked $DROP`;
print " -> Blocking packets going to $blocked\n";
`$IPTABLES -t filter -A blocked -d $blocked $DROP`;
}
print "* Banned host chain complete\n\n";
# Filter/INPUT is the filter for incoming packets destined for the router. We only
# really want to allow the LAN to get here, and we don't want to be able to initiate
# a connection with the LAN.
print "* Beginning filter->INPUT\n";
print " -> Checking for invalid TCP packets\n";
print `$IPTABLES -t filter -A INPUT $TCP $CHECK_BAD_TCP`;
print " -> Checking for banned hosts\n";
print `$IPTABLES -t filter -A INPUT $CHECK_BLOCKED`;
print " -> Allowing all loopback packets\n";
print `$IPTABLES -t filter -A INPUT -i $LO_INTERFACE $ACCEPT`;
print " -> Allowing all New and Established LAN Packets\n";
print `$IPTABLES -t filter -A INPUT -i $LAN_INTERFACE -s $LAN_RANGE $NEW_EST $ACCEPT`;
print " -> Logging/dropping everything that doesn't match the rules\n";
print `$IPTABLES -t filter -A INPUT $LOG 'INPUT packet died: '`;
print "* filter->INPUT complete\n\n";
# Filter/OUTPUT is the filter for the outgoing packets leaving the router. We only
# really want to allow the router to get to the LAN, and even then we only want to
# allow it if the LAN initiated the connection.
print "* Beginning filter->OUTPUT\n";
print " -> Checking for invalid TCP packets\n";
print `$IPTABLES -t filter -A OUTPUT $TCP $CHECK_BAD_TCP`;
print " -> Checking for banned hosts\n";
print `$IPTABLES -t filter -A OUTPUT $CHECK_BLOCKED`;
print " -> Allowing all loopback packets\n";
print `$IPTABLES -t filter -A OUTPUT -o $LO_INTERFACE $ACCEPT`;
print " -> Allowing all related and established LAN packets\n";
print `$IPTABLES -t filter -A OUTPUT -o $LAN_INTERFACE -s $LAN_RANGE $EST_REL $ACCEPT`;
print " -> Logging/dropping everything that doesn't match the rules\n";
print `$IPTABLES -t filter -A OUTPUT $LOG 'OUTPUT packet died: '`;
print "* filter->OUTPUT complete\n\n";
# Filter/FORWARD is the filter for packets that are crossing the router. This is trickier
# because everything needs to be able to forward to everything under the right conditions.
print "* Beginning filter->FORWARD\n";
print " -> Checking for invalid TCP packets\n";
print `$IPTABLES -t filter -A FORWARD $TCP $CHECK_BAD_TCP`;
print " -> Checking for banned hosts\n";
print `$IPTABLES -t filter -A FORWARD $CHECK_BLOCKED`;
print " -> Allowing the LAN to send connections anywhere\n";
print `$IPTABLES -t filter -A FORWARD $FROM_LAN $NEW_EST $ACCEPT`;
print " -> Allowing the DMZ to make / return connections (bad connections won't be routed)\n";
print `$IPTABLES -t filter -A FORWARD $FROM_DMZ $EST_REL $ACCEPT`;
print " -> Allowing the Internet to return connections to the LAN\n";
print `$IPTABLES -t filter -A FORWARD $FROM_INET $TO_LAN $EST_REL $ACCEPT`;
print " -> Allowing the Internet to initiate and return connections to the DMZ\n";
print `$IPTABLES -t filter -A FORWARD $FROM_INET $TO_DMZ $NEW_EST_REL $ACCEPT`;
foreach my $entry(@DMZ_ALLOWED_OUTGOING)
{
if($entry =~ m/^([a-zA-Z]+|\*)\/([0-9.]+|\*)\/([0-9.]+|\*):([0-9]+)$/)
{
my $protocol = $1;
my $internal = $2;
my $external = $3;
my $port = $4;
print " -> Allowing $internal to connect to $external on $protocol/$port\n";
$protocol = ($protocol eq "*") ? "" : "-p $protocol";
$internal = ($internal eq "*") ? "-s $DMZ_RANGE" : "-s $internal";
$external = ($external eq "*") ? "" : "-d $external";
$port = ($port eq "*") ? "" : "--dport $port";
print `$IPTABLES -t filter -A FORWARD $protocol $internal $external $port $ACCEPT`;
}
else
{
print " -> ** INVALID ENTRY: $entry\n";
}
}
print " -> Logging/dropping everything that doesn't match the rules\n";
print `$IPTABLES -t filter -A FORWARD $LOG 'FORWARD packet died: '`;
print "* filter->FORWARD complete\n\n";
# Nat/PREROUTING allows us to NAT packets before the routing decision has been made. This
# is useful when the Internet tries to send a packet to the DMZ. We change the destination
# address to the DMZ address before it is routed, then when the routing decision is made the
# packet ends up in the DMZ and everybody is happy.
print "* Beginning nat->PREROUTING\n";
foreach my $protoport(keys(%DMZ_ALLOWED_INCOMING))
{
if($protoport =~ m/^([a-zA-Z]+)\/([0-9]*)$/)
{
my $protocol = $1;
my $port = $2;
my $ip = $DMZ_ALLOWED_INCOMING{$protoport};
print " -> NATing external port '$port' on '$protocol' to DMZ ip '$ip'\n";
`$IPTABLES -t nat -A PREROUTING -p $protocol $FROM_INET --dport $port -j DNAT --to-destination $ip`;
}
else
{
print " -> **ERROR**: All NAT ports must be in the form of 'PROTOCOL/port', like 'TCP/80'\n";
print " -> Bad rule: $protoport\n";
}
}
print "* nat->PREROUTING complete\n\n";
# nat/POSTROUTING allows us to NAT packets after routing them. This is useful when a
# host behind the NAT is sending out a packet -- its source address is rewritten to point
# to the router. LAN packets going to the Internet have to be NATed for the internet to
# be able to response properly, and LAN packets going to the DNZ ought to be NATed to protect
# the identity of the internal host
print "* Beginning nat->POSTROUTING\n";
print " -> NATing LAN packets going to the Internet to the address $INET_IP\n";
print `$IPTABLES -t nat -A POSTROUTING -s $LAN_RANGE $TO_INET -j SNAT --to-source $INET_IP`;
print " -> NATing LAN packets going to the DMZ to the address $DMZ_IP\n";
print `$IPTABLES -t nat -A POSTROUTING -s $LAN_RANGE $TO_DMZ -j SNAT --to-source $DMZ_IP`;
foreach my $entry(@DMZ_ALLOWED_OUTGOING)
{
if($entry =~ m/^([a-zA-Z]+|\*)\/([0-9.]+|\*)\/([0-9.]+|\*):([0-9]+)$/)
{
my $protocol = $1;
my $internal = $2;
my $external = $3;
my $port = $4;
print " -> NATing outgoing packets from $internal to $external on $port/$protocol\n";
$protocol = ($protocol eq "*") ? "" : "-p $protocol";
$internal = ($internal eq "*") ? "-s $DMZ_RANGE" : "-s $internal";
$external = ($external eq "*") ? "" : "-d $external";
$port = ($port eq "*") ? "" : "--dport $port";
print `$IPTABLES -t nat -A POSTROUTING $protocol $internal $external $port -j SNAT --to-source $INET_IP`;
}
else
{
print " -> ** INVALID ENTRY: $entry\n";
}
}
print "* nat->POSTROUTING complete\n\n";
print "IPTables startup complete!\n\n";
}
<edit> Remove stupid smilies