How to use ferm firewall rules on OpenWRT

OpenWRT has a very flexible Firewall configuration system, but for various reasons I want to use rules generated by ferm instead.

Use this howto on your own risk! I do not guarantee that it's free of errors! If you're not absolutly sure about something written here, or you don't know how to make backups of your router config and how to savely restore them: don't follow these instructions!

First of all, since there is no package for ferm available, we have to install it manually. Luckily, it's only a simple Perl script, so just fetch the ferm script file, copy it to /usr/sbin/ferm and make it executable.

Next, the required perl packages have to be installed with opkg. I've started with a basic set of Perl modules and installed the missing ones one after one, until a call to "ferm" didn't throw an error. These are the packages I've finally installed:

# opkg install perl perlbase-autoloader perlbase-config perlbase-cpan perlbase-dynaloader \
  perlbase-errno perlbase-essential perlbase-fcntl perlbase-file perlbase-getopt perlbase-integer \
  perlbase-io perlbase-pod perlbase-posix perlbase-selectsaver perlbase-socket perlbase-symbol \
  perlbase-term perlbase-tie perlbase-time perlbase-universal perlbase-xsloader

Next, place you're ferm ruleset at /etc/firewall.ferm. This is my, for the moment, but of course you can extend it as you want. This ruleset allows SSH from WAN port, so ensure that password authentication is disabled! Otherwise, remove the rule at line 42:

@def $IF_LAN = br-lan;
@def $IF_LAN_ALL = ($IF_LAN);

@def $IF_WAN = eth0.2;
@def $IF_WAN_ALL = ($IF_WAN);

# NATing rules - only required for IPv4
domain ip {
    table nat {
        chain PREROUTING {
            policy ACCEPT;
        }
        chain POSTROUTING {
            policy ACCEPT;
            outerface $IF_WAN_ALL {
                MASQUERADE;
            }
        }
        chain INPUT {
            policy ACCEPT;
        }
        chain OUTPUT {
            policy ACCEPT;
        }
    }
}

# IPv4 specific chains
domain ip {
    table filter {
        chain reject {
            protocol tcp REJECT reject-with tcp-reset;
            REJECT reject-with icmp-port-unreachable;
        }
        chain accept_ctstate_dnat {
            mod conntrack ctstate DNAT ACCEPT;
            RETURN;
        }
        chain accept_icmp_input {
            protocol icmp icmpv6-type 8 ACCEPT;
            RETURN;
        }
        chain accept_icmp_forward {
            RETURN;
        }
    }
}

# IPv6 specific chains
domain ip6 {
    table filter {
        chain reject {
            protocol tcp REJECT reject-with tcp-reset;
            REJECT reject-with icmp6-port-unreachable;
        }
        chain accept_ctstate_dnat {
            # Invalid on ipv6
            RETURN;
        }
        chain accept_icmp_input {
            protocol ipv6-icmp icmpv6-type (128 129 1 2 3 4/0 4/1 133 135 134 136) mod limit limit 1000/sec ACCEPT;
            RETURN;
        }
        chain accept_icmp_forward {
            protocol ipv6-icmp icmpv6-type (128 129 1 2 3 4/0 4/1) mod limit limit 1000/sec ACCEPT;
            RETURN;
        }
    }
}

# Common rules
domain (ip ip6) {
    table mangle {
        chain PREROUTING {
            policy ACCEPT;
        }
        chain FORWARD {
            policy ACCEPT;
            outerface $IF_WAN_ALL {
                protocol tcp tcp-flags (SYN RST) SYN TCPMSS clamp-mss-to-pmtu;
            }
        }
        chain INPUT {
            policy ACCEPT;
        }
        chain OUTPUT {
            policy ACCEPT;
        }
        chain POSTROUTING {
            policy ACCEPT;
        }
    }

    table filter {
        chain drop_syn_burst {
            mod limit limit 25/sec limit-burst 50 RETURN;
            DROP;
        }

        chain INPUT {
            policy DROP;

            # Accept everything on localhost, established connection,
            # ping and SSH, but reject syn bursts.
            interface lo ACCEPT;
            mod conntrack ctstate (RELATED ESTABLISHED) ACCEPT;
            protocol tcp tcp-flags (FIN SYN RST ACK) SYN jump drop_syn_burst;
            jump accept_ctstate_dnat;
            jump accept_icmp_input;
            protocol tcp dport 22 ACCEPT;

            # Accept everything comming from the internal network
            interface $IF_LAN_ALL ACCEPT;

            # On regular WAN interface, only accept incomming NAT traffic
            interface $IF_WAN_ALL {
                jump accept_ctstate_dnat;
                jump reject;
            }

            # Missed something?!
            jump reject;
        }

        chain FORWARD {
            policy DROP;

            # Allow established connections
            mod conntrack ctstate (RELATED ESTABLISHED) ACCEPT;
            jump accept_ctstate_dnat;

            # Allow anything from local interfaces
            interface $IF_LAN_ALL ACCEPT;

            # On regular WAN interface, only accept ICMP (IPv6)
            interface $IF_WAN_ALL {
                jump accept_icmp_forward;
                jump reject;
            }

            # Missed something?!
            jump reject;
        }

        chain OUTPUT {
            # Basically: Accept every output
            policy ACCEPT;
            outerface lo ACCEPT;
            mod conntrack ctstate (RELATED ESTABLISHED) ACCEPT;
            outerface $IF_LAN_ALL ACCEPT;
            outerface $IF_WAN_ALL ACCEPT;
        }
    }
}

Now write an "include" script for OpenWRT's firewall so that it get's executed when the firewall is started or reloaded:

/etc/firewall.ferm.sh

/usr/sbin/ferm /etc/firewall.ferm

And append the following block to /etc/config/firewall:

config include
    option path '/etc/firewall.ferm.sh'
    option reload '1'

And remove the include config for /etc/firewall.user. You could disable (option enabled '0') every config rule as they would be replaced by ferm nevertheless, but don't disable the zone configs! They are still required so that fw3 reloads the ruleset when the interfaces changes.

Finally, if you want to be able to edit your ferm rules via the LuCI web frontend, symlink /etc/firewall.ferm to /etc/firewall.user.

Remember: If you add new interface to your filewall zones this won't have any effect! You have to manually add them to the ferm config. I've added some variables at the top of the config that makes this easier.

Add new comment