Routing Traffic Through a VPN Connection

If the system on which PPTP or OpenVPN is running is a router, and you only want to route packets from it or from certain machines connected to it or perhaps only certain types of traffic (e.g. all mail traffic outbound to SMTP on port 25) through the VPN tunnel, you can use iproute2 to accomplish this. This method works, regardless of what type of VPN tunnel you are using but, if you are using OpenVPN, you must use the "--route-noexec" parameter when you bring up the tunnel to prevent OpenVPN from setting up the routes itself.

The first step, which is the same regardless of which types of packets you wish to route through the VPN tunnel, consists of adding a special table for the VPN tunnel to the routing tables. Use your favorite editor to update or add:

/etc/iproute2/rt_tables:

     #
     # reserved values
     #
     255     local
     254     main
     253     default
     0       unspec
     #
     # local
     #
     #1      inr.ruhep
     200     vpn.tunnel

Then, add a default gateway to the "special" table so that any packets that are routed through it will actually be sent down the VPN tunnel. For PPTP, this will look something like this:

     su
     /sbin/ip route add default via 195.12.34.56 dev ppp2 table vpn.tunnel
     /sbin/ip route flush cache

Or, for OpenVPN, this will look something like this:

     su
     /sbin/ip route add default via 10.1.1.11 dev tun0 table vpn.tunnel
     /sbin/ip route flush cache

You should be able to observe that this default routing is in effect as follows:

     /sbin/ip route list table vpn.tunnel

Which should produce a result like this (for PPTP):

     default via 195.12.34.56 dev ppp2

or like this (for OpenVPN):

     default via 10.1.1.11 dev tun0

Now that you have a "special" routing table that can be used to route only the packets that you select for transport through the VPN tunnel, one method that you can use to choose those packets is to tell iproute2 that all packets from certain machines should be routed through that table. In this case, you will be routing packets based on their source IP address. By way of example, we'll assume that you want to route all packets from a single machine that is connected to the router:

     su
     /sbin/ip rule add from 192.168.11.4 table vpn.tunnel

You can verify that the special table is in effect as follows:

     /sbin/ip rule ls

You should see something like this:

     0:      from all lookup local
     32765:  from 192.168.11.4 lookup vpn.tunnel
     32766:  from all lookup main
     32767:  from all lookup default

Note that, in the above examples, we assumed that, when the VPN tunnel came up, the interface" assigned to it was either "ppp2" or "tun0" and that the corresponding IP address of the tunnel was either "195.12.34.56" or "10.1.1.11", depending on which type of tunnel was started. Naturally, whenever bring up a VPN tunnel the actual device names and IP address will be assigned dynamically. While you can certainly add and remove the default route and routing rule or rules by hand, whenever you bring up a VPN tunnel, the vpn-updown script (shown in either the "Starting/Stopping a PPTP Tunnel and Routing Traffic Through It" or "Starting/Stopping an OpenVPN Tunnel and Routing Traffic Through It" sections) can be used to add and remove such rules and the default route automatically.

If, as above, the system on which PPTP or OpenVPN is running is a router but, instead of routing all traffic from certain machines, you only want to route certain types of traffic passing through it (e.g. all SMTP traffic), you can still use iproute2 to accomplish this. In fact, we prefer to use this approach to route traffic through the VPN tunnel even when the routing selection is based solely on source IP address and could be accomplished by a routing rule, as shown above, because this method is much more flexible, in our opinion.

Plus, it has one extra advantage that using a routing rule does not. This technique allows packets that are supposed to be routed through the VPN tunnel to be dumped instead of being delivered through some other route or specifically routed somewhere else, if and when the VPN tunnel is down. This feature is especially important if, for example, you are using the VPN tunnel to obscure the source of your Internet traffic by routing it to a remote location. You certainly don't want packets being delivered through your regular Internet connection, if your VPN tunnel disconnects.

As above, you should begin by adding a special table for the VPN tunnel to the routing tables, with your favorite editor. If you won't be simultaneously employing the previous approach and this approach, you can reuse the same table name as above, otherwise you should pick a different name, as we've shown in this example:

/etc/iproute2/rt_tables:

     #
     # reserved values
     #
     255     local
     254     main
     253     default
     0       unspec
     #
     # local
     #
     #1      inr.ruhep
     200     vpn.tunnel
     201     vpn.marked

Now that you again have your "special" routing table, you can use netfilter to mark all of the packets that should be routed through the VPN tunnel so that they can be properly routed, based on their marks, by iproute2. Let's assume, you're looking to route most of the traffic from a particular machine through the tunnel but, as an added wrinkle, you want traffic bound for a couple of specific IP addresses (in this case your DNS servers) to be left alone. Add some iptables rules like this:

     su
     /sbin/iptables -N VPN_TUNNEL -t mangle
     # Allow our DNS servers.
     /sbin/iptables -A VPN_TUNNEL -t mangle -d 151.203.0.84 -j RETURN
     /sbin/iptables -A VPN_TUNNEL -t mangle -d 204.122.16.8 -j RETURN
     /sbin/iptables -A VPN_TUNNEL -t mangle -d 216.231.41.2 -j RETURN
     # Mark the packet for tunneling.
     /sbin/iptables -A VPN_TUNNEL -t mangle -j MARK --set-mark 1
     # Add each of the systems that are to be routed through the VPN tunnel.
     /sbin/iptables -A PREROUTING -t mangle -i eth0 -s 192.168.1.84 -j VPN_TUNNEL
     /sbin/iptables -A PREROUTING -t mangle -i eth1 -s 192.168.11.4 -j VPN_TUNNEL

Once you have the rules set as you wish, you can save them with:

     su
     iptables-save >mytables.sav

If you need to restore the configuration, you can do so with:

     su
     iptables-restore <mytables.sav

Marking packets using other criteria is also possible. For example, if you wanted to route all of your incoming SMTP traffic through the VPN tunnel to an email server at a secure/remote location, you can mark the packets like this:

     su
     /sbin/iptables -A PREROUTING -t mangle -i eth0 -p tcp --dport 25 \
         -j VPN_TUNNEL
     /sbin/iptables -A PREROUTING -t mangle -i eth1 -p tcp --dport 25 \
         -j VPN_TUNNEL

Now that you have marking rules in place, you can cause all of the marked packets to be routed through your "special" routing table, using the mark that iptables (known to ip as the firewall, hence the tag "fwmark") adds to each packet. For PPTP, you should do something like this:

     su
     /sbin/ip route add default via 195.12.34.56 dev ppp2 table vpn.marked
     /sbin/ip route flush cache
     /sbin/ip rule add fwmark 1 table vpn.marked

Or, if you have an OpenVPN tunnel, you might do something like this:

     su
     /sbin/ip route add default via 10.1.1.11 dev tun0 table vpn.marked
     /sbin/ip route flush cache
     /sbin/ip rule add fwmark 1 table vpn.marked

You could even get creative by setting up multiple VPN tunnels, multiple "special" routing tables and default routes, and then marking different types of packets with different firewall marks. This would route one type of traffic down one VPN tunnel and another type of traffic down another VPN tunnel. Complicated network topologies and routes are quite possible.

If you are using NARC as your firewall, you can add rules, such as those shown above, to your narc-custom.conf file. The following lines will mark any packets from the machines whose traffic is to be routed through the VPN tunnel. As discussed above, if the packets aren't routed, they will be dumped:

/etc/narc/narc-custom.conf:

.

       .

#
# Prerouting rule to tag certain kinds of packets from certain machines # in the DMZ with a flag that can be used by iproute2 to route them via # a VPN tunnel to a remote system.
#
# First, the rule looks for all packets that aren't to be routed through # the tunnel. Typically, these are packets that use protocols that # shouldn't be routed (e.g. DNS lookups or those that are already being # tunneled somewhere else) or packets bound for systems known to be OK. # For all such packets, the rule simply returns. #
# If the packets are still of interest, all those from the machines to # be rerouted are marked with a flag of 1 to tell iproute2 to reroute # them through the tunnel.
#
$IPTABLES -N VPN_TUNNEL -t mangle
#
# Allow our DNS servers.
#
# Note that this can be dangerous if your intention is to hide who you # are, to the outside world, by virtue of tunneling all of your packet # traffic off to some remote VPN service. If the system that is supposed # to be hidden inadvertently asks for an address from one of these DNS # servers and then starts accessing that address through the VPN, a clever # observer could put two and two together and figure out the real IP # address of the hidden system.
#
# Therefore, make sure that all systems that are supposed to be hidden do # not access any DNS servers except those that are approved for use with # the VPN provier, or disable this feature herein. Otherwise, you may # experience a DNS leak that could expose your hidden system to the world. #
$IPTABLES -A VPN_TUNNEL -t mangle -d 151.203.0.84 -j RETURN $IPTABLES -A VPN_TUNNEL -t mangle -d 151.203.0.85 -j RETURN $IPTABLES -A VPN_TUNNEL -t mangle -d 204.122.16.8 -j RETURN $IPTABLES -A VPN_TUNNEL -t mangle -d 216.231.41.2 -j RETURN #
# Allow other VPN tunnels that are directly connected. #
$IPTABLES -A VPN_TUNNEL -t mangle -d 123.45.67.89 -j RETURN $IPTABLES -A VPN_TUNNEL -t mangle -d 123.45.67.98 -j RETURN #
# Since all packets from certain machines are sent here, just in case # locally-bound packets from those machines somehow get past this router, # don't tunnel any packets that are bound for our local network segments. #
$IPTABLES -A VPN_TUNNEL -t mangle -d 192.168.1.0/24 -j RETURN $IPTABLES -A VPN_TUNNEL -t mangle -d 192.168.11.0/24 -j RETURN

     # Mark the packet for tunneling.
     $IPTABLES -A VPN_TUNNEL -t mangle -j MARK --set-mark 1
     #
     # Add a rule for each of the systems that are to be routed through the
     # VPN tunnel.  This will send all of those systems' packets through the
     # VPN prerouting chain of the mangle table.
     #
     # Note that you should not route packet traffic through the VPN tunnel
     # for any systems that are supposed to be hidden, if they have any
     # external ports open to the outside network (either through port
     # forwarding or through a direct connection, such as this system itself
     # has).  Pay particular attention that you don't route any packet traffic
     # for this system itself through the VPN connection, if it is routing
     # packets for any attached hidden system.
     #
     # If you do this, there is a vulnerability that can allow an outside
     # observer to determine the actual IP address of the hidden system by
     # probing its external ports with UDP packets that will be answered
     # through the VPN tunnel.  See the full description of the problem in the
     # /etc/openvpn/tun-updown script.
     #
     $IPTABLES -A PREROUTING -t mangle -i eth0 -s 192.168.1.84 -j VPN_TUNNEL
     $IPTABLES -A PREROUTING -t mangle -i eth1 -s 192.168.11.4 -j VPN_TUNNEL
     #
     # If you wish, you can send a whole range of systems through the VPN
     # tunnel.  For example, this rule routes all of the hosts that are
     # assigned IP addresses by DHCP, on a DMZ segment, through the VPN tunnel
     # by checking for any source address in the range used by DHCP.
     #
     $IPTABLES -A PREROUTING -t mangle -i eth1 -m iprange \
         --src-range 192.168.11.150-192.168.11.200 -j VPN_TUNNEL
     #
     # If the VPN tunnel is ever down, we don't want packets marked for the
     # tunnel to go anywhere since, presumably, they were being tunneled for
     # a good reason.  If we ever get as far as the forward queue (i.e. the
     # packets are just about to be delivered) and we find a marked packet
     # bound for the external interface, we should reject it.
     #
     # Normally, marked packets shouldn't make it this far, bound for the
     # external interface, because iproute2 will take note of their marking
     # and reroute them to the VPN tunnel (when it is up).
     #
     # Note that all attempts to deliver VPN packets to the external interface
     # are logged with a prefix of VPN.
     #
     $IPTABLES -N VPN_REJECT
     $IPTABLES -A VPN_REJECT -j LOG --log-level $NORM_LOG_LEVEL \
         --log-tcp-options --log-ip-options --log-prefix "VPN_REJECT "
     $IPTABLES -A VPN_REJECT -j VPN_REJECT
     # Hook the rule in to the forward chain.
     $IPTABLES -I FORWARD -m mark --mark 1 -o $EXTERNAL_INTERFACE -j VPN_REJECT
          .
          .
          .

Note that in the above example, the changes that we've made to narc-custom.conf allow other VPN tunnels that are bound directly from the machines we're checking to pass directly through to outside VPN servers without being routed through a second tunnel (i.e. the one we're setting up).

If you use this feature, you'll note that the rules that check for other, directly connected VPN tunnels use specific IP addresses, not symbolic host names. This is because using symbolic names in an iptables rule could cause problems (consider what would happen to the packets bound for a DNS server, requesting the lookup of a name, as they traversed the iptables filters -- can you say "recursion"). However, in most cases, VPN connections are usually established using a symbolic name. But, the IP address that the symbolic name maps to could change (after you used reverse DNS to look up its mapped IP address and set it in your NARC custom configuration).

This being the case, if you want to punch holes through for directly connected VPN tunnels, you might want to create a script that checks to see if the VPN servers are all where you think they should be and send you email if anything changes. The script can be run at regular intervals as a cron job:

/etc/ppp/vpncheck or /etc/openvpn/vpncheck:

     #!/bin/sh
     #
     # Check that the VPN tunnel servers still have the same IP addresses as
     # expected.  The reason we do this is because the rules used by iptables to
     # allow unhindered routing to the VPN tunnel servers should not use
     # symbolic machine names (think about doing a remote DNS lookup in the
     # component that is routing all of the system's packets -- potentially not
     # a good situation).  Instead, the rules use exact IP addresses so, if an
     # IP address changes, unhindered routing to one or more of the VPN tunnel
     # servers can fail silently.
     #
     # If any IP doesn't match what is expected, send email to root.
     #
     # Note that this script may be passed an argument of "debug" to cause
     # debugging information to be dumped to stdout.  Normally, it runs silently
     # and sends email to root if an error occurs.
     #
     # Note that this script uses the SERVERROLE setting in
     # /etc/sysconfig/clustering to determine which server it is running on.  If
     # it determines that it is running on a secondary server, it does nothing.
     # If no setting is found, the default is to act as a standalone server.
     #
     ############################################################################
     #
     # The list of VPN tunnel servers and their IP addresses (see
     # /etc/narc/narc-custom.conf).
     #
     VPNServs=("vpn1.mydomain.com" "vpn2.mydomain.com")
     VPNAddrs=(  "123.45.67.89"      "123.45.67.98"   )
     #
     # Debugging flag.
     #
     DebugFl="no"
     ############################################################################
     #
     # Check if a VPN server's DNS entry maps its expected IP address.
     #
     IsMappedOK()
     {
     #
     # Get the IP address that the server name maps to.  See if it is mapped OK.
     # We need to check this because the narc-custom.conf file must use real IP
     # addresses, not DNS addresses.  However, some VPN servers are subject to
     # remapping, so we need to check that they are where we think they are.
     #
     # Note that piping the result of host through sed twice handles the case
     # where the output of host looks like:
     #
     #      vpn.myhost.com is an alias for myhost.com.
     #      myhost.com has address 71.183.239.131
     #
     # Sometimes, the DNS address for the VPN server is aliased and can change
     # so we want to be able to look up the alias herein to ensure that the
     # actual VPN server is at the address we expected, not what its aliased to.
     #
     ServMapped=`host $1 | sed "s/^. \([0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\)/\1/"`
     ServMapped=`echo $ServMapped | sed "s/^. \([0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\)/\1/"`
     if test $3 == "yes"; then
         echo Server $1, expected $2, got $ServMapped
     fi
     if test x"$ServMapped" == x"$2"; then
         return 0  #Success
     else
         return 1  #Failure
     fi
     }
     ############################################################################
     #
     # Source the clustering configuration.
     #
     if [ -f /etc/sysconfig/clustering ] ; then
         . /etc/sysconfig/clustering
     else
         SERVERROLE=Standalone
     fi
     #
     # If this is the Secondary server, we're all done.
     #
     if [ x"$SERVERROLE" == xSecondary ]; then
         exit 0
     fi
     #
     # See if we're debugging.
     #
     if test x"$1" == x"debug"; then
         DebugFl="yes"
     fi
     #
     # Loop through all the VPN servers and check them all.
     #
     Curr=0
     ServFail=0
     FailMsg=""
     for ServName in ${VPNServs[*]}; do
         #
         # Check if the server is mapped OK.
         #
         if (! IsMappedOK $ServName ${VPNAddrs[$Curr]} $DebugFl); then
             let ServFail+=1
             FailMsg="${FailMsg}Server $ServName is not mapped to expected address ${VPNAddrs[$Curr]}"$'\n'
         fi
      let Curr+=1

done
#
# See how everything went.
#
if ( test $ServFail -gt 0 ); then

      if test $DebugFl == "yes"; then
          echo $FailMsg
      else
          /bin/mail -s "Problem with VPN tunneling" root <<-ENDMSG
          There seems to be a problem with VPN tunneling.  Here's a synopsis:
          $FailMsg
          Perhaps you should check /etc/narc/narc-custom.conf
          ENDMSG
      fi

fi