Building a simple ODROID based firewall

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.

1. Setup ArchLinux Arm

I won't go into detail here, since everything 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

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 eth0 and 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 eth0. 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

So for 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 EmitDNS=yes and 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 10.0.0.11-10.0.0.30/24.

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/udp and 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 /etc/systemd/resolved.conf:

LLMNR=no

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

Security is always a trade-off and should fit the attacker you expect. 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.

comments (0) - add comment

No comments so far, leave one?