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.