ODROID boards are perfect for building custom Linux based firewall solutions. A friend of mine recently asked me for instructions on how to do that. So here they are.
What you need...
- ODROID C2 (or better)
- USB-Ethernet Adapter (e.g. UGREEN adapter with AX88772A chipset)
- microSD reader (e.g. Transcend TS-RDF5)
- ArchLinux ARM
The ODROID runs with an eMMC memory card, not a microSD card. To write the eMMC, the ODROID is usually shipped with an microSD Adapter. Unfortunately this adapter doesn't work well with many SD Card readers. After trying different readers, I can recommend the Transcend TS-RDF5.
The general idea...
We want a basic firewall which separates two networks from each other. On one side you have a trust (internal) network. On the other side you have an untrust (external) network. The firewall will deny access attempts from the external to the internal network, but will allow traffic from the internal to the external network.
There are different ways to achieve this with an ODROID. One is having a VLAN capable switch which separates the networks, in this case you only need one network interface. I decided here for option two: Attach an USB-to-Ethernet Adapter so that you have two network interfaces. One network interface is connected to the untrusted network, while the other one is connected to your trust network.
Update March 2020
Unfortunately, Hardkernel (the company behind the ODROID boards) only provides a very old kernel for the ODROID C2. Recently, this version broke with the latest systemd version used in ArchLinux. Therefore, I've updated the article with a paragraph which describes how to use the mainline kernel instead. With that kernel, not all hardware features will be enabled, but all features which are needed for the firewall to work properly, will work.
1. Setup ArchLinux Arm and switch to mainline kernel
The default installation is nicely described here. Just follow the instructions.
Once Arch is installed, login via
ssh. Don't forget to change the default passwords (
passwd). Once done update Arch via
# pacman -Syu
We will then switch to the mainline kernel. For this will replace uboot (bootloader) as well as the kernel package. Just run the commands below. You will be asked whether you want to remove
linux-odroid-c2. Confirm that.
# pacman -R uboot-odroid-c2 # pacman -S uboot-odroid-c2-mainline linux-aarch64
Since we need to edit some config files, you should install an editor of your choice now, e.g.
# pacman -S vim
2. Connect the USB to Ethernet Adapter
As mentioned above the UGREEN 100 MBit LAN Adapter (AX88772A chipset) worked out of the box. The adapter should show up as
eth1 like that:
# ip addr | grep eth1 3: eth1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
If the adapter doesn't show up, check
dmesg for any error messages. Now we have one network interface for every network.
eth0 will be used for our untrusted network,
eth1 will be used for the trusted network.
3. Setup basic firewall rules
We take the whitelisting approach. We block any incoming traffic and we deny packet forwarding (that means handing packets between
eth1). For simplicity we allow outgoing traffic.
In the following we make it the other way around. First we set all rules which allow certain traffic, then we set the default policies to
DROP. If you do it the other way around you might block yourself out when you're connected via ssh.
We allow any traffic via localhost:
# iptables -A INPUT -i lo -j ACCEPT
We drop any invalid traffic:
# iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
The conntrack mechanism of the kernel keeps a connection table. With this table the kernel knows which connections belong to an already established connection and which doesn't. We use this mechanism to identity already "known" connections (i.e. incoming packets which belong to connections we have set up earlier). In addition we allow any related packets:
# iptables -A INPUT -i eth0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
We also allow ICMP echo requests (ping), the rule above handles the ICMP echo reply already (i.e. the kernel knows that ICMP echo request and replies are related to each other):
# iptables -A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
We also need to allow packet forwarding from the trusted to the untrusted network. Note that this doesn't enable IPv4 forwarding, we will do this in a later step.
# iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
Same as above. We don't forward any new incoming connections, we only allow traffic of connections we already know or are related:
# iptables -A FORWARD -i eth0 -o eth1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
Allow SSH access from the trusted network:
# iptables -A INPUT -i eth1 -p tcp -m tcp --dport 22 -j ACCEPT
Finally set the default policy, we also set the default policy for IPv6 here. For simplicity we drop any IPv6 packets:
# iptables -P INPUT DROP # iptables -P FORWARD DROP
For IPv6 we also drop outgoing traffic.
# ip6tables -P INPUT DROP # ip6tables -P OUTPUT DROP # ip6tables -P OUTPUT DROP
It should be mentioned here that blocking all IPv6 traffic can cause problems. Sometimes services communicate via IPv6 loopback address, which would be denied by the policy above. For our minimal setup this is not needed, but if you see any strange issues running other services on the ODROID, make sure you allow IPv6 loopback traffic via:
# ip6tables -A INPUT -s ::1 -d ::1 -j ACCEPT # ip6tables -A INPUT -d ::1 -s ::1 -j ACCEPT
We will now set static IPs, configure DHCP, DNS as well as IPv4 Forwarding and Masquerading. There are different ways to configure DHCP and DNS. I decided to only use systemd's internal services. In my opinion this is the cleanest way since it doesn't require any additional services.
4. Configure the untrust network interface
There should be a file
/etc/systemd/network/eth.network. Rename it to
eth0.network and fill it with the following content:
[Match] Name=eth0 [Network] Address=192.168.1.10/24 Gateway=192.168.1.1 DNS=192.168.1.1 IPForward=ipv4 ConfigureWithoutCarrier=yes LinkLocalAddressing=no IPv6AcceptRA=no
Most of the settings should be straight forward:
Address is the IP you assign to
Gateway will be your untrusted network's gateway.
DNS is the DNS which your firewall will use (e.g. when you update packages on the firewall). You could use a privacy aware public DNS here if you don't trust the DNS. In this example it is the same as the gateway and we assume we can trust it.
IPForward=ipv4 will enable IPv4 forwarding. That means we "allow" the kernel to forward packets from on network interface to the other whenever needed.
ConfigureWithoutCarrier will tell systemd to setup your NIC even if no network cable is plugged in. The other two options disable IPv6 stuff which we don't need for this setup.
5. Configure the trust network interface
Add a second file
/etc/systemd/network/eth1.network with the following content:
[Match] Name=eth1 [Network] Address=10.0.0.1/24 IPMasquerade=yes ConfigureWithoutCarrier=yes DHCPServer=yes LinkLocalAddressing=no IPv6AcceptRA=no [DHCPServer] DNS=192.168.1.1 EmitDNS=yes PoolOffset=20 PoolSize=10
eth1 we assign
10.0.0.1 and we allow masquerading. It is not necessary to use masquerading here, but it makes things a bit simpler. Without masquerading you would need to add additional routes to your untrust network's gateway (you would need a rule which tells your untrust gateway where to find your internal 10.0.0.0/24 network). Note that when enabling masquerading, ip forwarding will be activated implicitly for this network interface (see here for more details).
We also tell systemd we want a
DHCPServer enabled for
eth1. So any clients in our internal network will get an IP via DHCP. The settings for the DHCP server can be found further down. Here the lines
DNS=192.168.1.1 will tell the DHCP server to set
192.168.1.1 as DNS in any DHCP response. Note that this doesn't need to be the same DNS as we used in step 4. The pool settings allow to configure the size of the DHCP pool (20 IPs) and an offset of 10, so the IP pool will be
Last but not least we need a firewall rule in order to allow incoming DHCP requests on the trust interface (
eth1) - note that the DHCP replies are already handled by the rules we had defined earlier:
iptables -A INPUT -i eth1 -p udp -m udp --dport 67 -j ACCEPT
6. Make all iptables rules persistent
All iptables rules are only temporary so far. In order to make them persistent run:
# iptables-save > /etc/iptables/iptables.rules # ipt6tables-save > /etc/iptables/ip6tables.rules # systemctl enable iptables.service # systemctl enable ip6tables.service
This will save the iptables rules and make sure they are reloaded on every boot.
7. Optional step - Disable Link-local Multicast Name Resolution
With default settings systemd's DNS will listen on port
5353/tcp for Link-local Multicast Name Resolution (LLMNR) requests. Normally you don't want that and in this set up the firewall doesn't allow these packets anyway. So we disable LLMNR by editing the file
8. Reboot and final checking
You can now reboot the ODROID. Connect a machine to the trust network and wait for the ODROID to come up. You should get an IP assigned via DHCP in the
10.0.0.11-30/24 range. Now connect the ODROID to the untrust network with the second NIC and check that you can reach other networks (the internet).
Additional security hints
This is only a basic setup. For a more secure setup you might also block outgoing traffic by default. You would then need additional rules which allow the traffic you want. A good general rule regarding security is: Disable what you don't need in order to minimize the attack surface.