Extending the packet filter

Output filtering

In the previous chapter we learned how to perform basic input filtering. We even created some rules to filter incoming packets and drop those that were deemed invalid. While this protects us from incoming packets it does nothing to protect the rest of the Internet from us. We can solve this very easily by adding a similar chain to that which we created to filter incoming TCP packets. The only difference is that this time we shall connect it to the OUTPUT chain.

/usr/local/sbin/config-firewall
iptables -N TCP-OUT-FILTER
iptables -A TCP-OUT-FILTER -j TCPCHECK

iptables -A OUTPUT -p tcp -j TCP-OUT-FILTER

Add the above lines of code to the end of the script and execute it as usual. Now when you display the Netfilter status you should see output similar to that given below.

lisa config-firewall
lisa iptables -L -v
Chain INPUT (policy ACCEPT 42582 packets, 5132K bytes) 
 pkts bytes target          prot opt in     out     source          destination 
   23  1484 TCP-IN-FILTER   tcp  --  any    any     anywhere        anywhere 
 
Chain OUTPUT (policy ACCEPT 7808 packets, 2021K bytes) 
 pkts bytes target          prot opt in     out     source          destination 
   17  3444 TCP-OUT-FILTER  tcp  --  any    any     anywhere        anywhere 
 
Chain LOGDROP (1 references) 
 pkts bytes target          prot opt in     out     source          destination 
    0     0 LOG             all  --  any    any     anywhere        anywhere            LOG level warning prefix `FIREWALL - DROP:' 
    0     0 DROP            all  --  any    any     anywhere        anywhere 
 
Chain TCP-IN-FILTER (1 references) 
 pkts bytes target          prot opt in     out     source          destination 
   23  1484 TCPCHECK        all  --  any    any     anywhere        anywhere 
 
Chain TCP-OUT-FILTER (1 references) 
 pkts bytes target          prot opt in     out     source          destination 
   17  3444 TCPCHECK        all  --  any    any     anywhere        anywhere 
 
Chain TCPCHECK (2 references) 
 pkts bytes target          prot opt in     out     source          destination 
    0     0 LOGDROP         tcp  --  any    any     anywhere        anywhere            tcp flags:!FIN,SYN,RST,ACK/SYN state NEW 
Information:
From now on, and that includes the output above, we shall not be listing the output for chains which we have seen before or which we are not interested in. In the above output we therefore chose to omit the FORWARD chain.
 

As before we can see that packets are being routed to the TCP-OUT-FILTER chain by examining the pkts column. It should be noted at this point that the number given here is the number of packets which were matched by this rule, not the number of packets which have been inspected.

More infrastructure

So far we have concentrated on what kind of packets we don't want coming in to or leaving our system. This is a good start but we are no closer to being able to set the INPUT and OUTPUT chain's policies to DROP as we have no rules to ACCEPT any packets.

Before we can start adding rules to our chains we need to lay some foundations so that we can keep our rules organised in a logical fashion. To achieve this we shall create four new chains called TCP-IN-REQ, for incoming requests, TCP-OUT-RESP, for outgoing responses, TCP-OUT-REQ, for outgoing requests, and finally TCP-IN-RESP for their incoming responses. This should allow us to more easily keep track of what we are allowing and in which direction.

Add the following lines to the script, between the lines shown in grey, so that they come before the creation of the TCP-IN-FILTER chain but after we have added the last rule to the TCPCHECK chain.

/usr/local/sbin/config-firewall
iptables -N TCPCHECK
iptables -A TCPCHECK -p tcp ! --syn -m state --state NEW -j LOGDROP
iptables -A TCPCHECK -m state --state INVALID -j LOGDROP

iptables -N TCP-IN-REQ
iptables -N TCP-OUT-RESP
iptables -N TCP-OUT-REQ
iptables -N TCP-IN-RESP

iptables -N TCP-IN-FILTER
iptables -A TCP-IN-FILTER -j TCPCHECK

We can now add the lines shown below to hook-up the chains we have just created to the TCP-IN-FILTER and TCP-OUT-FILTER chains which we created earlier. Again, existing lines are shown in grey.

/usr/local/sbin/config-firewall
iptables -N TCP-IN-FILTER
iptables -A TCP-IN-FILTER -j TCPCHECK
iptables -A TCP-IN-FILTER -j TCP-IN-REQ
iptables -A TCP-IN-FILTER -j TCP-IN-RESP

iptables -A INPUT -p tcp -j TCP-IN-FILTER

iptables -N TCP-OUT-FILTER
iptables -A TCP-OUT-FILTER -j TCPCHECK
iptables -A TCP-OUT-FILTER -j TCP-OUT-RESP
iptables -A TCP-OUT-FILTER -j TCP-OUT-REQ

iptables -A OUTPUT -p tcp -j TCP-OUT-FILTER
Information:
We have assumed that you are firewalling a server so we have ordered the chains to ensure that the heaviest traffic chains will come first. If you are firewalling a workstation you will probably want to switch the order of the chains so that outgoing requests and their incoming responses come first.
 

All packets entering or leaving the system will now traverse not just the TCPCHECK chain but, assuming they were not already dropped, they will also traverse the appropriate request and response chains, depending on their direction.

You can check that everything is working by comparing the relevant lines of the Netfilter status with the example given below. As before we have omitted irrelevant output.

lisa config-firewall
lisa iptables -L -v
Chain INPUT (policy ACCEPT 44186 packets, 5286K bytes) 
 pkts bytes target          prot opt in     out     source          destination 
   13   964 TCP-IN-FILTER   tcp  --  any    any     anywhere        anywhere 
 
Chain OUTPUT (policy ACCEPT 8516 packets, 2108K bytes) 
 pkts bytes target          prot opt in     out     source          destination 
    7   924 TCP-OUT-FILTER  tcp  --  any    any     anywhere        anywhere 
 
Chain TCP-IN-FILTER (1 references) 
 pkts bytes target          prot opt in     out     source          destination 
   13   964 TCPCHECK        all  --  any    any     anywhere        anywhere 
   13   964 TCP-IN-REQ      all  --  any    any     anywhere        anywhere 
   13   964 TCP-IN-RESP     all  --  any    any     anywhere        anywhere 
 
Chain TCP-IN-REQ (1 references) 
 pkts bytes target          prot opt in     out     source          destination 
 
Chain TCP-IN-RESP (1 references) 
 pkts bytes target          prot opt in     out     source          destination 
 
Chain TCP-OUT-FILTER (1 references) 
 pkts bytes target          prot opt in     out     source          destination 
    7   924 TCPCHECK        all  --  any    any     anywhere        anywhere 
    7   924 TCP-OUT-RESP    all  --  any    any     anywhere        anywhere 
    7   924 TCP-OUT-REQ     all  --  any    any     anywhere        anywhere 
 
Chain TCP-OUT-REQ (1 references) 
 pkts bytes target          prot opt in     out     source          destination 
 
Chain TCP-OUT-RESP (1 references) 
 pkts bytes target          prot opt in     out     source          destination 

Simple services

Now that we have the infrastructure in place we can start to build the rules which will ACCEPT traffic. In this section we shall only cover simple services which use a single port for communications such as HTTP and SSH. We shall cover complex services which use multiple ports, such as FTP, in the next section.

As HTTP is probably the most widely used protocol on the Internet let's start by allowing access to an HTTP server running on the local machine. We can accomplish this by adding the following lines to our firewall configuration script.

/usr/local/sbin/config-firewall
iptables -N TCP-OUT-REQ
iptables -N TCP-IN-RESP

iptables -A TCP-IN-REQ -p tcp --dport http -m state --state NEW -j ACCEPT
iptables -A TCP-IN-REQ -p tcp --dport http -m state --state ESTABLISHED -j ACCEPT
iptables -A TCP-OUT-RESP -p tcp --sport http -m state --state ESTABLISHED -j ACCEPT

iptables -N TCP-IN-FILTER
iptables -A TCP-IN-FILTER -j TCPCHECK

The above three rules allow new incoming request packets, packets which are part of an established incoming connection, and packets which are part of the already established response connection respectively. The first two rules use the dport sub-matcher of the protocol matcher to ensure that the request is destined for the HTTP port on the local machine. The third rule uses the sport sub-matcher to ensure that the outgoing response is coming from the local HTTP server. As you can see most commonly used protocols are known to the sub-matchers by name. For less well known protocols a port number can be specified. All the rules also use the state matcher which we met earlier.

Information:
We could have combined the NEW and ESTABLISHED state matches by combining them in to a comma separated list. We have separated them in this example to make it easier to apply the rate limiting code which we shall discuss later.
 

If we want to allow the local machine to access web servers hosted on other machines, and we probably do, then we can use the rules given below. Add them to the script below the rules which we inserted earlier.

/usr/local/sbin/config-firewall
iptables -A TCP-OUT-REQ -p tcp --dport http -m state --state NEW         -j ACCEPT
iptables -A TCP-OUT-REQ -p tcp --dport http -m state --state ESTABLISHED -j ACCEPT
iptables -A TCP-IN-RESP -p tcp --sport http -m state --state ESTABLISHED -j ACCEPT

As you can see the rules are the same for both the client-side and the server-side, the only difference is which chain they are added to. In this case the first two rules represent the request, so they are added to the outgoing request chain, while the third rule represents the response, so is added to the incoming response chain.

Complex services

We mentioned earlier that some services, such as FTP, use more complex protocols which require the use of multiple related connections. In the case of the FTP protocol a separate connection is used for the transfer of data. The port number for this second connection can vary widely. It can either be the default data port, which for FTP is port 20, it can specified during the negotiation either by the requesting host, in which case the server will open a connection to the specified port, or by the server, in which case the client will open a connection to the specified port. This is clearly going to require a different approach to that used for the simple protocols above.

Lets continue with the FTP example we have started by adding the following rules to the firewall configuration script just below the ones we added earlier. These rules will allow access to the control port of the FTP server and hence are exactly the same as those used for simple servers.

/usr/local/sbin/config-firewall
iptables -A TCP-IN-REQ   -p tcp --dport ftp -m state --state NEW         -j ACCEPT
iptables -A TCP-IN-REQ -p tcp --dport ftp -m state --state ESTABLISHED -j ACCEPT
iptables -A TCP-OUT-RESP -p tcp --sport ftp -m state --state ESTABLISHED -j ACCEPT
Information:
You might want to leave a blank line between the blocks as this part of the script will get quite complex by the end of this tutorial. You should also be commenting the script as we recommended earlier so that when you come back to it in several months time you will have some idea what is going on!
 

Now that our clients can connect to the control port of our FTP server we need to address the problem of the data port. If we knew that data was always requested on our data port we could simply open the data port to all connections in the same way that we would open any other port. Unfortunately for us the FTP protocol is more complex than that and requires the client to open the data connection from its data port to a port which the server specifies. Knowing this we could decide to open all ports on our server to connections which come from the FTP data port but this would be foolish as anyone could use this to bypass our firewall altogether, it also wouldn't address the problem of passive transfers. What we really need to be able to do is watch the FTP control port and see which ports are going to be used and then open just those.

For that reason the Netfilter developers created the conntrack interface and related modules. One of these modules provides us with exactly the functionality which we require here, in this case the FTP conntrack module. There are other modules available for various protocols including, but by no means limited to, SCTP, TFTP and PPTP.

Use of the conntrack subsystem is very simple. In fact we have already been using it! Every time we have used the state matcher we have been making use of the connection tracking information maintained by the Netfilter connection tracking sub-system. In addition to the basic information which we have been exploiting the various conntrack modules also keep track of the ports which have been specified for use during conversations on the control connections of their related protocols.

Several matchers are capable of making use of this information to provide additional functionality which allows rules to be written to act on the associated packets accordingly. The most used of these is the RELATED state match. It can be used in place of the NEW state to allow access to ports required for related communication channels. Another useful matcher, which we shall also be using here, is the helper matcher which is used to determine which of the conntrack extension modules identified this as related traffic.

Now that we know how to access the state information maintained by the conntrack subsystem and the FTP conntrack module, we can create some appropriate rules to add to our script.

/usr/local/sbin/config-firewall
iptables -A TCP-OUT-RESP -m helper --helper ftp -m state --state RELATED     -j ACCEPT
iptables -A TCP-OUT-RESP -m helper --helper ftp -m state --state ESTABLISHED -j ACCEPT
iptables -A TCP-IN-RESP -m helper --helper ftp -m state --state RELATED -j ACCEPT
iptables -A TCP-IN-RESP -m helper --helper ftp -m state --state ESTABLISHED -j ACCEPT

The first pair of rules, shown above, is used for the case where the server is opening the connection to a data port specified by the client. The second pair of rules covers the case where the client is opening a connection to a port specified by the server. We need both of these pairs of rules to allow for both active and passive transfers respectively. You should also note that the RELATED state is essentially the same as the NEW state in that it only matches the first packet on a connection. If the ESTABLISHED matches were omitted from the above code only one packet would make it through the firewall.

Caution:
An interesting side effect of having to cover the case of both passive and active transfers is that the rules are the same regardless of whether it is a server or a client which we are configuring. These rules only need to be included once therefore even if we are allowing FTP access as both a client and a server. You will still need rules for the client side of the control connection however.
 

If you wish to be able to open FTP control connections from the local machine to remote servers then you will need to add the control port rules above again with the direction part of the chain specifier reversed in the same way as we did for the HTTP protocol earlier. For your convenience the necessary code is given below.

/usr/local/sbin/config-firewall
iptables -A TCP-OUT-REQ -p tcp --dport ftp -m state --state NEW         -j ACCEPT
iptables -A TCP-OUT-REQ -p tcp --dport ftp -m state --state ESTABLISHED -j ACCEPT
iptables -A TCP-IN-RESP -p tcp --sport ftp -m state --state ESTABLISHED -j ACCEPT

You should now be able to add similar entries to the configuration script to allow access to all the other TCP based protocols which you use.