ferm - a firewall rule parser for linux
ferm options inputfiles
ferm compiles ready to go firewall-rules from a structured
rule-setup. These rules will be executed by the preferred kernel
interface, such as ipchains(8)
and iptables(8).
Besides just executing all rules in one command, the obvious gain is the possibility to provide a structured description of a firewall. No need anymore for tedious typing all firewalls into custom scripts, you can now write logically and coherent rules using a C-style nesting structure, and let ferm create all rules for you.
ferm will also aid in modularizing firewalls, because it creates the possibility to split up the firewall into several different files, which can be reloaded at will, so you can dynamically adjust your rules.
ferm, pronounced ``firm'', stands for ``For Easy Rule Making''.
The structure of a proper firewall file looks like simplified C-code. Only a few syntactic characters are used in ferm- configuration files. Besides these special caracters, ferm uses 'keys' and 'values', think of them as options and parameters, or as variables and values, whatever.
With these words, you define the characteristics of your firewall. Every firewall consists of two things: First, look if network traffic matches certain conditions, and second, what to do with that traffic.
You may specify conditions that are valid for the kernel interface program you are using, probably iptables(8). For instance, in iptables, when you are trying to match tcp packets, you would say:
iptables --protocol tcp
In ferm, this will become:
protocol tcp;
Just typing this in ferm doesn't do anything, you need to tell
ferm (actually, you need to tell iptables(8)
and the kernel) what
to do with any traffic that matches this condition:
iptables --protocol tcp -j ACCEPT
Or, translated to ferm:
protocol tcp accept;
Noticed the ; character? We're getting to that now, because there are some special characters in ferm that make life easy.
Here's a list of the special characters:
This character *makes* the rule. It gathers all the information, all parameters and targets, special things or whatever, that currently is 'valid', and tries to make a decent rule out of it. ferm will do nothing without this character!
Example:
proto tcp ACCEPT;
THis example shows a single rule, defined by two keys and one value.
Anything defined before this block will still be available within all rules inside this block. You can nest blocks in blocks as far as you like. For every rule defined in this block the values defined before this block will apply. Usually you would define an often used parameter as the protocol in front of this block, and anything special inside it.
You can put as many rules (using the <;> character) as you like insode this block. but there should always be one or more, although you will get away with none. Not very usefull except for when you frequently edit you config file, and might want to leave a chain empty.
Since the nesting block is left associative, it cannot be bound to keys defined after the block.
Example:
chain INPUT proto tcp { syn DENY; ACCEPT; }
This block shows two rules inside a block, which will both be merged with anything in front of it, so you will get two rules:
iptables -A INPUT -p tcp -y -j DENY iptables -A INPUT -p tcp -j ACCEPT
set IF eth0 set %IF ACCEPT set TARGET %IF
will result in this:
%IF = eth0 %eth0 = ACCEPT %TARGET = eth0
If you want to put multiple arguments into a variable, you should do it like this:
%IFS = "eth0,eth1,ppp0"
This way, ferm will recognize it being a list of values, and split it whenever needed. If you use spaces, ferm won't recognize it and think its just a value with spaces. The comma tells ferm to split it up when needed.
Example:
proto ( tcp udp icmp )
this will result in three rules:
... -p tcp ... ... -p udp ... ... -p icmp ... Only values can be 'listed', so you cannot do something like this:
proto tcp ( ACCEPT LOG );
but you can do this:
chain (INPUT OUTPUT FORWARD) proto (icmp,udp,tcp) DENY;
(which will result in nine rules!)
Values can be separated either by spaces or commas. The array symbol is both left- and right-associative, in contrast with the nesting block, which is left-associative only.
#
These symbols glue all the keywords into a structure, which allows you to specify some keys only a few times, and let them apply to any key/value pairs defined within an entire block, for instance:
proto tcp { dport 22 ACCEPT; syn DPORT 0:1023 DENY; } ACCEPT;
Now here, the 'proto tcp' is valid within the block, but not anymore after is, resulting in:
... -p tcp --dport 22 -j accept ... -p tcp -y --dport 0:1023 -j deny ... -j accept # note '-p tcp' is not in here!
Some important notes:
- Ferm inserts the rules 'chronologically', so the first rule will be inserted before the second one.
- Anything defined within a block is no longer valid when that block ends.
- Everything defined within the current block that is 'effectuated', will be no longer defined immediately after that point.
- Everything defined before a block is undefined when this block closes.
If you do not understand this, don't worry, it alle becomes clear by itself.
Two types of keys exist:
Firewall keys define a set of firewall packet matching criteria that is supported by the kernel backend. They look like 'name value' pairs or like 'switch'. For instance:
proto tcp
or:
syn
A 'name value' pair lets you fill in a value for a certain condition you would like to match packets against, switches are like on/off light switches on the wall, if you specify a switch, you turn paket matching for whatever the switch stands, on. In the latter example, you turn SYN-packet matching on for this rule.
Both types can optionally be preceded by a !. This will be handled that you don't want something to be matching it:
!syn
or:
! syn
Means you want packets which *don't* have the syn-flag set to be matched. Or even:
proto ! tcp
Means you want to match *anything but* packets from the tcp protocol.
Read iptables(8)
or ipchains(8)
to see where the ! can be used.
Using option keys alter the behaviour of ferm; they can be used to e.g. clear chains before use, or turn off certain sanity checks.
Example:
option verbose
This option makes ferm show a lot of information about what it is doing.
The syntax is very simple, let's start with a simple example:
chain input { proto tcp ACCEPT; }
This will add a rule to the predefined input chain, matching and accepting all tcp packets. Ok, let's make it more complicated:
chain (input,output) { proto (udp,tcp) ACCEPT; }
This will insert 4 rules, namely 2 in chain input, and 2 in chain output, matching and accepting both udp and tcp packets. Normally you would type this for ipchains(8):
ipchains -A input -p tcp ACCEPT ipchains -A output -p tcp ACCEPT ipchains -A input -p udp ACCEPT ipchains -A output -p udp ACCEPT
Note how much less typing we need to do? :-)
Basically, this is all there is to it, although you can make it quite more complex. Something to look at:
chain input policy ACCEPT { destination 10/8 port ! ftp goto mychain sport :1023 tos 4 settos 8 mark 2; destination 10/8 port ftp DENY; }
My point here is, that *you* need to make nice rules, keep them readable to you and others, and not make it into a mess.
It would aid the reader if the resulting firewall rules were placed here for reference. Also, you could include the nested version with better readability.
Try using comments to show what you are doing:
# this line enables transparent http-proxying for the internal network: proto tcp if eth0 daddr ! 192.168.0.0/255.255.255.0 dport http REDIRECT 3128;
You will be thankfull for it later!
chain input policy ACCEPT { interface (eth0,ppp0) { # deny access to notorius hackers, return here if # no match was found to resume normal firewalling goto badguys;
protocol tcp goto fw_tcp; protocol udp goto fw_udp; } }
The more you nest, the better it looks. Make sure the order you specify is correct, you would not want to do this:
chain forward { proto ! udp DENY; proto tcp dport ftp ACCEPT; }
because the second rule will never match. Best way is to specify first everyting that is allowed, and then deny everything else. Look at the examples for more good snapshots. Most people do something like this:
proto tcp { dport ( ssh http ftp ) ACCEPT; dport 1024:65535 ! syn ACCEPT; DROP; }
To make life easy, ferm allows you to use shorthands for most keywords. A list of shorthand notations is available at the end of this section.
What kind of value you provide for a keyword depends on the keyword entirely, e.g. 'protocol' expects 'tcp', 'udp' or 'icmp', 'log-prefix' expects a value like '``whoops, someone rang the doorbell''' and 'destination-port' can accept values like 'http', '80' or '0:1023'. Take a look at the kernel backend program manual for possible values and how they look like.
Note you may put a value in single quotes or double quotes, if this may be required because a value contains spaces:
log-prefix "Dropped tcp package: "
Please keep in mind that some characters have special meaning, so it might be wise to refrain from using any other character then letters and digits and spaces unless you need them and know what you're doing. Take a look at VARIABLES and SHELL ESCAPES for more information about that.
input
, output
or forward
, or user-defined
chains.
-i
switch in
ipchains(8)
and iptables(8).
ipchains(8)
hasn't got this
parameter.
Note that you need to specify a protocol, before you can use ports, that is because not all protocols support the ideas of ports.
Here are some examples of valid addresses:
192.168/8 (identical to the next one:) 192.168.0.0/255.255.255.0 my.domain.com
And some examples of valid ports/ranges:
80 http ssh:http 0:1023 which is equivalent to :1023 1023:65535 which is equivalent to 1023:65535
-h
icmp''. Examples: ping, pong.
02 04 08 10
ifconfig(8)
if you want to find the MTU for
your system (the default is usually 1500 bytes).
Fragments are frequently used in DOS attacks, because there is no way of finding out the origin of a fragment packet.
To avoid ambiguity, always specify the policies of all predefined chains explicitly.
See also log-[level|prefix|tcp-sequence|tcp-options|ip-options]
ferm also supports internal variables. This may come in handy if you wish to define often used parameters in advance, making the ferm configuration files even more easy to understand.
Setting variables is very easy with the set command. Here's some examples:
set EXTERAL_IP "111.22.33.44" set INTERNAL_IP '10.0.0.1'
Both these statements set the variable to what is in between the quotes, you may afterwards refer to them like this:
chain input daddr ! %EXTERNAL_IP DROP;
The value of the variable will then be inserted into the rule and passed to the firewall program.
the set command can actally be abused even more, since the following statements also work:
set A "1" set B %A set %A "2"
After these statements, variable %A yields value ``1'', variable %B holds the value ``1'', and the variable %1 holds the value ``1'' also.
More importantly, these variables can be used to store arrays or lists of values:
set DNSSERVERS "111.2.33.1,111.2.33.2"
When this variable is inserted into a configuration file, the rule that it applies to will automatically be split up into two different firewall rules for each IP number given in the list.
Here's some even more complicated stuff that works:
set INTERNALINTERFACES "eth0,eth1,eth2" set EXTERNALINTERFACES "ppp0,tunl0" set INTERFACES "%INTERNALINTERFACES,%EXTERNALINTERFACES,lo"
NOTE: Beware of mixing '' string values within new string values, because the trailing ' might be concatenated with another one in the variable that you are including it. Take a look at this:
set IF1 'eth0' set IF2 'eth1' set IFS '%IF1,%IF2'
Variable IFS will now contain the value ''eth0','eth1'' and that is probably not what you want. Better do this:
set OF1 "eth0" set OF2 "eth1" set OFS "%OF1,%OF2"
Which will result in variable OFS holding the value ``eth0,eth1'', which will be split up correctly, namely into ``eth0'' and ``eth1''.
Ferm supports shell escaping in two ways. First, you may insert a shell escaped string into a set command, second, you may insert a shell escaped string into any place of a value.
There is a fundamental difference in this. Ferm will handle shell escapes itself when they are used in a set construction, so the variable then contains the value that was returned from the shell escape. You may later refer to this value again without the command being executed again.
When you use a shell escaped string as a value without it being in a set statement, the exact string is just copied in the generated rule, and when parsing is finished, ferm will call the shell with the entire rule, and thus the shell escaped string. Only at this moment, the shell will execute the string and insert the value back into the kernel interface program. Thus, ferm will never see the real value of that.
Examples:
set DNSSERVERS `grep nameserver /etc/resolv.conf | awk '{print $2}'` chain input proto tcp saddr %DNSSERVERS ACCEPT;
This way, ferm will interpret the value for DSSERVERS itself, put a separating comma between multiple values if needed, and store this information in the variable DNSSERVERS. The output will be like:
iptables -t filter -A INPUT -p tcp -s 192.168.0.1 -j ACCEPT iptables -t filter -A INPUT -p tcp -s 192.168.0.2 -j ACCEPT
Otherwise, when you include a shell escape as a regular value in between other ferm-statements:
chain input proto tcp saddr `grep nameserver /etc/resolv.conf | awk '{print $2}'` ACCEPT;
The shell escape is not parsed directly, but passed along with the, e.g. iptables command, and subsequently, the shell will insert whatever that value may become itself:
iptables -t filter -A INPUT -p tcp -d `grep nameserver /etc/resolv.conf | awk '{print $2}'` -j ACCEPT
Note that if the shell escape here yields more lines, something could go wrong here easily. You are warned! Better not make ferm SUID too I guess ;-)
Here's a complete list of possible shorthands, just to reduce the amount of typing:
mincost min-cost 2 02 0x02
reliability reliable 4 04 0x04
max-throughput maxthroughput 8 08 0x08
lowdelay interactive min-delay 10 0x10
clear 0 00 0x00
Options can be specified with the ``option'' keyword, which can be defined anywhere within the document. Although that may be fine, you almost allways want to define them at the beginning of your document, because the behaviour changes at the moment they are specified.
All options can also be specified on the command line, which has a few more available. The equivalent for the commandline options that are also available in the firewall file is mentioned in the firewall file options section.
ipchains(8)
or iptables(8)
commands, but
skip instead. This way you can parse your data, use --lines
to view the output.
ipchains(8)
etc., you can see which rule caused
the error.
ipchains(8), ipfwadm(8), iptables(8)
A good firewall is not the only step in security, even the firewall may be insecure, or someone breaks into your house and steals the hard disk out of your PC. Do not rely on this firewall tool for the use of mission critical or confidential data. It is not fit for such a purpose!
Instead, use this tool to expand your current use of ipchains(8)
and routing, create a flexible firewall and look out for
anything suspicious. Be carefull with open ports and servers,
always get the latest, patched versions. Read more about
firewalls before experimenting, you are warned! You might
also read the COPYING file provided with the package or
visit www.gnu.org to find more about the license.
The package comes with a directory full of goodies (examples) that you can try, adjust for your system or just read if you want to understand the syntax and it's possibilities. Look in the ``examples'' directory.
The Operating system currently supported is only linux, although it may be possible to port this program to support FreeBSD or SOLARIS firewall systems, provided they supply a similar firewalling scheme. (Does anybody known about that?)
Required are 2 packages: Perl5, under which this ferm runs, and one of the kernel firewall programs, suited for your system and kernel version.
The respective required kernel versions for each of the kernel
firewall programs (ipchains(8), ipfwadm(8)
or iptables(8))
is also
needed. This means you have to have a kernel which can use the
firewalling thing, something you might have to compile a kernel
for, or set some switches in /proc. Look at the man pages of
those kernel programs for more information.
ferm allows almost anything the used firewall program allows, so go ahead and specify complex port ranges, icmp by number or worse. Just be warned.
Although quite sophisticated, the kernel interface programs
ipchains(8)
and iptables(8)
are very limited in some respects.
ferm is only an interface to improve the handling of
these programs, and is therefore limited by the possibilities
of these programs.
Ipfwadm(8)
is extremely limited in rule-building, upgrade or
succomb in it. Nothing ferm can do about it.
The ipfwadm(8)
interface is really limited due to being unable to
test it and having no experience with it at all. I'll be
concentration on iptables(8), which supports much more options
and will be quite more flexible.
Several nasty cleanups are not done well, which may result in surviving data. Tried to remove all of them but suspect more of them to occur.
The --log-prefix construct does not allow certain characters to be put between ``''. Make sure you don't use the bracket {} and [] characters, the ! and , are also not correctly parsed.
* Improve ipfwadm(8)
handling or removing it altogether
* Add more examples, with modularized snipplets (include option)
* Make rpm's for RH and SuSE, or better: get you to do that!
* Review the second half of the manual page
* Make ferm bug you more about errors, i.e. increase validity checking to high levels
Copyright (C) 2001-2003, Auke Kok <auke.kok@planet.nl>
ferm is released under the Gnu Public License, see the COPYING file that came with the package or visit www.gnu.org.
This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Auke Kok (auke.kok@planet.nl)