Taming the Wild Netfilter
Before we look at examples, let's set some things up. Scripts are nice, and one that runs from rc.local (wherever that is on your system) is always good. So let's write an rc.iptables script as part of our example to implement Netfilter during bootup (see Listing 1). (Note: all iptables rules will start $IPT and should continue unbroken to the end of the line, i.e., the next command. Rules should not be broken on a line.)
We've set up some variables to start, stopped forwarding traffic through the system, then inserted some modules. The ip_tables module allows us to start writing rules. The ip_nat_ftp module is necessary if we are using the NAT table to do SNAT or MASQUERADE (covered below) to allow active FTP. The ip_conntrack_ftp allows connection tracking of FTP. This module automatically loads the ip_conntrack module because it is dependent on ip_conntrack. If you don't need or want the ip_conntrack_ftp module, but want to make sure your firewall does IP defragmentation (a good idea), you can substitute ip_conntrack for ip_conntrack_ftp. Let's continue with our script:
for i in filter nat mangle do $IPT -t $i -F $IPT -t $i -X done
The above lines will flush (-F) all rules in the chain shown as the argument to -F. Since no chain name was given as an argument, -F will flush all chains. Note that this has to be done for each table, hence the loop. If you aren't using a particular table, you can delete it from the for statement. As each loop starts, you will note that a new module has been loaded: first the iptable_filter module, then the iptable_nat module and finally the iptables_mangle module. If you remove mangle from the loop, the iptables_mangle module will not be loaded. Unused modules can be removed at your leisure.
Under ipchains, you would have used something like
ipchains -F -X
to accomplish the same thing.
Before we continue, let's assume the following setup: a home user and three systems that access the Internet through our firewall/workstation PC. Access is via dial-up (ppp0). If your setup is different, substitute your external interface for ppp0. The systems are not offering any services outside their own network, which is on eth0. We do, however, want all systems inside to be able to surf the Web. For this first example, we'll assume a dynamic IP from an outside ISP [see Listing 2, rc.iptables.dynamic, at ftp.linuxjournal.com/pub/lj/listings/issue89/4815.tgz].
Since our firewall PC is also a workstation, it will be originating and terminating its own traffic. While not a good idea for a firewall (these should be dedicated systems) on a home network, we really don't need a dedicated firewall. With this in mind, remember that one difference between iptables and ipchains is in the INPUT, FORWARD and OUTPUT chains. In ipchains, packets traversing the FORWARD chain came from INPUT and went through FORWARD to OUTPUT, so we could locate our rules in the INPUT chain and be safe for packets going to the FORWARD chain. The iptables implementation only uses INPUT for the local system and FORWARD for the other systems. In our case we need identical rules in each of the FORWARD and INPUT chains. To prevent duplicating a lot of rules, let's create a user chain called tcprules and call it from both INPUT and FORWARD. Continuing our script then:
$IPT -t filter -N tcprules
The ipchains equivalent of this rule would be the same, excluding the -t filter option of the incantation:
ipchains -N tcprulesNow a little Netfilter magic. We want to prevent someone from connecting to our systems from the outside but permit connections to the outside to be established by our users. The following rules make use of Netfilter's stateful capability:
$IPT -t filter -A tcprules -i ppp+ -m state --state ESTABLISHED,RELATED -j ACCEPT $IPT -t filter -A tcprules -i ! ppp+ -m state --state NEW -j ACCEPT $IPT -t filter -A tcprules -i ppp+ -m state --state NEW,INVALID -j DROPIf you want to do something similar in ipchains, the closest you could come is to deny syn packets to the ppp+ interface:
ipchains -A input -i ppp+ ! -y -j DENYA couple of quick notes at this point. First, the “!” negates whatever follows it. So ! ppp+ is the same as specifying all other interfaces (in the case of our home user, lo and eth0). The “+” on the end of the ppp tells Netfilter this rule applies to all ppp interfaces.
As for the ESTABLISHED, RELATED, NEW and INVALID arguments, they are more than they appear to be. ESTABLISHED permits traffic to continue where it has seen traffic before in both directions. ESTABLISHED obviously applies to TCP connections but also to UDP traffic, such as DNS queries and traceroutes as well as ICMP pings. In fact, packets are first checked to see if the connection exists in the connection tracking table (/proc/net/ip_conntrack). If so, the chains aren't run, the original rule is applied and the packets pass. In some cases, Netfilter is faster than its predecessor because of this check. The RELATED argument covers a multitude of sins. This argument is applied to active FTP, which opens a related connection on port 20, but also applies to ICMP traffic related to the TCP connection. The NEW argument applies to packets with only the SYN bit set (and the ACK bit unset). The INVALID applies to packets that have invalid sets of options, as in an XMAS tree scan.
The above rules permit the internal systems to pass new connections internally and externally, but don't permit incoming new or invalid packets.
Since we're going to be masquerading our network behind our firewall, we'll need to set up a masquerading rule. This process is very similar to the one used in ipchains. Assuming our internal network is 192.168.0.0/24, we'll use the following rule:
$IPT -t nat -A POSTROUTING -o ppp+ -s 192.168.0.0/24 -d 0/0 -j MASQUERADE
One difference ipchains users should note is the use of -o ppp+ instead of -i ppp+. This is because with ipchains, we dealt with interfaces as -i. With iptables, -i stands for input interface, and -o stands for output interface. If you mistakenly use -i ppp+ in the line above, no masquerading will take place. In fact, the rule should never match at all.
Let's finish our script by implementing the tcprules above where they are needed, implementing our filter-table policies and turning ip_forwarding back on:
$IPT -t filter -A INPUT -j tcprules $IPT -t filter -A FORWARD -j tcprules $IPT -t filter -P INPUT DROP $IPT -t filter -P FORWARD DROP echo 1 > /proc/sys/net/ipv4/ip_forward
By default, the filter-table policies are all ACCEPT. Given the stateful nature of Netfilter, this affords sufficient protection, unlike ipchains. If you think about the stateful rules we've implemented, you'll see that no harm should come with the default policy; in fact, nothing should ever arrive at the default policy. But some believe that it's always better to play it safe, so we can drop anything not addressed already. Note that the policy doesn't have a target per se, so we don't use -j. This is also why only built-in targets can be policies—policies aren't really targets. If you really want REJECT as your policy, you'll need to add something like the following:
$IPT -t filter -A tcprules -i ppp+ -j REJECT --reject-with icmp-host-unreachableThe above rule applies to all packets coming in on your ppp interface. Make sure this rule is your last rule because nothing will pass this rule.
While you could make the rule
$IPT -f filter -A tcprules -j REJECT --reject-with icmp-host-unreachable
you will find that your internal hosts are also blocked because this rule will apply to all interfaces (something you should keep in mind with policies as well).
At this point, I should mention that, unlike ipchains, a target match does not necessarily terminate a chain. For example, if you use the following rule in tcprules:
$IPT -t filter -I INPUT -p ICMP -m icmp --icmp-type echo-request -m limit --limit 1/minute -j LOG --log-prefix "ICMP-packet "
the next rule (which may or may not also match this packet) will be processed. If a rule match does not drop, reject, accept or queue the packet and is not a return, the chain will continue.
The ipchains crowd will note that under iptables, LOG is a target by itself rather than a simple -l option following the target, as with ipchains. This permits some flexibility, as can be seen from the above rule. The limit match prevents someone malicious from flooding your logs. The LOG target can send a prefix with its message so that it's easy to grep from the logs.
Before we move on, all ipchains users know you can view the chains by using something like
ipchains -L -n
This will show you the chains. With Netfilter, we need to look at the chains, table by table:
iptables -t filter -L -n iptables -t nat -L -n iptables -t mangle -L -nYou can add a -v to get even more information on each rule:
iptables -t filter -L -nvNext is an example to demonstrate the new SNAT and DNAT. If you understand the mangle table and its use, you probably don't need this article and will be e-mailing me about any errors you've noticed.
In this example, we'll assume our home user has a broadband connection and is using eth0 for the internal network and eth1 for the external network. The script begins the same as before, but when we get to the tcprules, we're going to do something a little different. Here, we're going to assume the user has a static IP of 188.8.131.52, which gives us more leeway [see Listing 3, rc.iptables.static, at ftp.linuxjournal.com/pub/lj/listings/issue89/4815.tgz]. In fact, we can also assume the user has his own domain name and is running his own e-mail server on port 25, which is located on an internal system with IP 192.168.0.2 (the firewall is 192.168.0.1). His DNS entry points to 184.108.40.206 as his mail server, as shown in Listing 4.
The first two tcprules are the same as in the first example. But before we drop all other connections, we accept connections on port 25. Then, in the nat table, we take the port 25 connection and forward it to another internal host on the same port. You can do this for all your connections. Remember that if you do this forward for DNS, you need to forward UDP as well as TCP. In fact, unless someone outside is going to do zone transfers, you can drop the TCP part and only pass UDP.
Notice that now, instead of using MASQUERADE as a target for outgoing connections, we're using SNAT. In case you wondered, the S stands for source, which is what is being changed. In the case of DNAT, we changed the destination of the packet. The argument --to-source can take a range of IPs, so your firewall can look like several hosts. If you have five usable IPs from your ISP, you can use all five as outgoing. Then you can point different services to different IPs and have up to five systems behind your firewall answering DNS queries, hosting web sites, accepting mail, etc. Netfilter will also allow you to do rudimentary load balancing. When a range of destinations is used with DNAT, the system with the least number of connections (not necessarily the system with the lightest load) gets the connection.
About the only other thing you may want to know to get started is how to increase the maximum number of connections tracked. This number is arrived at by default depending on the amount of RAM your system has. However, the number is conservative and can be increased. You can find this number this way:
On my system with 256MB of RAM, the number is 16376.