Linux Router with VPN on a Raspberry Pi
Note this article is a work in progress!
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:
Installation
This guide assumes you're using AlpineLinux from a micro sdcard in ram-disk mode. First format the sdcard as described, stop when you get to the end of the installation guide 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 post-down ip link set dev eth1 down
Basic IPtables firewall with routing
This demonstrates how to set up basic routing with a permissive outgoing firewall. Incoming packets are blocked.
First install iptables:
apk add iptables ip6tables
#!/bin/sh iptables -F iptables -t nat -F export WAN=ppp0 export INT_IF=eth0 export EXT_IF=eth1 export WAN_TUNNEL=tun0 # Allows internet access on gateway iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A INPUT -m conntrack --ctstate INVALID -j DROP # ICMP iptables -A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable iptables -A INPUT -p tcp -j REJECT --reject-with tcp-rst iptables -A INPUT -j REJECT --reject-with icmp-proto-unreachable # SSH To Modem iptables -A FORWARD -i ${INT_IF} -o ${EXT_IF} -s 192.168.1.0/24 -d 192.168.0.0/30 -j ACCEPT iptables -A FORWARD -i ${EXT_IF} -o ${INT_IF} -s 192.168.0.0/30 -d 192.168.1.0/24 -j ACCEPT iptables -A FORWARD -i ${INT_IF} -o ${EXT_IF} -s 192.168.1.0/24 -d 192.168.0.0/30 -j ACCEPT iptables -A FORWARD -i ${EXT_IF} -o ${INT_IF} -s 192.168.0.0/30 -d 192.168.1.0/24 -j ACCEPT # SSH To Router iptables -A INPUT -i ${INT_IF} -p tcp -s 192.168.1.0/24 --dport ssh -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT iptables -A OUTPUT -o ${INT_IF} -p tcp --sport ssh -m conntrack --ctstate ESTABLISHED -j ACCEPT # Accept DNS from LAN (This router runs a DNS forwarder) iptables -A INPUT -i ${INT_IF} -p tcp -s 192.168.1.0/24 --dport 53 -j ACCEPT iptables -A OUTPUT -o ${INT_IF} -p tcp --sport 53 -j ACCEPT iptables -A INPUT -i ${INT_IF} -p udp -s 192.168.1.0/24 --dport 53 -j ACCEPT iptables -A OUTPUT -o ${INT_IF} -p udp --sport 53 -j ACCEPT # Accept NTP from LAN iptables -A INPUT -i ${INT_IF} -p tcp -s 192.168.1.0/24 --dport 123 -j ACCEPT iptables -A OUTPUT -o ${INT_IF} -p tcp --sport 123 -j ACCEPT iptables -A INPUT -i ${INT_IF} -p udp -s 192.168.1.0/24 --dport 123 -j ACCEPT iptables -A OUTPUT -o ${INT_IF} -p udp --sport 123 -j ACCEPT #Locks down certain services so they only work from the LAN: iptables -I INPUT 1 -i ${INT_IF} -j ACCEPT iptables -I INPUT 1 -i lo -j ACCEPT iptables -A INPUT -p UDP --dport bootps ! -i ${INT_IF} -j REJECT iptables -A INPUT -p UDP --dport domain ! -i ${INT_IF} -j REJECT # Drop TCP / UDP packets to privileged ports iptables -A INPUT -p TCP ! -i ${INT_IF} -d 0/0 --dport 0:1023 -j DROP iptables -A INPUT -p UDP ! -i ${INT_IF} -d 0/0 --dport 0:1023 -j DROP iptables -I FORWARD -i ${INT_IF} -d 192.168.1.0/24 -j DROP iptables -A FORWARD -i ${INT_IF} -s 192.168.1.0/24 -j ACCEPT iptables -A FORWARD -i ${WAN} -d 192.168.1.0/24 -j ACCEPT iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o ${WAN} -j MASQUERADE # Bittorrent forward (before we had a VPN tunnel) iptables -t nat -A PREROUTING -p tcp --dport 6881:6889 -i ${WAN} -j DNAT --to 192.168.1.100 iptables -t nat -A PREROUTING -p udp --dport 6881:6889 -i ${WAN} -j DNAT --to 192.168.1.100 iptables -P INPUT DROP iptables -P OUTPUT ACCEPT iptables -P FORWARD DROP echo 1 > /proc/sys/net/ipv4/ip_forward for f in /proc/sys/net/ipv4/conf/*/rp_filter ; do echo 1 > $f ; done /etc/init.d/iptables save
/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 file:
authoritative; ddns-update-style interim; 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; # Note we've set these to the same IP address # as the router because it will run a ntp forwarder through tlsdate option domain-name-servers 192.168.1.1; # Note we've set these to the same IP address # as the router because it will run a DNS forwarder through dnscypt } host printer { hardware ethernet XX:XX:XX:XX:XX:XX; # Set fixed addresses, eg a printer fixed-address 192.168.1.9; }
Make sure to add this to the default run level once configured:
rc-update add dhcp default
ntpd
/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
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 as mentioned.
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.
Install the necessary packages:
apk add openvpn iproute2 iputils
/etc/iproute2
Add "10 vpn" 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 10 vpn
/etc/network/interfaces
Next up add the virtual interface: eth0:2.
# Loop back interface auto lo iface lo inet loopback # Loop back interface used by unbound+dnscrypt-proxy auto lo:1 iface lo:1 inet static address 127.0.0.2 netmask 255.0.0.0 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 # The rules tell the router to route anything on 192.168.2.0/24 into the VPN routing table up ip rule add from 192.168.2.0/24 table vpn up ip rule add to 192.168.2.0/24 table vpn # Some server you don't want to go through the VPN tunnel when on the 192.168.2.0/24 (eg voip server) # I found this useful when I wanted to receive SIP calls on my phone up ip rule add to <IP OF VOIP SERVER> table main prio 32000 up ip rule add from <IP OF VOIP SERVER> table main prio 32001 # Another exception - Note the priority is increased by one per rule up ip rule add to <IP OF OTHER SERVER> table main prio 32002 up ip rule add from <IP OF OTHER SERVER> table main prio 32003 # 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 internode post-down ip link set dev eth1 down
Advanced IPtables rules that allow us to route into our two routing tables
#!/bin/sh iptables -F iptables -t nat -F export WAN=ppp0 export INT_IF=eth0 export EXT_IF=eth1 export WAN_TUNNEL=tun0 export VPN_VIRT_IF=eth0:2 # Allows internet access on gateway iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT iptables -A INPUT -m conntrack --ctstate INVALID -j DROP # ICMP iptables -A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable iptables -A INPUT -p tcp -j REJECT --reject-with tcp-rst iptables -A INPUT -j REJECT --reject-with icmp-proto-unreachable # SSH To Modem iptables -A FORWARD -i ${INT_IF} -o ${EXT_IF} -s 192.168.1.0/24 -d 192.168.0.0/30 -j ACCEPT iptables -A FORWARD -i ${EXT_IF} -o ${INT_IF} -s 192.168.0.0/30 -d 192.168.1.0/24 -j ACCEPT iptables -A FORWARD -i ${INT_IF} -o ${EXT_IF} -s 192.168.1.0/24 -d 192.168.0.0/30 -j ACCEPT iptables -A FORWARD -i ${EXT_IF} -o ${INT_IF} -s 192.168.0.0/30 -d 192.168.1.0/24 -j ACCEPT # SSH To Router iptables -A INPUT -i ${INT_IF} -p tcp -s 192.168.1.0/24 --dport ssh -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT iptables -A INPUT -i ${INT_IF} -p tcp -s 192.168.2.0/24 --dport ssh -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT iptables -A OUTPUT -o ${INT_IF} -p tcp --sport ssh -m conntrack --ctstate ESTABLISHED -j ACCEPT # Accept DNS from LAN (This router runs a DNS forwarder) iptables -A INPUT -i ${INT_IF} -p tcp -s 192.168.1.0/24 --dport 53 -j ACCEPT iptables -A OUTPUT -o ${INT_IF} -p tcp --sport 53 -j ACCEPT iptables -A INPUT -i ${INT_IF} -p udp -s 192.168.1.0/24 --dport 53 -j ACCEPT iptables -A OUTPUT -o ${INT_IF} -p udp --sport 53 -j ACCEPT iptables -A INPUT -i ${VPN_VIRT_IF} -p tcp -s 192.168.2.0/24 --dport 53 -j ACCEPT iptables -A OUTPUT -o ${VPN_VIRT_IF} -p tcp --sport 53 -j ACCEPT iptables -A INPUT -i ${VPN_VIRT_IF} -p udp -s 192.168.2.0/24 --dport 53 -j ACCEPT iptables -A OUTPUT -o ${VPN_VIRT_IF} -p udp --sport 53 -j ACCEPT # Accept NTP from LAN iptables -A INPUT -i ${INT_IF} -p tcp -s 192.168.1.0/24 --dport 123 -j ACCEPT iptables -A OUTPUT -o ${INT_IF} -p tcp --sport 123 -j ACCEPT iptables -A INPUT -i ${INT_IF} -p udp -s 192.168.1.0/24 --dport 123 -j ACCEPT iptables -A OUTPUT -o ${INT_IF} -p udp --sport 123 -j ACCEPT iptables -A INPUT -i ${VPN_VIRT_IF} -p tcp -s 192.168.2.0/24 --dport 123 -j ACCEPT iptables -A OUTPUT -o ${VPN_VIRT_IF} -p tcp --sport 123 -j ACCEPT iptables -A INPUT -i ${VPN_VIRT_IF} -p udp -s 192.168.2.0/24 --dport 123 -j ACCEPT iptables -A OUTPUT -o ${VPN_VIRT_IF} -p udp --sport 123 -j ACCEPT #The next step locks the services so they only work from the LAN: iptables -I INPUT 1 -i ${INT_IF} -j ACCEPT iptables -I INPUT 1 -i ${VPN_VIRT_IF} -j ACCEPT iptables -I INPUT 1 -i lo -j ACCEPT iptables -A INPUT -p UDP --dport bootps ! -i ${INT_IF} -j REJECT iptables -A INPUT -p UDP --dport domain ! -i ${INT_IF} -j REJECT # Drop TCP / UDP packets to privileged ports iptables -A INPUT -p TCP ! -i ${INT_IF} -d 0/0 --dport 0:1023 -j DROP iptables -A INPUT -p UDP ! -i ${INT_IF} -d 0/0 --dport 0:1023 -j DROP # VPN for 192.168.2.0/24 iptables -I FORWARD -i ${INT_IF} -d 192.168.2.0/24 -j DROP iptables -A FORWARD -i ${INT_IF} -s 192.168.2.0/24 -j ACCEPT # Excepted server, that won't be routed into the VPN when on 192.168.2.0/24 # Eg our VOIP server iptables -A FORWARD -i ${WAN} -s <IP OF VOIP SERVER> -j ACCEPT iptables -t nat -A POSTROUTING -d <IP OF VOIP SERVER> -o ${WAN} -j MASQUERADE # A bittorrent port that has been forwarded iptables -t nat -A PREROUTING -p tcp --dport 6881:6889 -i ${WAN_TUNNEL} -j DNAT --to 192.168.1.100 iptables -t nat -A PREROUTING -p tcp --dport 6881:6889 -i ${WAN_TUNNEL} -j DNAT --to 192.168.1.100 # Forward incoming VPN tunnel traffic into into the 'VPN' subnet (aka 192.168.2.0/24) iptables -A FORWARD -i ${WAN_TUNNEL} -d 192.168.2.0/24 -j ACCEPT iptables -t nat -A POSTROUTING -s 192.168.2.0/24 -o ${WAN_TUNNEL} -j MASQUERADE # Forwarding rules for 192.168.1.0/24 to not use the VPN iptables -I FORWARD -i ${INT_IF} -d 192.168.1.0/24 -j DROP iptables -A FORWARD -i ${INT_IF} -s 192.168.1.0/24 -j ACCEPT iptables -A FORWARD -i ${WAN} -d 192.168.1.0/24 -j ACCEPT iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o ${WAN} -j MASQUERADE iptables -P INPUT DROP iptables -P OUTPUT ACCEPT iptables -P FORWARD DROP echo 1 > /proc/sys/net/ipv4/ip_forward for f in /proc/sys/net/ipv4/conf/*/rp_filter ; do echo 1 > $f ; done /etc/init.d/iptables save