CrowdSec is a proud participant in the Microsoft Copilot for Security Partner Private Preview
Read more

CrowdWall, a Tough Firewall for 50€ – Part 2: The Software Stack

How to set up an effective firewall and preserve your security & privacy with Orange Pi R1+, Netfilter, AdGuard and CrowdSec.

Welcome to the second part of our trilogy where the goal is to inspire you to create a very efficient firewall to protect your remote work environment, family, or your small business, which offers a serious level of security, at a low cost.

Have you not yet read the first part that focuses on selecting hardware and installing the base OS it’s not too late. Find it here.

This second part is focused on how to set up firewall functionality, setup client VPN to protect your identity online, setting up AdGuard for the entire network, DuckDNS if you don’t have a static IP as well as port knocking which is a cool way to close down your internet exposed services to anyone but you (or anyone else who knows the secret combination to enter). The third and final part will be about how to secure your network even more with CrowdSec – how to set up a canary device that ‘tweets’ when unexpected events like for instance port scanning occurs; something you would typically never do yourself.

It’s also being part of a larger project, CrowdSec, which is blocking Internet attacks and sharing IPs that launched them. You protect yourself and others at the same time. In essence, this comprehensive guide will show you how to:

  • Create security robust enough to resist even if passwords are compromised
  • Create a reliable hardware environment for your firewall
  • Install the OS on it and create a rock-solid Firewall to protect your activities
  • Add CrowdSec to protect your WLAN services you’d like to expose over the Internet and detect if any local IoT device is going rogue (e.g. cams, assistants, connected speakers, etc.)
  • Allow external access to DMZ-like services to control your home on distance and access your firewall
  • Add a VPN to protect your anonymity online
  • Add an anti-advertisement & anti-tracking system

Part 2: The Software stack

The firewall

It’s already in there, provided by the netfilter subsystem, which is integrated into the kernel. The nft (nftables) command is here to help us interact with the netfilter layer. The following script is commented so that you can easily modify it according to your needs and projects or even port it to iptables (nftable  predecessor). All scripts can be found here.

Out of the box, it handles:

  • Multiple internet connection with a dynamic routing table capable of sending packets to one or the other connection based on your rules (destination port, src or destination IP, protocols, etc.)
  • Protection against usual network shenanigans
  • Inbound, forward and outbound traffic filtering
  • CrowdSec integration to defend your exposed services and have a Canary to detect if your LAN is being scanned
  • Port knocking integration so that you can simply use a port knocking app to unlock access to your network from wherever you are.

Just cut/paste it from this doc:

$ cat | sudo tee /etc/nftables.conf

Here is the script to adapt and copy/paste:

#!/usr/sbin/nft -f

#Part 1: Setting statics
define wan                     = eth0
define lan                       = lan0
define vpn                      = tun0
define localhost            = lo
define vpn_net              =
define vpnserver           = tun1
define vpn_server_net  =
define machine1           =
define machine2           =
define antilanscan        = { 22, 8080 } #Part 8: Anti lan scan
define cameras             = {, }
define icmp_v6          = { destination-unreachable, echo-reply, echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert, packet-too-big, parameter-problem, time-exceeded }

#Part 2: Reset nft tables
flush ruleset

#Part 3: NAT
table ip nat {
  chain prerouting {
    type nat hook prerouting priority -100; policy accept;
      iif $wan tcp dport 2222 dnat to $machine1:22 # incoming connexion on port 2222 will be forwarded to  on 22
      iif $wan tcp dport 5001 dnat to $machine2      # sames port 5001 and but this time port is 5001 to 5001, no change
  chain postrouting {
    type nat hook postrouting priority 100; policy accept;
      oif $wan snat to
      oif $wan2 snat to
      oifname $vpn masquerade # snat to $vpnip
      oifname $vpnserver snat to

#Part 4: Creating IP sets
table inet filter {

  set whitelist_candidates {
    type ipv4_addr . inet_service
    flags timeout

  set whitelist {
     type ipv4_addr
     flags interval, timeout

  set whitelist_portknockd {
     type ipv4_addr
     flags timeout

  chain input {
    type filter hook input priority 0; policy drop;
      icmpv6 type $icmp_v6 accept
    #Part 5: Portknocking (next 4 lines)
      tcp dport 314 add @whitelist_candidates {ip  saddr . 18 timeout 2s}
      tcp dport 18 ip saddr . tcp dport @whitelist_candidates add @whitelist_candidates {ip  saddr . 939 timeout 2s}
      tcp dport 939 ip saddr . tcp dport @whitelist_candidates add @whitelist_candidates {ip  saddr . 101 timeout 2s}
      tcp dport 101 ip saddr . tcp dport @whitelist_candidates add @whitelist {ip saddr timeout 180s} log prefix "Portknocked: "
      ct state related,established            accept
      iif $lan tcp dport != $fwopenports log prefix "LAN SCAN:"  drop #Part 8: Anti lan scan
      ip saddr @whitelist                            accept
      ip saddr @whitelist_portknockd      accept
      iifname $lan                                       accept
      iifname $localhost                            accept
      iifname $vpnserver                           accept
      iifname $wan ip saddr != @whitelist                             drop
      iifname $wan ip saddr != @whitelist_portknockd       drop
      iifname $wan ct state invalid                                        log prefix "Invalid packet:" drop
      iif != lo ip saddr                                            log prefix "Fake localhost:" drop

  chain forward {
    type filter hook forward priority 0; policy drop;
    #Part 6: Prerouting (next two lines)
      iifname $vpn meta l4proto udp ip daddr $machine1 udp dport 22 accept
      iifname $vpn meta l4proto tcp ip daddr $machine2 tcp dport 5001 accept
    #Part 4: Creating IP sets (next two lines)
      ip saddr @whitelist                       accept
      ip saddr @whitelist_portknockd accept
      iif $lan                                            accept
      iifname $vpnserver                      accept
      ct state established,related       accept
      ip saddr $cameras log prefix "Camera trying to exit:" drop
      ct state invalid log prefix "Invalid packet:" drop

  chain output {
    type filter hook output priority 0; policy accept;
#Part 7: Mangling
table ip mangle {
  chain prerouting {
    type filter hook prerouting priority -150; policy accept;
      mark != 0x0                                               meta mark set ct mark
      ip saddr $machine1 tcp sport 10001    meta mark set 0x2
      ip saddr $machine1 udp sport 10001   meta mark set 0x2
      ip saddr $machine1                                meta mark set 0x1
      ip saddr $machine2                                meta mark set 0x4

  chain postrouting {
    type filter hook postrouting priority -150; policy accept;
      ct mark set mark

After inserting it, save the file by pressing CTRL+D.

This version is a simplified version of the firewall script available from the repo above, but the most important part is here already. This nftables.conf file would not be enough by itself to handle several possible routes. Be sure to also use the ip rules script to create your routing tables, see below, section IP route.

NFtables (netfilter) Firewall details

  • Part 1 are basic variable definitions.
  • Part 2 resets all nft tables. (Used to take 10 lines with Iptables)
  • In part 3 (NAT) we are defining what happens to packets that are incoming and destined to another machine behind the firewall. They are dealt with very early in the filtering process, in a prerouting chain. The postrouting chain is about telling what IP should be used for translating IPs coming from various subnets.
  • The second table, filter, is part 4 that create IP sets. They are tables that contain IP addresses, sometimes with expiry dates. Three sets are created, one for potential candidates to whitelisting, the second for confirmed whitelisted IPs and the third will be used in conjunction with port knocking. The reason for creating two different whitelists of IPs is that the port knocking integrated in this NFT configuration (part 5) will only store the IP that knocked. Logical? No, not really. But CGNAT, used by telecom operators to run 4G networks, ruins it all and usually, it’s not a 1:1 IP translation but a range of IP that is used. So when your phone is port knocking 3 ports quickly, it’s usually with one IP but then when you connect on your SSH port, another IP is used. Luckily, they often sit in the same /24 range. The problem doesn’t exist with IPv6 obviously. So knockd will fill the second set with a range and the first one is still useful when you connect from elsewhere like a hotel. (I didn’t find any way to add a range to the set directly from an nft configuration file)
  • Part 6 is related to prerouting. It’s not enough to redirect the connection since the packet is passing two interfaces, we also need to accept this in the forward rules.
  • Part 7 is mangling. This is where we instruct the firewall to mark packets according to our own rules. 0x03 is the VPN server you host yourself and where you receive inbound traffic, 0x02 is the VPN client which you use to establish an outbound tunnel through a VPN provider. It’s through here you send the traffic of machine 1. In this example, we want machine1 to be using connection 2 when it’s starting a connection on port UDP or TCP 10001, otherwise it will be using connection 1. Machine 2 will always use your alt connection (here marked as 0x4) and machine 1, except for ports 10001 will use connection 1. This can be adapted with ports, source addresses, destination addresses, protocols, etc. (Note that the table has the highest priority and will be “executed” first, before all other rules). Use cases are easy here: you can send your professional workstation packets through a dedicated connection for example. Or send all your peer-to-peer traffic through a VPN or your TV IP through a VPN to avoid Geo limitations, etc. See the IP route section to understand fully how nft mangle + IP route cooperate here.
  • Part 8, Anti lan scan will be covered later on in this guide, but the global concept is to watch for unusual port scans, coming from our LAN-facing ethernet adapter, that would denote an IoT device being compromised or a hacker doing a lateral move in your network (classic in Ransomware scenario). We’ll have a canary setup here, see below for a more detailed description.

Obviously, all those rules are given as examples but it should be fairly easy for you to adapt them to your own context.

Port knocking

So this script handles port knocking by itself with the lines tagged in yellow, but the CGNAT problem forces us to have a fallback plan. We’ll use knockd to handle the matter on our machine.

But why is port knocking in the first place?

Well, take for granted that any application you expose might have an unknown security flaw. Or that your passwords are compromised. If the attacker doesn’t have access to the application port in the first place, even if he knows your pass or has a secret “headshot” 0 days exploit to launch, with port knocking he cannot even try in the first place, except if he uses the exact same IP as you do. This very heavily limits the risks of getting compromised. Also, using your 4G connection is far less risky than connecting to a Hotel (or public place) Wifi. But how to just allow a temporary connection from those locations? Well, port knocking is the (very underrated) key.

So before connecting to your machine, you will just launch a little app that will port “knock” your machine, nicely whitelisting the public IP you’re using and give you access to VPN, SSH, RDP, whatever you want.

KnockonD will do nicely on iOS, Knock on Ports for Android). It sends a stream of packets, in a certain order, to add your current IP address in a set that is whitelisted in the firewall. (Careful, some ISPs (like broadband carriers) are doing CGNAT, which can cripple this technique, but we’ll try to put a workaround together)

On the OPI you just need to run:

$ apt-get install knockd

And edit the configuration file (/etc/knockd.conf) as follows. Modify to your own port sequence:

        logfile = /var/log/knockd.log
        interface = eth0

	sequence    = 17:tcp,19:tcp,39:tcp,105:tcp
	seq_timeout = 10
	command     = nft add element inet filter whitelist_portknockd "{ `echo %IP% | cut -f1,2,3 -d "."`".0/24" timeout 180s }"
     	tcpflags    = syn

	sequence    = 443:tcp,25:tcp,53:udp,80:tcp,53:udp,80:tcp,443:tcp
	seq_timeout = 10
	command     = nft add element inet filter whitelist_portknockd "{ `echo %IP% | cut -f1,2,3 -d "."`".0/24" timeout 180s }"
	tcpflags    = syn

Two sequences here, one classic and another one that is compatible with heavily filtered networks that won’t allow you to access all ports freely. Both add not just your IP but your IP in a 24 range in the whitelisted set. This one is really made to address the pesky CGNAT problem.

IP Route

You also need a script that will create those multiple routing tables, and that will be able to use the marks we set in the script (part 7 in the nftables script).

Here is the script creating multiple routing tables, allowing different default routes for different usages. It’s also available from my GitHub:

Note: If you don’t have multiple wan connections like me, adjust accordingly by removing all occurrences of WAN2.


# Provides:                  multiroute
# Required-Start:        $network
# Required-Stop:        $network
# Should-Start:
# Should-Stop:
# Default-Start:            2 3 4 5
# Default-Stop:            0 1 6
# Short-Description:    Multiroute manager
# Description:              Manage multi-routing

   VPN=`ifconfig|grep tun0`
   VPNSERVER=`ifconfig|grep tun1`
 [[ ! -z "$VPN" ]] && VPNIF="tun0" && VPN=1 && VPNCLIENTIP=`ip -o addr | grep -v inet6 | grep tun0 | awk '{split($4, a, "/"); print a[1]}'` &&  VPNCLIENTROUTE=`ip route show|grep -v inet6 | grep "tun0 proto" | cut -f 1 -d " "`
  [[ ! -z "$VPNSERVER" ]] && VPNSERVERIF="tun1" && VPNSERVER=1 && VPNSERVERIP=`ip -o addr |grep -v inet6 | grep $VPNSERVERIF |awk '{split($4, a, "/"); print a[1]}'` && VPNSERVERROUTE=`ip route show |grep -v inet6 | grep $VPNSERVERIF | cut -f 1 -d " " | head -1`

  ip rule del from all fwmark 1 2>/dev/null
  ip rule del from all fwmark 2 2>/dev/null
  ip rule del from all fwmark 3 2>/dev/null
  ip rule del from all fwmark 4 2>/dev/null
  ip rule del lookup maincnx    2>/dev/null
  ip rule del lookup vpnclient  2>/dev/null
  ip rule del lookup vpnserver  2>/dev/null
  ip rule del lookup altcnx     2>/dev/null
  ip route flush table maincnx
  ip route flush table vpnclient
  ip route flush table vpnserver
  ip route flush table altcnx
  for i in /proc/sys/net/ipv4/conf/*/rp_filter; do echo 0 > "$i"; done # To avoid packet drop

  [[ $VPN ]] && echo -e "$ORANGE -> VPN IS UP (route: $VPNCLIENTROUTE, on dev: $VPNIF, ip: $VPNCLIENTIP) $END"

  ip route add table maincnx default dev $WAN via
  ip route add table maincnx dev $LAN src
  ip route add table maincnx dev $WAN src
  ip route add table maincnx dev $WAN2 src
  [[ $VPN ]] && ip route add table maincnx $VPNCLIENTROUTE dev $VPNIF src $VPNCLIENTIP
  [[ $VPNSERVER ]] && ip route add table maincnx dev $VPNSERVERIF src
  ip rule add from table maincnx

  [[ $VPN ]] && ip route add table vpnclient default dev $VPNIF via $VPNCLIENTIP
  [[ $VPN ]] && ip route add table vpnclient $VPNCLIENTROUTE dev $VPNIF src $VPNCLIENTIP
  [[ $VPN ]] && ip route add table vpnclient dev $LAN src
  [[ $VPN ]] && ip route add table vpnclient dev $WAN src
  [[ $VPN ]] && ip route add table vpnclient dev $WAN2 src
  ip rule add from $VPNCLIENTIP table vpnclient

  [[ $VPNSERVER ]] && ip route add table vpnserver default dev $VPNSERVERIF via $VPNSERVERIP
  [[ $VPNSERVER ]] && ip route add table vpnserver dev $LAN src
  [[ $VPNSERVER ]] && ip route add table vpnserver dev $WAN src
  [[ $VPNSERVER ]] && ip route add table vpnserver dev $WAN2 src
  [[ $VPNSERVER ]] && ip route add table vpnserver dev $VPNSERVERIF src
  [[ $VPNSERVER ]] && ip rule add from $VPNSERVERIP table vpnserver

  ip route add table altcnx default dev $WAN2 via
  ip route add table altcnx dev $LAN src
  ip route add table altcnx dev $WAN src
  ip route add table altcnx dev $WAN2 src
  ip rule add from table altcnx

  ip rule add from all fwmark 1 table maincnx
  [[ $VPN ]] && ip rule add from all fwmark 2 table vpnclient
  [[ $VPNSERVER ]] && ip rule add from all fwmark 3 table vpnserver
  ip rule add from all fwmark 4 table altcnx
  ip route flush cache

  for i in /proc/sys/net/ipv4/conf/*/rp_filter; do echo 0 > "$i"; done # To avoid packet drop
  echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward > /dev/null

case "$1" in

  [[ $VPN ]] && sleep 5 # Wait for VPN to be up if not yet started when the firewall script kicks in
  /usr/bin/logger -t "Multi route" "Starting" -p4
  /usr/bin/logger -t "Multi route" "VPN CLIENT DETECTED, ADDING RULES" -p4
  /usr/bin/logger -t "Multi route" "VPN SERVER DETECTED, ADDING RULES" -p4
  exit 0

  /usr/bin/logger -t "Multi route" "Stopped" -p4
  echo 0 | sudo tee /proc/sys/net/ipv4/ip_forward > /dev/null
  ip rule del from all fwmark 1 2>/dev/null
  ip rule del from all fwmark 2 2>/dev/null
  ip rule del from all fwmark 3 2>/dev/null
  ip rule del from all fwmark 4 2>/dev/null
  ip route flush cache
  exit 0

  /usr/bin/logger -t "Multiroute" "restart initiated" -p4
  $0 stop
  sleep 1
  echo -e '\n'
  $0 start

  echo -e "$YELLOW Usage: /etc/init.d/multiroute.sh {start|stop|restart}$END"
  exit 1

exit 0

The script is an old-fashioned SysV init script that goes into /etc/init.d after you’ve made it executable with chmod 755 and will be executed during startup.

This script basically creates four different routing tables. So instead of having just one “default route” for all your machines, your firewall now has four different tables, each containing a set of specific routing rules. So if you mark a packet with 0x2, (see in the nftables configuration, the mangle part) the machine will ship it through the VPN connection. If you tag it with 0x4, it will use your alternate connection, say a 4G for example. The tag 0x3 will be for the VPN Server and the 0x2 for VPN client. It’s just magic how many opportunities this kind of IPtables / Nftables mangling system, coordinated with IP routing can open.


We speak here of a client VPN. A VPN that you subscribed to and want to be able to use in certain situations, or more precisely with certain packets. If you want to bypass a geographical lock from some TV broadcasters or Netflix or use some protocols rather on an anonymous connection than the usual one, or just to preserve your anonymity this is the way to do it.

Once you find your dream VPN provider, they will most likely give you files to set up your VPN connection, and usually, they are made for both Wireguard and OpenVPN. I will show you how to install and configure the latter.
You will just need to install OpenVPN, upload those files (usually a .conf file also embedding all certificates and a user.pass file with the credentials) in the /etc/openvpn directory, enable openvpn in /etc/default/openvpn and you should be able to connect.

$ sudo apt-get install openvpn
$ sudo sed -i '/#AUTOSTART="all"/s/^#//g' /etc/default/openvpn

A typical OpenVPN client configuration looks like this:

dev tun
proto udp
remote [OPENVPN SERVER IP] 1194
resolv-retry infinite
tun-mtu 1500
tun-mtu-extra 32
mssfix 1450
ping 15
ping-restart 0
reneg-sec 0
comp-lzo no
log /var/log/openvpn.log
remote-cert-tls server
auth-user-pass /etc/openvpn/user.pass.vpn
verb 3
cipher AES-256-CBC
auth SHA512
script-security 2
up "/etc/init.d/firewall restart"
down "/etc/init.d/firewall restart"


key-direction 1

# 2048 bit OpenVPN static key

There is a minor security vulnerability here. Using the auth-user-pass /etc/openvpn/user.pass.vpn file is not ideal. This file contains your credentials for connection in plaintext to avoid providing them manually during initialization of the VPN. This plaintext isn’t encrypted and could expose your credentials if your firewall is ever seized or you become prone to a very critical vulnerability, like a 0day buffer overflow of some sort, would successfully compromise your firewall. On my end, I live with it, but you’ve been warned.

PS: The firewall is automatically reloaded when the VPN goes up or down to add the proper rules.

Create the directory for custom scripts

We need a directory to save custom scripts. Create /usr/local/scripts and sudo chown it to your current user for convenience.


While we are at it, let’s make this machine easy for you to locate online. Should you have a dynamic IP address, just crash by DuckDNS, create yourself an account and register your IP and duckdns.org subdomain for free. Here is a little script to help you update it on a regular basis:

$ echo url="https://www.duckdns.org/update?domains=YOUR_DOMAINE&token=YOUR_TOKEN&ip=" | curl -k -o /var/log/duckdns.log -K -

The token is found under your login at duckdns.org after clicking the ‘>>> reCAPTCHA <<<’ button.

Use the script by saving it to e.g. /usr/local/scripts/duckdns.sh, chmod 755 it and execute it every half an hour with a crontab like this:

$ crontab -e

$ crontab -e

It should look somewhat like this:


#m   h  dom mon dow      command
17   *   *   *   *       cd / && run-parts --report /etc/cron.hourly
45  10   *   *   *       test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47   6   *   *   7       test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52   6   1   *   *       test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
30   *   *   *   6       /usr/local/scripts/duckdns.sh

Only the last line is added by us. The first lines are usually present default. YMMV.

Note: In order for the user who executes the duckdns.sh script to have permissions to write a log file in /var/log it needs to be part of the syslog group. This can easily be done by running sudo addgroup <user> syslog.


Adguard is a really cool piece of software that is basically running a DNS that resolves all advertisement servers to (resulting in many ads not being shown. Hooray!). You install it on your LAN and instead of connecting directly to or your ISP DNS, you tell all your LAN users to rather use it.

If your request is not going toward an Ad server, it’s just resolved by the DNS you instructed Adguard to use. Otherwise, your client, say your mobile phone, will just ask this ad from (himself), getting nothing in return. With this, a chrome plugin like Adblock as well as youtube Adblock and advertisement will be a thing of the past.

I highly recommend visiting their Github here. Installation is fairly easy:

$ curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v

Note: The script will obtain root permissions and ask for those as needed.

Now we have a DNS relay running on localhost.

You can also just use the DNS servers of Adguard directly. These are present at and

If you want to finalize your setup of Adguard, just connect with a browser to the firewall (likely on at this stage), on port 3000. It should look like this:

Adguard wizard

Use the wizard to set up Adguard and continue the tutorial.

Adding a DHCP server to finalize our LAN setup

Well, now that most of the tools are up & running, let’s have a DHCP running to give addresses to machines in the LAN, which your ISP box won’t do anymore since it’s on the other side of the firewall. While we are at it, we can now ship IP addresses with the local Adguard DNS to get rid of ads.

$ sudo apt-get install isc-dhcp-server
$ cat | sudo tee /etc/dhcp/dhcpd.conf

Copy/paste this (and end with CTRL+D):

deny declines;
log-facility local7;

subnet netmask {
  interface eth1;
  option routers;
  option broadcast-address;
  option domain-name-servers,; # Local adguard DNS proxy + online adguard DNS
  default-lease-time 259200;
  max-lease-time 604800;

host machine1 {
  hardware ethernet 00:11:22:33:AA:DD;

host machine2 {
  hardware ethernet 85:55:85:42:12;

Given as an example, I advise you to have static IPs directly like for machine 1 & 2 in this file if you can. This makes it easier to locate them later. We point the DNS to our local instance of Adguard and, as a backup also to their online DNS if our own is not replying fast enough.

So this was part 2 of our trilogy of how to install the CrowdWall. I hope you enjoyed reading it as much as I enjoyed writing it. Stay tuned for part 3 where things really start to get interesting once we add CrowdSec to the mix.

No items found.