Using Ferm to sweeten IPTables

What is ferm?

Ferm is a 'frontend' for iptables written in Perl. The best way to describe it is a firewall compiler (although it can do more than that, as we'll see later). Ferm provides a syntax that is simple, rich, and flexible for writing rules, ferm then generates a list of iptables rules. Rules created in ferm can run on any device with iptables as whatever you write in ferm will always correspond to an iptables-save output. Ferm can manage your systems firewall (apply the generated rules, run as a daemon etc) in which case there are optimizations applied. However, this is optional. You can use it to generate and simply iptables rules by simply piping in the ferm script and getting iptables out, allowing it to be used as an easy shorthand. Ferm is a single perl script. While packages are available for most linux distros, it is strongly recommended that you get your copy via developers repository. Managing IPTables by interactively running commands on live machines really sucks. It's lots of painful typing, feels very redundant, and you always run the risk of being locked out of your machine.

With ferm you can:

  • Store your rules into a single flat file with a nice C style syntax
  • Structure rules in code blocks
  • Organize your rule set into separate files and then you can put your highly modular template on github
  • Use command line backticks to discover variables
  • Generate rules using loops and functions
  • It's all up to you !

Getting ferm

$ git clone https://github.com/MaxKellermann/ferm

Installation is optional and you can just run the scripts out of ferm/src, otherwise you run a make install

If you install ferm from a package, it may ask you to "enable at boot", dont do that, it will overwrite your existing iptable rules with a barebones template.

Using ferm

When learning the basics of ferm, it's best to run as normal user and do a dry run with --noexec so that we don't accidentally clobber our firewall.

First, we get our existing iptables rules

$ sudo iptables-save > iptables.rules

Then we can pipe it into import-ferm

$ import-ferm < iptables.rules

import-ferm:
Translate iptables-save into a ferm.conf file, reads from standard input

 wget -qO- https://etherarp.net/static/iptables-example/rules.v4.txt | import.ferm

Older versions of import-ferm (such as in the ubuntu repos) had trouble parsing rules involving some iptables-extensions such as connaddr-src, the development version didnt have this issue.

ferm --remote:
Reads the ferm file and prints the compiled iptables-save rules

Looking at a ferm.conf file

@def $external_interface=`ip route show | grep default | grep link | awk '{print $3}'`;                         
@def $vpn_subnet=`ip route show | grep $(grep server </etc/openvpn/server.conf | grep -E "\b(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\." |awk '{print $2}') | awk '{print $1}'`;                                            
@def $vpn_interface=tun0;                                                                                       
@def $desired_resolver=`grep --max-count=1 nameserver /etc/resolv.conf | awk '{print $2}'`;                     
@def $vpn_proto=`grep -e "^proto" /etc/openvpn/server.conf | awk '{print $2}'`;                                 
@def $vpn_port=`grep --max-count=1 -e "^port" /etc/openvpn/server.conf  | awk '{print $2}'`;                    
@def $ssh_trusted=`echo $SSH_CONNECTION | awk '{print $1}'`;                                                    
@def &stateful_preamble()={                                                                                     
    mod conntrack ctstate INVALID DROP;                                                                         
    mod conntrack ctstate (ESTABLISHED RELATED) ACCEPT;                                                         
}                                                                                                               


domain ip                                                                                       
{                                                                                               
    table nat 
    {
        chain PREROUTING 
        {
                policy ACCEPT;
                protocol (tcp udp) daddr ! $desired_resolver dport 53 DNAT to "$desired_resolver:53";
        }
        chain POSTROUTING 
        {
                policy ACCEPT;
                saddr $vpn_subnet outerface $external_interface MASQUERADE;
        }
    }
    table filter 
    { 
        chain INPUT 
        {
           policy DROP;
           &stateful_preamble();
           protocol $vpn_proto dport $vpn_port ACCEPT;
           saddr ($ssh_trusted $vpn_subnet) protocol tcp dport 22  ACCEPT;
        }
        chain FORWARD 
        {
           policy DROP;
           &stateful_preamble();
           interface $vpn_interface outerface $external_interface ACCEPT;
        }
        chain OUTPUT policy ACCEPT;
    }
}

Variables

Here we can define static constants.
For example,

@def $vpn_interface=tun0;                                                                                       

Codeblocks

Looking at the example earlier (in the CSS terminal), let's look at how we would write it in ferm's syntax

interface (virbr0 virbr1) protocol (tcp udp) dport (67 53) ACCEPT;

We can also use C style code block to nest statements based on a common prefix. A slightly awkward example I thought of on the spot

mod conntrack {
    ctstate INVALID DROP;
    ctstate (ESTABLISHED RELATED) ACCEPT;
    ctstate NEW {
        saddr 192.0.2.64 {
            protocol tcp {
                dport 22 ACCEPT;
                dport 80 ACCEPT
            }
        }
     }
  }
    

Backticks

Backticks are very useful. They allow you to store the output of a command inside a variable. This is useful because a host can download a ferm.conf template, run ferm, and then discover information about their environment.

Here are some examples of backticks from the vpn example above.

@def $vpn_proto=`grep -e "^proto" /etc/openvpn/server.conf | awk '{print $2}'`;    

@def $vpn_port=`grep --max-count=1 -e "^port" /etc/openvpn/server.conf  | awk '{print $2}'`;   

Functions

Functions are kind of variables except they define entire blocks of ferm code rather than string literals.

Here is a simple example of a function

@def &stateful_preamble()={                                                                                     
    mod conntrack ctstate INVALID DROP;                                                                         
    mod conntrack ctstate (ESTABLISHED RELATED) ACCEPT;                                                         
}          

We then call it

chain FORWARD 
{
    policy DROP;
    &stateful_preamble();
    [...]
}

Of course, functions typically like arguments. Here is an example of a simple parameterized function

&def &LOGLIMIT($rate, $prefix) ={
    mod limit limit $rate LOG log-prefix $prefix;
}

Well that's probably enough for now, I might expand this post later. Hope it was of use.