Configuring Debian GNU/Linux as an OpenVPN router

This Howto describes the setup of a Debian GNU/Linux OpenVPN router. It uses the same mechanism for automated firewall updates as described in the Linux firewall Howto and extends it to support NAT and port forwarding. Hosts on the LAN network will only be able to access the Internet when the VPN connection is active, otherwise the firewall will not load NAT rules – even packet forwarding on kernel level is disabled in this case.

Requirements

Install Debian GNU/Linux with the SSH Server and Standard system utilities software collections. Then follow the guide to setup an IPredator OpenVPN connection on your router. Make sure to understand all concepts lined out in the Linux firewall Howto (e.g. dynamic firewall updates via udev actions, ulogd packet logging).

Needed files

Installation

Packages

To use the ferm frontend for iptables with proper logging support you need the install the ferm and ulogd packages.

# apt-get install ferm ulogd ulogd-pcap

To hand out IP addresses to clients sitting behind the router you also need to install a DHCP server:

# apt-get install isc-dhcp-server

Since this is a router only, it should not run NFS daemons. Deinstall everything NFS related by executing the following commands:

# apt-get --purge remove nfs-common rpcbind

Firewall automation via udev

Copy fermreload.sh to /usr/local/bin/fermreload.sh and make it executable:

# chmod 555 /usr/local/bin/fermreload.sh

Next place 81-vpn-firewall.rules into /etc/udev/rules.d and restart udev:

# /etc/init.d/udev restart

Enabling the ulogd logging daemon

Edit /etc/ulogd.conf and activate the PCAP plugin. Change the configuration for the log emulator to write to /dev/null to dismiss messages to this log frontend. Log dropped packets to a proper PCAP file instead.

..

plugin="/usr/lib/ulogd/ulogd_PCAP.so"

..

[LOGEMU]
#file="/var/log/ulog/syslogemu.log"
file="/dev/null"

..

[PCAP]
file="/var/log/ulog/pcap.log"
sync=1

Start ulogd via its initscript:

# /etc/init.d/ulogd start

Logs are written to /var/log/ulog/pcap.log. Use the tail command and pipe its output to tcpdump to view the live log of dropped packets:

tail -F /var/log/ulog/ulogd.log | tcpdump -nr -

Configure network interfaces

The router has two network interfaces, eth0 and eth1. eth0 is used to connect to a modem or another ISP router, eth1 faces the client network. While eth0 gets its IP assign dynamically through DHCP, eth1 is statically configured to 192.168.2.1/24. Edit /etc/network/interfaces to reflect this setup:

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth0
iface eth0 inet dhcp

auto eth1
iface eth1 inet static
	address 192.168.2.1
	netmask 255.255.255.0

DHCP Client

Your router acts as a DHCP client on eth0. Always use an IPredator DNS server to resolve hostnames on the router. Supersede the DHCP option for domain name servers in /etc/dhcp/dhclient.conf:

# echo "supersede domain-name-servers 46.246.46.246, 194.132.32.32;" >> /etc/dhcp/dhclient.conf

Packet forwarding

Packet forwarding is triggered in fermreload.sh based on udev interface add and remove actions. fermreload.sh got slightly extended in comparison to what you know from the Linux firewall Howto. Note the use of sysctl to alter net.ipv4.ip_forward.

#!/bin/bash
#
# fermreload.sh
# VER 0.1
#
# Reloads the ferm firewall ruleset and is invoked by
# the udev via /etc/udev/rules.d/81-vpn-firewall.rules.
#
# IPredator 2014
# Released under the Kopimi license.
#
# Blog post:  https://blog.ipredator.se/configuring-debian-gnu-linux-as-an-openvpn-router.html
#

LOGGER=/usr/bin/logger
LOGGER_TAG=$0

UDEV_ACTION=$1

FERM=/usr/sbin/ferm
FERM_CONF=/etc/ferm/ferm.conf

MSG_FW_RULE_ADD="Adding VPN firewall rules."
MSG_FW_RULE_REMOVE="Removing VPN firewall rules."
MSG_UDEV_ACTION_UNKNOWN="Unknown udev action."

case "$UDEV_ACTION" in
    add)
        $LOGGER -t $LOGGER_TAG $MSG_FW_RULE_ADD
        $FERM $FERM_CONF
        sysctl net.ipv4.ip_forward=1
        ;;
    remove)
        $LOGGER -t $LOGGER_TAG $MSG_FW_RULE_REMOVE
        sysctl net.ipv4.ip_forward=0
        $FERM $FERM_CONF
        ;;
    *)
        $LOGGER -t $LOGGER_TAG $MSG_UDEV_ACTION_UNKNOWN
        exit 1
esac

In a regular router setup net.ipv4.ip_forward is configured in /etc/sysctl.conf to enable packet forwarding on the system permanently. In this setup the sysctl gets toggled on demand because the main focus of this Howto is to prevent machines behind the router from communicating without a VPN connection being active. Combined with the removal of all NAT rules you can effectively and immediately suppress any outbound traffic from clients.

Firewall

For reference the complete ferm ruleset is listed here. The relevant details in regard to a NAT configuration are described in the next sections of this document.

# -*- shell-script -*-
#
# Configuration file for ferm(1).
#
# V: 0.1
#
# ferm manual: http://ferm.foo-projects.org/download/2.2/ferm.html
# Blog post:  https://blog.ipredator.se/configuring-debian-gnu-linux-as-an-openvpn-router.html
#

# Really make sure that these modules exist and are loaded.
@hook pre "/sbin/modprobe nf_conntrack_ftp";
@hook pre "/sbin/modprobe nfnetlink_log";

# Network interfaces.
@def $DEV_EXT = eth0;
@def $DEV_LAN = eth1;
@def $DEV_LOOPBACK = lo0;
@def $DEV_VPN = tun0;

# Network definition for the LAN interface. This is the network range
# your NAT clients sit in.
@def $NET_LAN = 192.168.2.0/24;

# Hosts
@def $HOST_TRANSMISSION = 192.168.2.64;

# Common application ports.
@def $PORT_DNS = 53;
@def $PORT_FTP = ( 20 21 );
@def $PORT_NTP = 123;
@def $PORT_SSH = 22;
@def $PORT_WEB = ( 80 443 );

# Transmission ports.
@def $PORT_TRANSMISSION = 16384:65535;

# The ports we allow OpenVPN to connect to. IPredator allows you
# to connect on _any_ port. Simply add more ports if desired but
# stick to only those that you really need.
@def $PORT_OPENVPN = (1194 1234 1337 2342 5060);

# Public DNS servers and those that are only reachable via VPN.
# DNS servers are specified in the outbound DNS rules to prevent DNS leaks
# (https://www.dnsleaktest.com/). The public DNS servers configured on your
# system should be the IPredator ones (https://www.ipredator.se/page/services#service_dns),
# but you need to verify this.
# The fail-safe default is to use any public DNS (0.0.0.0). 
# @def $IP_DNS_PUBLIC = ( 46.246.46.246 194.132.32.32 );
@def $IP_DNS_PUBLIC = 0.0.0.0;
@def $IP_DNS_VPN = ( 46.246.46.46 194.132.32.23 );

# Function to simplify port forwarding
@def &FORWARD_PORT($DEV_SRC, $DEV_DEST, $PROTO, $IP_DEST, $PORT) = {
    table filter chain FORWARD interface $DEV_SRC outerface $DEV_DEST daddr $IP_DEST \
        proto $PROTO dport $PORT ACCEPT;
    table nat chain PREROUTING interface $DEV_SRC proto $PROTO dport $PORT DNAT to $IP_DEST;
}

# Make sure to use the proper VPN interface (e.g. tun0 in this case).
# Note: You cannot reference $DEV_VPN here, substition does not take
#       place for commands passed to a sub shell.
@def $VPN_ACTIVE = `ip link show tun0 >/dev/null 2>/dev/null && echo 1 || echo`;

# VPN interface conditional. If true the following rules are loaded.
@if $VPN_ACTIVE {
    domain ip {

        # Forward port range for Transmission
        &FORWARD_PORT($DEV_VPN, $DEV_LAN, (tcp udp), $HOST_TRANSMISSION, $PORT_TRANSMISSION);

        table filter {
            chain OUTPUT {
                # Default allowed outbound services on the VPN interface.
                # If you need more simply add your rules here.
                outerface $DEV_VPN {
                    proto (tcp udp) daddr ($IP_DNS_PUBLIC $IP_DNS_VPN) dport $PORT_DNS ACCEPT;
                    proto tcp dport $PORT_FTP ACCEPT;
                    proto udp dport $PORT_NTP ACCEPT;
                    proto tcp dport $PORT_SSH ACCEPT;
                    proto tcp dport $PORT_WEB ACCEPT;
                }
            }
            chain FORWARD {
                # Connection tracking.
                mod state state INVALID DROP;
                mod state state (ESTABLISHED RELATED) ACCEPT;

                # Forward packets on the LAN interface.
                interface $DEV_LAN ACCEPT;
            }
        }
        table nat {
            chain POSTROUTING {
                # NAT all packets from the LAN to the VPN interface.
                saddr $NET_LAN outerface $DEV_VPN MASQUERADE;
            }
        }
    }
}


# The main IPv4 rule set.
domain ip {
    table filter {
        chain INPUT {
            # The default policy for the chain. Usually ACCEPT or DROP or REJECT.
            policy DROP;

            # Connection tracking.
            mod state state INVALID DROP;
            mod state state (ESTABLISHED RELATED) ACCEPT;

            # Allow local traffic to loopback interface.
            interface $DEV_LOOPBACK ACCEPT;
 
            # Allow all traffic on the LAN interface.
            interface $DEV_LAN ACCEPT;

            # Allow inbound SSH on your EXT interface.
            interface $DEV_EXT {
                proto tcp dport $PORT_SSH ACCEPT;
            }

            # Respond to ping ... makes debugging easier.
            proto icmp icmp-type echo-request ACCEPT;

            # Log dropped packets.
            ULOG ulog-nlgroup 1;
            DROP;
        }

        chain OUTPUT {
            policy DROP;

            # Connection tracking.
            mod state state INVALID DROP;
            mod state state (ESTABLISHED RELATED) ACCEPT;

            # Allow local traffic from the loopback interface.
            outerface $DEV_LOOPBACK ACCEPT;
  
            # Allow traffic to your local network.
            outerface $DEV_LAN ACCEPT;

            # Respond to ping.
            proto icmp icmp-type echo-request ACCEPT;

            # Allowed services on the EXT interface.
            outerface $DEV_EXT {
                proto (tcp udp) daddr $IP_DNS_PUBLIC dport $PORT_DNS ACCEPT;
                proto udp dport $PORT_NTP ACCEPT;
                proto (tcp udp) dport $PORT_OPENVPN ACCEPT;
                proto tcp dport $PORT_SSH ACCEPT;
            }

            # Log dropped packets.
            ULOG ulog-nlgroup 1;
            DROP;
        }

        chain FORWARD {
            policy DROP;

            # If you use your machine to route traffic eg. 
            # from a VM you have to add rules here!

            # Log dropped packets.
            ULOG ulog-nlgroup 1;
            DROP;
        }
    }
}

# IPv6 is generally disabled, communication on the loopback device is allowed.
domain ip6 {
    table filter {
        chain INPUT {
            policy DROP;

            # Allow local traffic.
            interface $DEV_LOOPBACK ACCEPT;

            DROP;
        }
        chain OUTPUT {
            policy DROP;

            DROP;
        }
        chain FORWARD {
            policy DROP;

            DROP;
        }
    }
}

Variables

To make the firewall config more readable and adaptable you should use proper variable names for the external, LAN, loopback and VPN interfaces. The IP range of the LAN network should also be defined in a variable.

# Network interfaces.
@def $DEV_EXT = eth0;
@def $DEV_LAN = eth1;
@def $DEV_LOOPBACK = lo0;
@def $DEV_VPN = tun0;

# Network definition for the LAN interface. This is the network range
# your NAT clients sit in.
@def $NET_LAN = 192.168.2.0/24;

Port forwarding

In this example the Transmission BitTorrent client is running on HOST_TRANSMISSION. It needs a certain port range to be forwarded to exchange data with other BitTorrent clients.

# Hosts
@def $HOST_TRANSMISSION = 192.168.2.64;

# Transmission ports.
@def $PORT_TRANSMISSION = 16384:65535;

The &FORWARD_PORT function cares for the actual port forwarding. It takes the source device, destination device, protocol, destination IP and the destination port as parameters and inserts the needed rules in the FORWARD filter and the PREROUTING NAT chain.

Dealing with two iptables chains for such a simple matter as forwarding a single port or a port range is comparably insane. ferm 4TW! The readability of the &FORWARD_PORT function body is a shining example why port forwarding could be considered one of the most unlit parts in the iptables syntax darkroom ™:

# Function to simplify port forwarding
@def &FORWARD_PORT($DEV_SRC, $DEV_DEST, $PROTO, $IP_DEST, $PORT) = {
    table filter chain FORWARD interface $DEV_SRC outerface $DEV_DEST daddr $IP_DEST \
        proto $PROTO dport $PORT ACCEPT;
    table nat chain PREROUTING interface $DEV_SRC proto $PROTO dport $PORT DNAT to $IP_DEST;
}

VPN activation an NAT

Compared to the VPN-related ruleset from the Linux firewall Howto the ruleset for a router needs to get extended:

  • The FORWARD chain processes packets that get forwarded through the router, thus packets on $DEV_LAN need to be accepted.
  • In the POSTROUTING chain outbound packets get rewritten to originate from the VPN IP, this process is called masquerading in the Linux world.

The combination of packet forwarding and masquerading is called NAT.

Note the use of the &FORWARD_PORT function in the following configuration sippet to forward ports to a host running Transmission. By embedding the function call inside the $VPN_ACTIVE conditional, the forwarded ports are only open as long as the VPN connection is active.

@if $VPN_ACTIVE {
    domain ip {

        # Forward port range for Transmission
        &FORWARD_PORT($DEV_VPN, $DEV_LAN, (tcp udp), $HOST_TRANSMISSION, $PORT_TRANSMISSION);

        table filter {

             ..

             chain FORWARD {
                # Connection tracking.
                mod state state INVALID DROP;
                mod state state (ESTABLISHED RELATED) ACCEPT;

                # Forward packets on the LAN interface.
                interface $DEV_LAN ACCEPT;
            }
        }
        table nat {
            chain POSTROUTING {
                # NAT all packets from the LAN to the VPN interface.
                saddr $NET_LAN outerface $DEV_VPN MASQUERADE;
            }
        }
    }
}

INPUT and OUTPUT chains

The INPUT and OUTPUT chains provide the same level of host connectivity known from the Linux firewall Howto. Only VPN connections can be established, further it is possible to access the router via SSH on external and on the LAN interface.

System start-up

To enable the ferm init script just edit /etc/default/ferm and set

ENABLED="yes"

The udev and ulogd daemons get started by default.

Setting up the DHCP server

To supply clients on the private network behind the router with IP addresses, you need to run a DHCP server serving requests on eth1. Edit /etc/dhcp/dhcpd.conf and enter the following subnet defintion:

default-lease-time 600;
max-lease-time 7200;

subnet 192.168.2.0 netmask 255.255.255.0 {
    range 192.168.2.64 192.168.2.127;

    option subnet-mask 255.255.255.0;
    option broadcast-address 192.168.2.255;
    option routers 192.168.2.1;
    option domain-name-servers 46.246.46.46, 194.132.32.23;
}
 

Note that the clients get a different DNS server assigned than what you have configured on the router due to superseding in /etc/dhcp/dhclient.conf. The DNS server the clients use is only available through the VPN tunnel. Alternatively you could install a local resolver on the router, e.g. unbound.

Set the parameter INTERFACES to eth1 in /etc/default/isc-dhcp-server to serve DHCP requests on that interface:

INTERFACES="eth1"

Connect a client to eth1 and try to receive an IP address via DHCP.

Final words

To verify proper firewall rule insertion and removal, watch /var/log/syslog. fermreload.sh inserts messages there when it gets invoked. Use iptables -nvL and iptables -nvL -t nat to dump regular and NAT firewall rules.

If you experience any problems after following this Howto, please contact support@ipredator.se. For error corrections or feedback please write an email to feedback@ipredator.se. Of course we are also available via our Online Chat.