Linux Router with VPN on a Raspberry Pi

From Alpine Linux

Rationale

This guide demonstrates how to set up a Raspberry Pi as an open source Linux router with a VPN tunnel. You will need a USB ethernet adaptor. I chose the Apple USB Ethernet Adapter as it contains a ASIX AX88772 which has good Linux support. Be sure to not buy a cheap counterfeit one as they do exist. You may choose to also buy an RTC clock. If you don't have an RTC clock, the time is lost when your Pi is shut down. When it is rebooted, the time will be set back to Thursday, 1 January 1970. As this is earlier than the creation time of your VPN certificates OpenVPN will refuse to start, which may mean you cannot do DNS lookups over VPN.

I had decided against re-flashing a consumer router with an embedded firmware like OpenWrt, DD-WRT or Tomato because these devices were not intended to run an alternate firmware, and there is usually not any manufacturer support when doing so. Their hardware designs are also certainly not open at all.

Support for devices varies significantly depending on which one you have - sometimes down to the revision of the router. Integrating wireless into the device creates significant driver compatibility issues. Many chipsets (Broadcom, Marvell) for example either require closed source blobs or have a restrictive license.

For wireless, a separate access point was purchased (Ubiquiti UniFi AP) because it contains a Atheros AR9287 which is supported by ath9k and I was keen to avoid blob drivers.

You could choose to use an old x86/amd64 system instead. This may be a more attractive option if you want to route speeds above 30 Mbit/s. You are limited by the fact the USB and Ethernet share the same bus with the Raspberry Pi.

If you want to route speeds above 100 Mbit/s you'll want to make use of hardware encryption like AES-NI. You can also improve performance by not using OpenVPN in AES-CBC mode and instead use an OpenSSH tunnel with AES-CTR or stunnel with something like ECDHE-RSA-AES256-GCM-SHA384.

The network in this tutorial looks like this:

Network Diagram
Network Diagram

Installation

This guide assumes you're using AlpineLinux from a micro sdcard in ramdisk mode. It assumes you've read the basics of how to use Alpine local backup First format the sdcard as described, stop when you get to the end of the Create a Bootable USB#Format_USB_stick

Download armhf.rpi.tar.gz archive and extract into FAT32 partition. You will need version 3.2.0 or greater if you have a Raspberry Pi 2.

Modem in full bridge mode

Your modem will need to be configured in "full bridge mode". The method for doing this varies depending on the interface on your device and is out of the scope of this tutorial.

The modem I am using is a Cisco 877 Integrated Services Router. It has no web interface and is controlled over SSH. More information can be found Configuring a Cisco 877 in full bridge mode.

Configuring PPP

Next up we need to configure our router to be able to dial a PPP connection with our modem.

apk add ppp-pppoe

Check that the interface between your router and modem is eth1, or change it. Enter your credentials at the bottom of the file or use /etc/ppp/chap-secrets

/etc/ppp/peers/yourISP

nolog

# Try to get the IP address from the ISP
noipdefault

# Try to get the name server addresses from the ISP
usepeerdns

# Use this connection as the default route.
defaultroute

defaultroute-metric 300

# detatch after ppp0 interface is created
updetach

# Replace previous default route
#replacedefaultroute

# rp-pppoe plug-in makes PPPoE connection so rp-pppoe package is not needed
#  Possibly, you may need to change interface according your configuration
plugin rp-pppoe.so eth1

# Uncomment if you need on-demand connection
#demand

# Disconnect after 300 seconds (5 minutes) of idle time.
#idle 300

# Hide password from log entries
hide-password

# Send echo requests
lcp-echo-interval 20
lcp-echo-failure 3

# Do not authenticate ISP peer
noauth

# Control connection consistency
persist
maxfail 0

# Control MTU size if your ISP does not force it
#mtu 1492

# Set your credentials
#  Alternatively you may use /etc/ppp/pap-secrets or /etc/ppp/chap-secrets files
user username@yourISP.tld
password <SECRET>

/etc/modules

Update modules to include pppoe:

pppoe

Network

/etc/hostname

Set this to your hostname eg:

<HOST_NAME>

/etc/hosts

Set your host and hostname

127.0.0.1	<HOST_NAME> <HOST_NAME>.<DOMAIN_NAME>

::1		<HOST_NAME> ipv6-gateway ipv6-loopback
ff00::0		ipv6-localnet
ff00::0		ipv6-mcastprefix
ff02::1		ipv6-allnodes
ff02::2		ipv6-allrouters
ff02::3		ipv6-allhosts

/etc/network/interfaces

Configure your network interfaces:

auto lo
    iface lo inet loopback

# internal interface
auto eth0
iface eth0 inet static
	address 192.168.1.1
	netmask 255.255.255.0

# external interface
auto eth1
iface eth1 inet static
	address 192.168.0.2
	netmask 255.255.255.252

# internet connection
auto ppp0
iface ppp0 inet ppp
	pre-up ip link set dev eth1 up
	provider <yourISP> # Make sure this is the same as /etc/ppp/peers/yourISP

Basic IPtables firewall with routing

This demonstrates how to set up basic routing with a permissive outgoing firewall. Incoming packets are blocked. The rest is commented in the rule set.

First install iptables:

apk add iptables ip6tables

#################################
# Basic iptables routing rule set
#################################

# Set up the nat table
*nat
:PREROUTING ACCEPT
:INPUT ACCEPT
:OUTPUT ACCEPT
:POSTROUTING ACCEPT

# Bittorrent forwarded to Linux Workstation through VPN
-A PREROUTING -i ppp0 -p tcp -m tcp --dport 6881:6889 -j DNAT --to-destination 192.168.1.20/32
-A PREROUTING -i ppp0 -p udp -m udp --dport 6881:6889 -j DNAT --to-destination 192.168.1.20/32

# Allows us to ssh into our modem
-A POSTROUTING -s 192.168.1.0/24 -d 192.168.0.1/32 -p tcp -m tcp --dport 22 -o eth1 -j MASQUERADE

# Allows us to access our web interface on our modem
-A POSTROUTING -s 192.168.1.0/24 -d 192.168.0.1/32 -p tcp -m tcp --dport 80 -o eth1 -j MASQUERADE

# Allows for network hosts to access the internet via WAN port
-A POSTROUTING -s 192.168.1.0/24 -o ppp0 -j MASQUERADE

# Commit the nat table
COMMIT

# Set up the filter table
*filter
:INPUT DROP
:FORWARD DROP
:OUTPUT ACCEPT

# Create rule chain per input interface for forwarding packets
:FWD_ETH0 - [0:0]
:FWD_ETH1 - [0:0]
:FWD_PPP0 - [0:0]

# Create rule chain per input interface for input packets (for host itself)
:IN_ETH0 - [0:0]
:IN_ETH1 - [0:0]
:IN_PPP0 - [0:0]

# Pass input packet to corresponded rule chain
-A INPUT -i lo -j ACCEPT
-A INPUT -i eth0 -j IN_ETH0
-A INPUT -i eth1 -j IN_ETH1
-A INPUT -i ppp0 -j IN_PPP0

# Log packets that are dropped in INPUT chain (useful for debugging)
-A INPUT -j LOG --log-prefix "INPUT: "

# Pass forwarded packet to corresponded rule chain
-A FORWARD -i eth0 -j FWD_ETH0
-A FORWARD -i eth1 -j FWD_ETH1
-A FORWARD -i ppp0 -j FWD_PPP0

# Log packets that are dropped in FORWARD chain (useful for debugging)
-A FORWARD -j LOG --log-prefix "FORWARD: "

# Forward traffic to LAN
-A FWD_ETH0 -s 192.168.1.0/24 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# Forward SSH packets from network to modem
-A FWD_ETH1 -s 192.168.0.1/30 -d 192.168.1.0/24 -p tcp -m tcp --sport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# Forward HTTP packets from network to modem
#-A FWD_ETH1 -s 192.168.0.1/30 -d 192.168.1.0/24 -p tcp -m tcp --sport 80 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# Forward traffic to ppp0 WAN port
-A FWD_PPP0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# SSH to Router
-A IN_ETH0 -s 192.168.1.0/24 -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# DNS to Router
-A IN_ETH0 -s 192.168.1.0/24 -p udp -m udp --dport 53 -m conntrack --ctstate NEW -j ACCEPT

# FreeRadius Client
-A IN_ETH0 -s 192.168.1.0/24 -p tcp -m tcp --dport 1812 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
-A IN_ETH0 -s 192.168.1.0/24 -p udp -m udp --dport 1812 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# NTP
-A IN_ETH0 -s 192.168.1.0/24 -p udp -m udp --dport 123 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# Accept traffic to router
-A IN_ETH0 -s 192.168.1.0/24 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# SSH To Modem from Router
-A IN_ETH1 -s 192.168.0.1/32 -d 192.168.0.0/30 -p tcp -m tcp --sport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# Accept incoming tracked PPP0 connection
-A IN_PPP0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# Commit the filter table
COMMIT

# Commit mangle tables (empty at the moment)
*mangle
:PREROUTING ACCEPT
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
:POSTROUTING ACCEPT
COMMIT

I'd also highly suggest reading these resources if you are new to iptables:

/etc/sysctl.conf

These sysctl settings harden a few things and were mostly borrowed from the ArchLinux wiki.

net.ipv4.ip_forward = 1
net.ipv4.conf.default.rp_filter = 1
kernel.panic = 120

#### ipv4 networking and equivalent ipv6 parameters ####

## TCP SYN cookie protection (default)
## helps protect against SYN flood attacks
## only kicks in when net.ipv4.tcp_max_syn_backlog is reached
net.ipv4.tcp_syncookies = 1

## protect against tcp time-wait assassination hazards
## drop RST packets for sockets in the time-wait state
## (not widely supported outside of linux, but conforms to RFC)
net.ipv4.tcp_rfc1337 = 1

## sets the kernels reverse path filtering mechanism to value 1(on)
## will do source validation of the packet's recieved from all the interfaces on the machine
## protects from attackers that are using ip spoofing methods to do harm
net.ipv4.conf.all.rp_filter = 1
net.ipv6.conf.all.rp_filter = 1

## tcp timestamps
## + protect against wrapping sequence numbers (at gigabit speeds)
## + round trip time calculation implemented in TCP
## - causes extra overhead and allows uptime detection by scanners like nmap
## enable @ gigabit speeds
net.ipv4.tcp_timestamps = 0
#net.ipv4.tcp_timestamps = 1

## log martian packets
net.ipv4.conf.all.log_martians = 1

## ignore echo broadcast requests to prevent being part of smurf attacks (default)
net.ipv4.icmp_echo_ignore_broadcasts = 1

## ignore bogus icmp errors (default)
net.ipv4.icmp_ignore_bogus_error_responses = 1

## send redirects (not a router, disable it)
net.ipv4.conf.all.send_redirects = 0

## ICMP routing redirects (only secure)
#net.ipv4.conf.all.secure_redirects = 1 (default)
net/ipv4/conf/default/accept_redirects=0
net/ipv4/conf/all/accept_redirects=0
net/ipv6/conf/default/accept_redirects=0
net/ipv6/conf/all/accept_redirects=0

# Disable ipv6
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1

DHCP

apk add dhcp

/etc/conf.d/dhcpd

Change DHCPD_IFACE="eth0" to the interface you want DHCP to run on.

/etc/dhcp/dhcpd.conf

Configure your DHCP configuration server. For my DHCP server I'm going to have three subnets. Each has a specific purpose. You may choose to have any number of subnets like below. The broadcast-address would be different if you used VLANs. However in this case we are not.

authoritative;
ddns-update-style interim;

shared-network home {
    # Subnet for regular nodes that require direct Internet access
    subnet 192.168.1.0 netmask 255.255.255.0 {
        range 192.168.1.10 192.168.1.240;
	default-lease-time 259200;
	max-lease-time 518400;
	option subnet-mask 255.255.255.0;
	option broadcast-address 192.168.1.255;
	option routers 192.168.1.1;
	option ntp-servers 192.168.1.1;
	option domain-name-servers 192.168.1.1;
	allow unknown-clients;

        host Gaming_Computer {
            hardware ethernet XX:XX:XX:XX:XX:XX;
            fixed-address 192.168.1.20;
            option subnet-mask 255.255.255.0;
            option routers 192.168.1.1;
	}
    }

    # Subnet for regular nodes that require VPN access
    subnet 192.168.2.0 netmask 255.255.255.0 {
        range 192.168.2.10 192.168.2.240;
	default-lease-time 259200;
	max-lease-time 518400;
	option subnet-mask 255.255.255.0;
	option broadcast-address 192.168.1.255;
	option routers 192.168.2.1;
	option domain-name-servers 192.168.1.1;
	ignore unknown-clients;

        host Linux_Workstation {
            hardware ethernet YY:YY:YY:YY:YY:YY;
            fixed-address 192.168.2.20;
            option subnet-mask 255.255.255.0;
            option routers 192.168.2.1;
	}
    }

    # Subnet for regular nodes that require no Internet access
    subnet 192.168.3.0 netmask 255.255.255.0 {
        range 192.168.3.10 192.168.3.240;
	default-lease-time 259200;
	max-lease-time 518400;
	option subnet-mask 255.255.255.0;
	option broadcast-address 192.168.1.255;
	option routers 192.168.3.1;
	option domain-name-servers 192.168.1.1;
	ignore unknown-clients;

        host printer {
            hardware ethernet ZZ:ZZ:ZZ:ZZ:ZZ:ZZ;
            fixed-address 192.168.3.9;
            option subnet-mask 255.255.255.0;
            option routers 192.168.3.1;
	}
    }
}

Make sure to add this to the default run level once configured:

rc-update add dhcp default

Synchronizing the clock

You can choose to use BusyBox's ntpd or you can choose a more fully fledged option like OpenNTPD

Busybox /etc/conf.d/ntpd

Allow clients to synchronize their clocks with the router.

# By default ntpd runs as a client. Add -l to run as a server on port 123.
NTPD_OPTS="-l -N -p <REMOTE TIME SERVER>"

Make sure to add this to the default run level once configured:

rc-update add ntpd default

Or if you prefer to synchronize with multiple servers...

OpenNTPD /etc/ntpd.conf

Install OpenNTPD

apk add openntpd

Add to default run level.

rc-update add openntpd default

/etc/ntpd.conf

# sample ntpd configuration file, see ntpd.conf(5)

# Addresses to listen on (ntpd does not listen by default)
listen on 192.168.1.1
listen on 192.168.2.1

# sync to a single server
#server ntp.example.org

# use a random selection of NTP Pool Time Servers
# see http://support.ntp.org/bin/view/Servers/NTPPoolServers
server 0.pool.ntp.org
server 1.pool.ntp.org
server 2.pool.ntp.org
server 3.pool.ntp.org

tlsdate

The time can also be extracted from a https handshake. If the certificate is self-signed you will need to use skip-verification:

apk add tlsdate

tlsdate -V --skip-verification -p 80 -H example.com

timezone

You might also want to set a timezone, see Setting the timezone.

Saving Time

There are two ways to do this. If you didn't buy an RTC clock see Saving time with Software Clock. If you did like the PiFace Real Time Clock see Saving time with Hardware Clock

Unbound DNS forwarder with dnscrypt

We want to be able to do our lookups using dnscrypt without installing dnscrypt on every client on the network. Therefore the router will also run a DNS forwarder and request unknown domains over dnscrypt for our clients.

Unbound

First install

apk add unbound

/etc/unbound/unbound.conf

# unbound.conf(5) man page.
#
# See /usr/share/doc/unbound/examples/unbound.conf for a commented
# reference config file.

server:
    # The following line will configure unbound to perform cryptographic
    # DNSSEC validation using the root trust anchor.
    # auto-trust-anchor-file: "/var/lib/unbound/root.key"
server:
verbosity: 1
num-threads: 4                                                        
interface: 192.168.1.1
 do-ip4: yes
 do-udp: yes
 do-tcp: yes
 access-control: 192.168.1.0/24 allow  # Specify the subnets you want to listen on
 access-control: 192.168.2.0/24 allow
 do-not-query-localhost: no
 chroot: ""       
 logfile: "/var/log/unbound.log"             
 use-syslog: no 
 hide-identity: yes
 hide-version: yes 
 harden-glue: yes
 harden-dnssec-stripped: yes
 use-caps-for-id: yes       
 private-domain: "<HOSTNAME>"      
 #local-zone: "localhost." static
 #local-data: "freebox.localhost. IN A 192.168.0.254"                                              
 #local-data-ptr: "192.168.0.254 freebox.localhost"
python:
remote-control:
forward-zone:
  name: "."
  forward-addr: 127.0.0.2@53

/etc/network/interfaces

You'll need a second loopback device, put it under the already existing one.

auto lo
    iface lo inet loopback

auto lo:1
iface lo:1 inet static
	address 127.0.0.2
	netmask 255.0.0.0

DNSCrypt

You'll need to pin the testing repository. See: Alpine Linux package management#Repository pinning

Then install:

apk add dnscrypt-proxy@testing

/etc/conf.d/dnscrypt-proxy

Enter a dnscrypt server, it should look something like this:

# DNSCRYPT_LOGFILE=/var/log/dnscrypt-proxy/dnscrypt-proxy.log

# override listen address where DNSCRYPT listen
DNSCRYPT_LOCALIP=127.0.0.2:53

RESOLVER=208.67.220.220:443                                                        
PROVIDER=2.dnscrypt-cert.opendns.com                                               
PUBKEY=B735:1140:206F:225D:3E2B:D822:D7FD:691E:A1C3:3CC8:D666:8D0C:BE04:BFAB:CA43:FB79

Finally add both to the default run level

rc-update add unbound default

rc-update add dnscrypt-proxy default

VPN Tunnel on specific subnet

As mentioned earlier in this article it might be useful to have a VPN subnet and a non-VPN subnet. Typically gaming consoles or computers might want low-latency connections. For this exercise we use fwmark.

Install the necessary packages:

apk add openvpn iproute2 iputils

/etc/modules

You'll want to add the tun module

tun

/etc/iproute2/rt_tables

Add the two routing tables to the bottom of rt_tables. It should look something like this:

#
# reserved values
#
255	local
254	main
253	default
0	unspec
#
# local
#
#1	inr.ruhep
1 ISP
2 VPN

/etc/network/interfaces

Next up add the virtual interface: eth0:2, just under eth0 will do:

auto eth0
iface eth0 inet static
	address 192.168.1.1
	netmask 255.255.255.0

# Virtual interface
auto eth0:2
iface eth0:2 inet static
	address 192.168.2.1
	netmask 255.255.255.0
        post-up /etc/network/fwmark_rules

/etc/sysctl.conf

If you want to use fwmark rules you need to change this setting. It causes the router to still do source validation.

net.ipv4.conf.all.rp_filter = 2

fwmark won't work if you have this set to 1.

/etc/network/fwmark_rules

In this file we want to put the fwmark rules and set the correct priorities.

#!/bin/sh

# Normal packets to go direct out WAN
/sbin/ip rule add fwmark 1 table ISP prio 100

# Put packets destined into VPN when VPN is up
/sbin/ip rule add fwmark 2 table VPN prio 200

# Prevent packets from being routed out when VPN is down.
# This prevents packets from falling back to the main table
# that has a priority of 32766
/sbin/ip rule add prohibit fwmark 2 prio 300

/etc/ppp/ip-up

Next up we want to create the routes that should be run when PPP comes online. There are special hooks we can use in ip-up and ip-down to refer to the IP address, ppp man file - Scripts You can also read about them in your man file if you have ppp-doc installed.

#!/bin/sh
#
# This script is run by pppd when there's a successful ppp connection.
#

# Flush out any old rules that might be there
/sbin/ip route flush table ISP

# Add route to table from subnets on LAN
/sbin/ip route add 192.168.1.0/24 dev eth0 table ISP prio 100
/sbin/ip route add 192.168.2.0/24 dev eth0 table ISP prio 200

# Add route from IP given by ISP to the table
/sbin/ip rule add from ${IPLOCAL} table ISP prio 100

# Add a default route
/sbin/ip route add table ISP default via ${IPLOCAL} prio 100

# Add route to Modem subnet
/sbin/ip route add 192.168.0.0/30 dev eth0 table ISP

/etc/ppp/ip-down

#!/bin/sh
#
# This script is run by pppd after the connection has ended.
#

# Delete the rules when we take the interface down
/sbin/ip rule del from ${IPLOCAL} table ISP prio 100

/etc/openvpn/route-up-fwmark.sh

OpenVPN needs similar routing scripts and it also has it's own special hooks that allow you to specify particular values. A full list is here OpenVPN man file - Environmental Variables

#!/bin/sh
#
# This script is run by OpenVPN when there's a successful VPN connection.
#

# Flush out any old rules that might be there
/sbin/ip route flush table VPN

# Add route to table from 192.168.2.0/24 subnet on LAN
/sbin/ip route add 192.168.2.0/24 dev eth0 table VPN

# Add route from VPN interface IP to the VPN table
/sbin/ip rule add from ${ifconfig_local} table VPN prio 200

# Add a default route
/sbin/ip route add default via ${ifconfig_local} dev ${dev} table VPN

/etc/openvpn/route-pre-down-fwmark.sh

#!/bin/sh
#
# This script is run by OpenVPN after the connection has ended
#

# Delete the rules when we take the interface down
/sbin/ip rule del from ${ifconfig_local} table VPN prio 200

What I did find was when starting and stopping the OpenVPN service if you used:

service openvpn stop

The rules in route-pre-down-fwmark.sh were not executed.

However:

/etc/init.d/openvpn stop

seemed to work correctly.

Advanced IPtables rules that allow us to route into our two routing tables

This is an expansion of the previous set of rules. It sets up NAT masquerading for the 192.168.2.0 to go through the VPN.

##################################################################################################################
# Advanced routing rule set
# Uses 192.168.1.0 via ISP
#      192.168.2.0 via VPN
#
# Packets to/from 192.168.1.0/24 are marked with 0x1 and routed to ISP
# Packets to/from 192.168.2.0/24 are marked with 0x2 and routed to VPN
# 
# http://nerdboys.com/2006/05/05/conning-the-mark-multiwan-connections-using-iptables-mark-connmark-and-iproute2/
# http://nerdboys.com/2006/05/08/multiwan-connections-addendum/
#
##################################################################################################################

#
# Mangle Table
# This is the place where our markings happen, whether they be 0x1 or 0x2
#
*mangle

# Set default policies for table
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]

# Restore CONNMARK to the MARK (If one doesn't exist then no mark is set)
-A PREROUTING -j CONNMARK --restore-mark --nfmask 0xffffffff --ctmask 0xffffffff

# If packet MARK is 2, then it means there is already a connection mark and the original packet came in on VPN
-A PREROUTING -s 192.168.2.0/24 -m mark --mark 0x2 -j ACCEPT

# Check exception (this is a server which when accessed on a 192.168.2.0/24 address will go out the ISP table) are 0x1
#-A PREROUTING -s 192.168.2.0/24 -d <IP_OF_EXCEPTED_SERVER>/32 -m mark --mark 0x1 -j ACCEPT

# Check packets coming from 192.168.2.0/24 are 0x2
-A PREROUTING -s 192.168.2.0/24 -j MARK --set-xmark 0x2/0xffffffff

# If packet MARK is 1, then it means there is already a connection mark and the original packet came in on ISP
-A PREROUTING -s 192.168.1.0/24 -m mark --mark 0x1 -j ACCEPT

# Check packets coming from 192.168.1.0/24 are 0x1
-A PREROUTING -s 192.168.1.0/24 -j MARK --set-xmark 0x1/0xffffffff

# Mark exception (this is a server which when accessed on a 192.168.2.0/24 address will go out the ISP table) as 0x1
#-A PREROUTING -s 192.168.2.0/24 -d <IP_OF_EXCEPTED_SERVER>/32 -j MARK --set-xmark 0x1/0xffffffff

# Save MARK to CONNMARK (remember iproute can't see CONNMARKs)
-A PREROUTING -j CONNMARK --save-mark --nfmask 0xffffffff --ctmask 0xffffffff
COMMIT

#
# Filter Table
# This is where we decide to ACCEPT, DROP or REJECT things
#
*filter

# Set default policies for table
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Create rule chain per input interface for forwarding packets
:FWD_ETH0 - [0:0]
:FWD_ETH1 - [0:0]
:FWD_PPP0 - [0:0]
:FWD_TUN0 - [0:0]

# Create rule chain per input interface for input packets (for host itself)
:IN_ETH0 - [0:0]
:IN_ETH1 - [0:0]
:IN_PPP0 - [0:0]
:IN_TUN0 - [0:0]

# Pass input packet to corresponding rule chain
-A INPUT -i lo -j ACCEPT
-A INPUT -i eth0 -j IN_ETH0
-A INPUT -i eth1 -j IN_ETH1
-A INPUT -i ppp0 -j IN_PPP0
-A INPUT -i tun0 -j IN_TUN0

# Log packets that are dropped in INPUT chain
-A INPUT -j LOG --log-prefix "DROPPED INPUT: "

# Track forwarded packets
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# Pass forwarded packet to corresponding rule chain
-A FORWARD -i eth0 -j FWD_ETH0
-A FORWARD -i eth1 -j FWD_ETH1
-A FORWARD -i ppp0 -j FWD_PPP0
-A FORWARD -i tun0 -j FWD_TUN0

# Log packets that are dropped in FORWARD chain
-A FORWARD -j LOG --log-prefix "DROPPED FORWARD: "

# Forward traffic to ISP
-A FWD_ETH0 -s 192.168.1.0/24 -j ACCEPT

# Forward traffic to VPN
-A FWD_ETH0 -s 192.168.2.0/24 -j ACCEPT

# Allow excepted server to be FORWARD to ppp0
#-A FWD_ETH0 -s 192.168.2.0/24 -d <IP_OF_EXCEPTED_SERVER>/32 -o ppp0 -j ACCEPT

# Forward SSH packets from network to modem
-A FWD_ETH1 -s 192.168.0.0/30 -d 192.168.1.0/24 -p tcp -m tcp --sport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
-A FWD_ETH1 -s 192.168.0.0/30 -d 192.168.2.0/24 -p tcp -m tcp --sport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# SSH to Router
-A IN_ETH0 -s 192.168.1.0/24 -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
-A IN_ETH0 -s 192.168.2.0/24 -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# DNS to Router
-A IN_ETH0 -s 192.168.1.0/24 -p udp -m udp --dport 53 -m conntrack --ctstate NEW -j ACCEPT
-A IN_ETH0 -s 192.168.2.0/24 -p udp -m udp --dport 53 -m conntrack --ctstate NEW -j ACCEPT

# FreeRadius Client (eg a UniFi AP)
-A IN_ETH0 -s 192.168.1.0/24 -p tcp -m tcp --dport 1812 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
-A IN_ETH0 -s 192.168.1.0/24 -p udp -m udp --dport 1812 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# Ubiquiti UAP Device Discovery Broadcast
-A IN_ETH0 -s 192.168.1.0/24 -p udp -m udp --dport 10001 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# NTP to Router
-A IN_ETH0 -s 192.168.1.0/24 -p udp -m udp --dport 123 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
-A IN_ETH0 -s 192.168.2.0/24 -p udp -m udp --dport 123 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# Accept traffic to router on both subnets
-A IN_ETH0 -s 192.168.1.0/24 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
-A IN_ETH0 -s 192.168.2.0/24 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# Allow excepted server to be INPUT to eth0 from LAN
#-A IN_ETH0 -s 192.168.2.0/24 -d <IP_OF_EXCEPTED_SERVER>/32 -o ppp0 -j ACCEPT

# SSH To Modem from Router
-A IN_ETH1 -s 192.168.0.0/30 -d 192.168.0.0/30 -p tcp -m tcp --sport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# Accept incoming tracked PPP0 connection
-A IN_PPP0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# Accept incoming tracked TUN0 connection
-A IN_TUN0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT

#
# NAT Table
# This is where translation of packets happens and "forwarding" of ports
# to specific hosts.
#
*nat

# Set default policies for table
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]

# Port forwarding for Bittorrent
-A PREROUTING -i tun0 -p tcp -m tcp --dport 6881:6889 -j DNAT --to-destination 192.168.2.20
-A PREROUTING -i tun0 -p udp -m udp --dport 6881:6889 -j DNAT --to-destination 192.168.2.20

# Allows hosts of the network to use the VPN tunnel
-A POSTROUTING -o tun0 -j MASQUERADE

# Allows hosts of the network to use the PPP tunnel
-A POSTROUTING -o ppp0 -j MASQUERADE
COMMIT

You may want to delete certain rules here that do not apply to you, eg the FreeRadius rules. That is covered later in this article.

OpenVPN Routing

Usually when you connect with OpenVPN the remote VPN server will push routes down to your system. We don't want this as we still want to be able to access the internet without the VPN. We have also created our own routes that we want to use earlier in this guide.

You'll need to add this to the bottom of your OpenVPN configuration file:

# Prevents default gateway from being set on the default routing table
route-noexec

# Allows route-up script to be executed
script-security 2

# Calls custom shell script after connection to add necessary routes
route-up /etc/openvpn/route-up-fwmark.sh
route-pre-down /etc/openvpn/route-pre-down-fwmark.sh

My VPNs are arranged like this in /etc/openvpn:

OpenVPN configuration file for that server:

countrycode.serverNumber.openvpn.conf

OpenVPN certs for that server:

countrycode.serverNumber.openvpn/countrycode.serverNumber.openvpn.crt
countrycode.serverNumber.openvpn/countrycode.serverNumber.openvpn.key
countrycode.serverNumber.openvpn/myKey.crt
countrycode.serverNumber.openvpn/myKey.key

So I use this helpful script to automate the process of changing between servers:

#!/bin/sh

vpn_server_filename=$1

rm -rf /etc/openvpn/openvpn.conf
ln -s $vpn_server_filename /etc/openvpn/openvpn.conf
chown -R openvpn:openvpn /etc/openvpn
chmod -R a=-rwx,u=+rX /etc/openvpn
chmod u=x /etc/openvpn/*.sh*


if grep -Fxq "#CustomStuffHere" openvpn.conf
then
    echo "Not adding custom routes, this server has been used previously"
else
    echo "Adding custom route rules"
    
    echo -e "#CustomStuffHere\
\n# Prevents default gateway from being set on the default routing table\
\nroute-noexec\
\n# Allows route-up script to be executed\
\nscript-security 2 \
\n# Calls custom shell script after connection to add necessary routes\
\nroute-up /etc/openvpn/route-up-fwmark.sh\
\nroute-pre-down /etc/openvpn/route-pre-down-fwmark.sh\
\n# Logging of OpenVPN to file\
\n#log /etc/openvpn/openvpn.log"\
>> /etc/openvpn/openvpn.conf
fi

echo "Remember to set BitTorrent port forward in vcp.ovpn.to control panel"

That way I can simply change between servers by running:

changevpn.sh countrycode.serverNumber.openvpn

and then restart openvpn. I am also reminded to put the port forward through on the VPN control panel so my BitTorrent client is connectable:

service openvpn restart

Finally add openvpn to the default run level

rc-update add openvpn default

WiFi 802.1x EAP and FreeRadius

A more secure way than using pre-shared keys (WPA2) is to use EAP-TLS and use separate certificates for each device. See FreeRadius EAP-TLS configuration

Other Tips

lbu cache

Configure lbu cache so that you don't need to download packages when you restart your router eg Local APK cache

This is particularly important as some of the images do not contain ppp-pppoe. This might mean you're unable to get an internet connection to download the other packages on boot.

lbu encryption /etc/lbu/lbu.conf

In /etc/lbu/lbu.conf you might want to enable encryption to protect your VPN keys.

# what cipher to use with -e option
DEFAULT_CIPHER=aes-256-cbc

# Uncomment the row below to encrypt config by default
ENCRYPTION=$DEFAULT_CIPHER

# Uncomment below to avoid <media> option to 'lbu commit'
# Can also be set to 'floppy'
LBU_MEDIA=mmcblk0p1

# Set the LBU_BACKUPDIR variable in case you prefer to save the apkovls
# in a normal directory instead of mounting an external media.
# LBU_BACKUPDIR=/root/config-backups

# Uncomment below to let lbu make up to 3 backups
# BACKUP_LIMIT=3

Remember to set a root password, by default Alpine Linux's root account is passwordless.

passwd root

Backup apkprov

It's a good idea to back up your apk provision file. You can pull it off your router to your local workstation with:

scp -r root@192.168.2.1:/media/mmcblk0p1/<YOUR HOST NAME>.apkovl.tar.gz.aes-256-cbc ./

And decrypt it with:

openssl enc -d -aes-256-cbc -in <YOUR HOST NAME>.apkovl.tar.gz.aes-256-cbc -out <YOUR HOST NAME>.apkovl.tar.gz

It can be encrypted with:

openssl aes-256-cbc -salt -in <YOUR HOST NAME>.apkovl.tar.gz -out <YOUR HOST NAME>.apkovl.tar.gz.aes-256-cbc

Harden SSH

Generate a SSH key

ssh-keygen -t rsa -b 4096

You will want to put the contents of id_rsa.pub in /etc/ssh/authorized_keys

You can put multiple public keys on multiple lines if more than one person has access to the router.

/etc/ssh/sshd_config

A couple of good options to set in here can be:

ListenAddress 192.168.1.1
ListenAddress 192.168.2.1

While this isn't usually a good idea, a router doesn't need more than one user.

PermitRootLogin yes

The most important options:

RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile  /etc/ssh/authorized_keys
PasswordAuthentication no
PermitEmptyPasswords no
AllowTcpForwarding no
X11Forwarding no

/etc/conf.d/sshd

You will want to add

rc_need="net"

This instructs OpenRC to make sure the network is up before starting ssh.


Finally add sshd to the default run level

rc-update add sshd default

References