Jump to Navigation

OpenVPN: Routing content for your connection point

Symptoms

You use a VPN to connect your mobile device, for example your laptop, to your server. All traffic should go through this VPN, as you do not trust random networks you may connect to. This works fine, except when you try to connect to your server through its public IP address. As this is also the IP address that is used as the VPN end point, traffic to it does not go through the VPN but is routed, unencrypted, through the untrusted network.

Solution

Even though traffic of the VPN itself (usually UDP port 1194) has to be routed over the internet, all other traffic to the VPN end point can go through the VPN itself. We will mark IP packets destined for the server that are no part of the VPN traffic and route them through the VPN.

On the server side, packets have to be returned through the VPN too. There are basically two ways to do this: add routing rules on the server side or change on the client side the source address of packets sent through the VPN. We will use the latter option, in order to keep all changes on the client side.

Assumptions

  • You are running Linux on the client side (laptop);
  • You use OpenVPN to establish the VPN connection;
  • You use OpenVPN in TAP mode (a layer 2 VPN)
    This also means your client will become part of a local network in which the server also has an IP address.
  • Your default route is through the VPN
    This is usually done with the redirect-gateway option of OpenVPN; using the def1 variant is also good enough.

Theory

To understand how we will implement this, you need to know a little about policy routing, asymetric routing and packet marking in the firewall.

Policy routing

If you use the route command, you will just see the main routing table. It is possible, though, to specify several routing tables and to select between them using routing rules. We will need to use the ip route and ip rule commands instead of plain route.

To see the list of currently defined routing rules, use ip rule or ip rule show. Example output from my system:

0:	from all lookup local 
1234: from all fwmark 0x4d2 lookup 1234
32766: from all lookup main
32767: from all lookup default

Each rule has a number and rules are evaluated in number order. After the lookup keyword the routing table name or number is displayed. The translation from names to numbers is specified in the file /etc/iproute2/rt_tables. Rules 0, 32766 and 32767 are always present; in the example above, rule 1234 has been definied additionally. Rules can call routing tables unconditionally (0, 32766 and 32767 above) or conditionally (1234 above: in this case, only when a firewall mark has been set).

Once a routing table has been selected, the best matching route in that table is used. But if no route matches at all, processing returns to the rule list and the next rule is tried. That is why, even though rule 0 is always unconditionally called, later rules can still apply1.

Routing tables can be inspected using the command ip route show NAME or ip route show table NR,  where NAME is  the name of the table or NR the table number (names are specified in /etc/iproute2/rt_tables). Though the syntax is a bit different, this works the same way as routes defined with route. With route, though, you only see routing table main (table 254).

Marking firewall packets

The Linux firewall can mark packets. This means a number is associated with the packet. The packet itself is not changed; packets can not be marked for use by other computers. It is very useful, though, to mark IP packets that have to be handled in a specific way; it is often easier to determine what packets need that at the start of the packet processing than at the end (especially if packets are changed using NAT2).

Marking of packets is best done in the so-called mangle table. We will use the OUTPUT chain; that is called for packets leaving the system before routing rules are consulted.

Asymetric routing

Asymetric routing means that the response to a packet that is sent to a specific destination is routed differently than the original packet. That is not a big issue - until firewalls are between you and your destination.

Most current firewalls are stateful; that means that they keep track of connections3. All packets that are part of an existing connection are allowed through; the decision whether a connection is allowed to pass through is made when the connection is established.

If you have asymetric routing this tracking of connections fails, either because return packets are going through different firewalls or because they are arriving from unexpected directions and not recognized as part of the connection. Thus, those packets are stopped and will never arrive.

If you have a complicated network, tools like policy routing may be needed to stop asymetric routing.

To illustrate this, let's see what happens if we would do our routing without NATting the source address. We send a packet from our laptop to the public IP-address of our server (which is also the end point of our VPN). The source IP-address will be our laptop's public IP-address. We force the packet through the VPN by policy routing. When a response packet is sent, the destination IP-address will be the source IP-address of our original packet, which is the public IP-address of our laptop. Unless the server is told (by policy routing) that the packet has to go through the VPN, it will be sent unencrypted over the internet and probably never arrive (as the firewall of our laptop will stop it).

There are two solutions here: either also implement policy routing on the server to force return packets through the VPN, or change the source IP-address of our original packet to the laptop's address on the VPN network; that way, the response will automatically go through the VPN.

Implementation

Outgoing packets destined for the public IP-address of the server will be marked for later processing, unless they are destined for the UDP port that is used for the VPN traffic. This is implemented by using the output chain of the iptables mangle table, which is called before routing starts. To keep things simple, we create a separate chain that we jump to:

# Set SERVERIP_PUB to the public IP of your server, the VPN end-point
# Create a new table vpn
iptables -t mangle -N vpn
# Add a rule that stops processing if the packet is part of the VPN traffic itself
iptables -t mangle -A vpn -p udp --dest "$SERVERIP_PUB" --dport 1194 -j RETURN
# Add a rule that marks the packet if its destination is the VPN end point on the server
iptables -t mangle -A vpn --dest "$SERVERIP_PUB" -j MARK --set-mark 1234
# Call this chain from the OUTPUT chain
iptables -t mangle -A OUTPUT -j vpn

Packets thus marked have to be policy routed. We will create a separate routing table that will be called only if a packet is marked. In that case, it has to be routed through the VPN. Note that we use a default route where we could have used a much more specific route.

# Set SERVERIP_VPN to the address of the server on the local VPN segment
# Create a new routing table with one default route through the VPN 
ip route add default via "$SERVERIP_VPN" table 2345
# Call the new table if the packet is marked
ip rule add fwmark 1234 lookup 2345 pref 3456

Finally, we have to source-NAT the marked packets. That is done with iptables, again, in this case in the POSTROUTING chain of the nat table. This chain is executed after routing has been resolved. Again, we use a separate chain to keep things simple:

# Set CLIENTIP_VPN to the address of the client on the local VPN segment
# Create a new table
iptables -t nat -N vpnnat
# If a packet is marked change its source address to the address within the VPN segment
iptables -t nat -A vpnnat -m mark --mark 1234 -j SNAT --to-source "$CLIENTIP_VPN"
# Call this new rule from the POSTROUTING chain in the nat table
iptables -t nat -A POSTROUTING -j vpnnat

Procedure

Below you will find a routing script. You can call it by hand, but it is much easier to call it automatically when the VPN is established or brought down.

Routing script

The script needs a lot of parameters because it is kept as general as possible. It needs to be called when the VPN is established with first parameter up, and after the VPN has been brought down with first parameter down. Other parametres will be shown if you call the script without any arguments.

#!/bin/bash

set -e

if [ "$#" != 9 ]; then
  cat >&2 << EOF
Syntax: $0 ACTION SERVERIP_PUB CLIENTIP_VPN SERVERIP_VPN VPNPORT TABLE RULE MARK IPTABLE
  ACTION: up or down
  SERVERIP_PUB: The public IP of the server, VPN end point
  CLIENTIP_VPN: The IP the client has in the VPN segment
  SERVERIP_VPN: The IP the server has in the VPN segment
  VPNPORT: UDP Port the VPN is running on, 1194 is the default
  TABLE: Number of the routing table (should not be in use)
  RULE: Number of the routing rule (should not be in use)
  MARK: Number of the packet marker (should not be in use)
  IPTABLE: Name of the iptables tables (should not be in use)
EOF
  exit 1
fi

ACTION="$1"
SERVERIP_PUB="$2"
CLIENTIP_VPN="$3"
SERVERIP_VPN="$4"
VPNPORT="$5"
TABLE="$6"
RULE="$7"
MARK="$8"
IPTABLE="$9"


# Always, unconditionally, remove everything; but failure to remove is
# not an error.

iptables -t nat -D POSTROUTING -j "$IPTABLE" 2>/dev/null || true
iptables -t nat -F "$IPTABLE" 2>/dev/null || true
iptables -t nat -X "$IPTABLE" 2>/dev/null || true
iptables -t mangle -D OUTPUT -j "$IPTABLE" 2>/dev/null || true
iptables -t mangle -F "$IPTABLE" 2>/dev/null || true
iptables -t mangle -X "$IPTABLE" 2>/dev/null || true
ip rule del pref "$RULE" 2>/dev/null || true
ip route flush table "$TABLE" 2>/dev/null || true


# If the VPN is started, add the new rules

if [ "$ACTION" = "up" ]; then
  ip route add default via "$SERVERIP_VPN" table "$TABLE"
  ip rule add fwmark "$MARK" lookup "$TABLE" pref "$RULE"

  iptables -t mangle -N "$IPTABLE"
  iptables -t mangle -A "$IPTABLE" -p udp --dest "$SERVERIP_PUB" --dport "$VPNPORT" -j RETURN
  iptables -t mangle -A "$IPTABLE" --dest "$SERVERIP_PUB" -j MARK --set-mark "$MARK"
  iptables -t mangle -A OUTPUT -j "$IPTABLE"

  iptables -t nat -N "$IPTABLE"
  iptables -t nat -A "$IPTABLE" -m mark --mark "$MARK" -j SNAT --to-source "$CLIENTIP_VPN"
  iptables -t nat -A POSTROUTING -j "$IPTABLE"
fi

ip route flush cache

Tying it to the VPN

In your VPN configuration file, add the following lines:

# Allow calling  user-defined scripts
script-security 2
# If you already have up and down scripts remember their names
up /etc/openvpn/vpnscript
down /etc/openvpn/vpnscript

Now create an executable file /etc/openvpn/vpnscript with the following contents:

#!/bin/bash

# If you already had an up/down script add calls here, for example:
##/etc/openvpn/old-script "$@"

# The first five arguments use environment variables that are set by openvpn.
# The last four arguments can be tweaked; the values here are more or less random.
/etc/openvpn/vpn-routing-conf "$script_type" "$trusted_ip" "$ifconfig_local" "$route_vpn_gateway" "$trusted_port" 1234 1234 1234 vpn
  • 1. It was quite hard to find this information, and I have found this out more or less by experimentation.
  • 2. Network Address Translation, changing the source of destination address of a packet and/or the source or destination port.
  • 3. Of course, only TCP creates real connections; UDP is connectionless. But firewalls usually treat responses on UDP packets as part of an "UDP connection" that is tracked and handled like a real TCP connection.


Technical_article | by Dr. Radut