Peoro's Odroid πŸ”—

This whole page is about the configuration peoro uses on his router/gateway odroids.

These odroids are meant to host all the vital services for a LAN (DHCP, DNS, natting etc) in a fancy way: routing some traffic through VPNs, blocking ads through custom DNS etc.

Running services πŸ”—

Check enabled systemd services with ls -lhR /etc/systemd/system/ | grep -- '->'.

When everything is configured, we should find the following running services:

The following should also be enabled, as they should be pre-configured with ALARM:

  • sshd, obviously.
  • systemd-networkd, handling our network configuration.
  • systemd-timesyncd, keeping our time synced.
  • getty@tty1, a terminal on TTY1.
  • amlogic, AMlogic HDMI Initialization. We can get rid of it.
  • remote-fs, meant as a dependency

Network configuration πŸ”—

Internet sharing πŸ”—

To enable packet forwarding, create /etc/sysctl.d/nat.conf:

net.ipv4.ip_forward = 1

The enable natting through iptables.

iptables πŸ”—

Show the iptables configuration using:

sudo iptables -L -nv -t filter; echo; echo; sudo iptables -L -nv -t nat --line-numbers

We can save the iptables configuration, so that it's loaded when the iptables is started by running:

sudo iptables-save | sudo tee /etc/iptables/iptables.rules

In order to enable natting (i.e. replacing our IP into packets forwarded by us and then removing it from the responses) we can do:

iptables -t nat -A POSTROUTING -j MASQUERADE

And if we want to capture all the DNS requests (even the ones directed to remote DNS servers) to reply them ourselves, we can do:

iptables -t nat -A PREROUTING -p udp -m udp --dport 53 -j DNAT --to-destination

Where is the local IP address.

OpenVPN πŸ”—

Allievi πŸ”—


# Auth
ca   allievi/allievi-ca.crt
cert allievi/client.crt
key  allievi/client.key

# Net
dev tun0

# Technical details
proto udp
rport 1194

auth SHA1       # Only for HMAC
ns-cert-type server

log-append /var/log/openvpn.allievi
verb 3

script-security 2
# adding this VPN as default gateway to table allievi and to the main table (with metric=20)
route-up        '/etc/openvpn/scripts/ add 20'
route-pre-down  '/etc/openvpn/scripts/ del 20'

peori πŸ”—


# Auth
ca   peori/ca.crt
cert peori/client.crt
key  peori/client.key

# Net
dev tun1

# Technical details
proto udp
rport 1194

auth SHA1       # Only for HMAC
ns-cert-type server

log-append /var/log/openvpn.peori
verb 3

script-security 2
# adding this VPN as default gateway to table peori and to the main table (with metric=10)
route-up        '/etc/openvpn/scripts/ add 10'
route-pre-down  '/etc/openvpn/scripts/ del 10'

If you want to expose the odroid's LAN to peori...

  1. Pick an even number for the client:
    • 170: odroid-l77
    • 172: odroid-w6
    • 174: odroid-third
  2. Use 192.168.NUMBER.0/24 for the new LAN.
  3. On the VPN server, create peori/client-config/VPN_CLIENT_NAME:
    # setting a static address for this client
    ifconfig-push 172.16.13.NUMBER 172.16.13.(NUMBER+1)
    # we're sending this client packets for it's /24 subnet
    iroute 192.168.(NUMBER).0


This adds routes to and through this VPN. Check the routing rules section.


set -x


ip route $C $trusted_ip via $(ip route get $trusted_ip | awk "{print \$3; exit}")
ip route $C default     via $ifconfig_remote    dev $dev        table "${config%.*}"
ip route $C default     via $ifconfig_remote    dev $dev        table fallback  metric $METRIC

Remember to make this script executable.

Routing πŸ”—

We want to use rule based routing to send packets incoming from different sources to various destinations through specific routes.

Everything should reach some destinations (e.g. our LAN), only a few hosts should be allowed directly to Internet, while others should be routed to the Internet through our VPNs.

We can do this by using rule-based routing.

Overview πŸ”—

Let's declare the routing tables we'll use in /etc/iproute2/rt_tables:

255     local
254     main
253     default
0       unspec
# local
#1      inr.ruhep
500     router    # only used to reach the router's subnet
1000    direct    # direct internet connection
2000    allievi   # through allievi VPN
3000    peori     # through peori VPN
9999    fallback  # everybody falling here

When the kernel needs to decide where to route a packet, it uses the first table (ascending order) that matches the packet. We want to...

  1. Let through all the packets directed to an enabled location (i.e. our local LAN, allievi and its network, peori and its network):
    $ ip rule | grep main
    32766:  from all lookup main                                         # everything matches
    $ ip route show table main via dev eth0                           # allievi VPN server via dev tun0                                # allievi LAN via dev tun0                               # allievi VPN client network dev tun0 proto kernel scope link src         # our allievi VPN client via dev eth0                            # peori VPN server via dev tun1                            # peori LAN dev tun1 proto kernel scope link src     # our peori VPN client dev eth0 proto kernel scope link src  # local LAN dev eth0 proto kernel scope link src  # router LAN # TODO: shouldn't this go in the router table?
  2. Let only our servers (with IPs in the range .1β†’.15) reach the router's LAN (in case they need to debug the router or whatever):
    $ ip rule | grep router
    50000:  from lookup router  # only our servers match
    $ ip route show table router
    # TODO: shouldn't the router for be here?
  3. Let only localhost and a few cherrypicked hosts directly to the Internet through our router:
    $ ip rule | grep direct
    100000: from all iif lo lookup direct              # localhost (lo interface) matches
    100000: from lookup direct          # hanna-tablet matches
    100000: from lookup direct           # boox matches
    $ ip route show table direct
    default via dev eth0 proto static  # local router address
  4. Let anything else reach the Internet through our VPNs:
    $ ip rule | grep fallback
    999999: from all lookup fallback              # everything matches
    $ ip route show table fallback
    default via dev tun0 metric 20    # internet through allievi
    default via dev tun1 metric 30  # internet through peori

Routing rules πŸ”—

We can modify routing rules with the ip rule command:

# packets from localhost (`lo` interface) go through the `direct` table
sudo ip rule add from all iif lo lookup direct priority 100000
# same for packets from
sudo ip rule add from lookup direct priority 100000
# etc

We can install rc-local and place those commands in /etc/rc.local.

Or, a better solution, is to use systemd-networkd to manage the whole configuration.

Routes πŸ”—

To modify the routes we can use ip route ... table $TABLE ... and ip rule ... table $TABLE ... or do it through some configuration files...

Systemd-networkd configuration πŸ”—

Let's add all the rules and routes to /etc/systemd/network/

# Interface we're matching

# The IP address we set to the interface

### Routing tables:

# `direct` table accepts packets from localhost
# ip rule add from all iif lo lookup direct priority 100000
IncomingInterface=lo  # fixed in

# `router` table accepts trusted hosts (.0~.15)
# ip rule add from .0/28 lookup fallback priority 500000

# `fallback` table accepts everything (with very low priority)
# ip rule add from all lookup fallback priority 999999

# adding a router through the router to `direct`
# ip route add default via .254 table direct dev eth0

# DISABLED, just for example...
# If our traffic to .254.0/24 (i.e. the actual router) needed to pass through .254 (i.e. access point between us and the router)
# ip route add .254.0/24 via .254 table router dev eth0
# [Route]
# Table=500
# Destination=
# Gateway=

The whole router table and everything about it, is just in case the router is on a different subnet than everything else.

This could be achieved if we use the router only as a modem (i.e. we don't use it neither as access point nor ethernet switch).

If that's not the case, ignore anything about it.

VPNs πŸ”—

We can also have alternative default routes (i.e. destination through our VPNs. We decided to place these in the fallback routing table, which will be available to everybody.

A cleanish way to do this, is to add (and remove) these routes to the right table when the VPN connection goes up. Our OpenVPN configuration already runs a script that adds a route to the VPN network to the main routing table, and a default route (with custom metrix - i.e. priority) to the fallback table.


Configure DHCPd4 in /etc/dhcpd.conf:

# enabling RFC 3442: classless static routes
option rfc3442-classless-static-routes code 121 = array of integer 8;
option ms-classless-static-routes code 249 = array of integer 8;

subnet netmask {
        # which IP addresses to assign

        # client's resolv.conf
        option domain-name-servers;
        option domain-search "l77.peoronet", "peoronet", "";
        #option domain-name-servers;

        # setting the gateway through odroid
        option routers;
        #option routers;

        host tin {
                hardware ethernet 10:bf:48:69:fd:91;
        # ...

Obviously fix all the IP addresses and domain names.

We'll need to start/enable the dhcpd4 service, but it's incompatible with the official Odroid kernel (3.16.57), since the latter doesn't support Ambient Capabilities. So create /etc/systemd/system/dhcpd4-peoro.service:

Description=IPv4 DHCP server

ExecStart=/usr/bin/dhcpd -4 -q -user dhcp -cf /etc/dhcpd.conf -pf /run/
# We pull in for a configured network connection.
# However this is not guaranteed to be the network connection our
# networks are configured for. So try to restart on failure with a delay
# of two seconds. Rate limiting kicks in after 12 seconds.


And start/enable that one instead.

DNS πŸ”—

We're using DNSMasq as DNS server.


# Not reading /etc/hosts
# Use /etc/hosts.l77 instead

# Use the pihole blacklist

# Ffilter out queries which the public DNS cannot answer, and which load the servers (especially the root servers) unnecessarily.
# these requests from bringing up the link unnecessarily.
# Never forward plain names (without a dot or domain part)
# Never forward addresses in the non-routed address spaces.

# Don't read /etc/resolv.conf or any other file.

# Sending a request in parallel to all our DNS servers, and using the first result
# Out DNS servers

# Cache size

# For debugging purposes, log each DNS query as it passes through dnsmasq.

# TTL for responses that come from /etc/hosts

# This allows it to continue functioning without being blocked by syslog, and allows syslog to use dnsmasq for DNS queries without risking deadlock

Pi Hole (ArchWiki) πŸ”—

Install pi-hole-server from AUR. Configure /etc/dnsmasq.conf and systemctl start pihole-FTL. Note that pi-hole will have installed its own fork of dnsmasq (called FTLDNS).

Append DBINTERVAL=60.0 to /etc/pihole/pihole-FTL.conf.

Run pihole -g to refresh the adblock list.

Disable (even temporarily) or re-enable pi-hole with:

pihole disable
pihole disable 5m  # 5 minutes
pihole enable

Web interface πŸ”—

TODO It sucks BTW D: It's in PHP...

Serial terminal πŸ”—

NOTE: if you want to use USB OTG while the Odroid is powered via a DC jack, you need to disconnect Odroid's jumper J1. Otherwise the odroid will overheat. (link). When J1 is connected, the odroid can be powered via the micro USB port.

Load the g_serial module on the raspberry to enable USB Gadget. We can load it permanently by creating /etc/modules-load.d/gserial.conf:

# Load g_serial, the gatget serial driver to use the micro USB port as a gadget device

Once that's up, we'll have /dev/ttyGS0. We probably want to start a getty on it:

sudo systemctl enable --now serial-getty@ttyGS0

On the connected computer we can now access the odroid terminal (it should be /dev/ttyACM0). Run:


# echo stty rows $LINES cols $COLUMNS

echo -e "Connecting to \e[31m/dev/ttyACM0\e[0m"
echo -e "Exit pressing \e[33m~^d\e[0m"
echo -e "After logging in, copy paste the following command:\e[0m"
echo -e "\e[32mstty rows $(tput lines) cols $(tput cols); set TERM $TERM; exec fish;\e[0m"
cu -l "/dev/ttyACM0"


  • We need a script to keep the hostname↔IP mapping synchronized between DHCP and DNS.
  • We need to try and get an IP address from an existing DHCP server - if we receive no IP, only then we start our one. This way we'd be able to seamlessly switch between managed and unmanaged networks on the fly, making testing a lot easier.