Making a minimal VPN routing virtual machine to isolate obnoxious VPN clients

After it was requested multiple times on my post about using pfSense as a Cisco AnyConnect VPN Client, I finally found some time to document the setup I ended up with as a replacement, the purpose of which was to not clutter up the pfSense box. pfSense, after all, acts as the main company router and is integral to the operation of the business network. I don’t want to modify it beyond what some future software update would be able to handle.

The solution to this issue, and several others that have popped up since then, is a set of very small virtual machines. I run each of these, one for each non-pfSense supported client VPN solution (there are quite a few unusual ones out there), with 64 megs of RAM and a mere 3GB of harddrive space.

VM Setup

The virtual machines I use run Debian, as that’s what I’m most familiar with and when setting up things like these, I want them to consume as little of my time as possible. The OpenConnect VPN client is readily available in the Debian package repositories. No mucking about with the apt sources. I also use shorewall to do the routing and firewall rules for me. Thus:

  • Debian (minimal install)
  • OpenConnect (from repositories)
  • Shorewall
  • A cup of coffee

Scripts

The script I use to keep it all going is pretty much copy-pasted from the post about using pfSense to do this, but here it is in its current form:

#!/bin/sh

# settings
user="user_name"
pass="secret_password"
host="vpn.remote.company.com"
test="nc -v -w 10 -z 172.16.30.2 3389"
tmpif="tun69"
iface="tun69"
pidfile="/tmp/${iface}.pid"
script="/usr/share/vpnc-scripts/vpnc-script"

# env
openconnect="/usr/sbin/openconnect"
ifconfig="/sbin/ifconfig"

# func
ifkill()
{
    $ifconfig "$1" down 2>/dev/null || :
    $ifconfig "$1" destroy 2>/dev/null || :
}

# check if we're already running
if [ -n "$test" ] && $test 2>/dev/null; then
    echo "Connection is already up"
    exit 0
fi

# scream
echo "Connection is not up! Attempting restart..." >&2

# clean up previous instance, if any
if [ -e "$pidfile" ]; then
    read pid <"$pidfile"
    echo "Killing previous pid: $pid"
    kill -TERM "$pid"
    rm "$pidfile"
fi
ifkill "$tmpif"
ifkill "$iface"

# open vpn connection
echo "$pass" |\
$openconnect \
    --background \
    --pid-file="$pidfile" \
    --interface="$tmpif" \
    --user="$user" \
    --passwd-on-stdin \
    --script="$script" \
    "$host"

# rename the interface
if [ "$iface" != "$tmpif" ]; then
    echo "Renaming $tmpif to $iface"
    $ifconfig "$tmpif" name "$iface"
fi

I run the above script every minute using the following line in root’s crontab:

* * * * * /root/bin/cron/vpnscript >/dev/null

Shorewall

To do the routing and firewall, there are a few files in /etc/shorewall that are essential for this setup. The first is the interfaces file, in which I define the virtual machine’s regular network interface and the “tun69” interface created by the VPN connection.

# ZONE INTERFACE BROADCAST OPTIONS
net eth0 detect dhcp,logmartians=1,nosmurfs,routefilter,tcpflags,routeback
vpn tun69 detect dhcp,logmartians=1,nosmurfs,routefilter,tcpflags

Then there’s the zones file. This is pretty basic, but matches the interfaces.

# ZONE TYPE
fw firewall
net ipv4
vpn ipv4

For the policy file, we want to allow traffic to flow from the net zone to the vpn zone.

# SOURCE DEST POLICY LOG_LEVEL
$FW vpn ACCEPT
$FW net ACCEPT
net vpn ACCEPT
all all REJECT info

The rules file allows SSH to control the VPN VM from the net zone, which is still in the company’s internal network, mind you.

#ACTION SOURCE DEST PROTO DEST    SOURCE ORIGINAL RATE  USER/ MARK CONNLIMIT TIME HEADERS SWITCH HELPER
#                   PORT  PORT(S)        DEST     LIMIT GROUP
#?SECTION ALL
#?SECTION ESTABLISHED
#?SECTION RELATED
#?SECTION INVALID
#?SECTION UNTRACKED
?SECTION NEW
SSH(ACCEPT+) net $FW

We need a bit of magic to do outbound NAT, with the masq file. This makes the connections the policy file allows to flow from the net zone to the vpn zone masquerade as whatever IP address the VPN client got for its tun69 interface. Without this, the connections would get relayed using their original IP, which the VPN client network probably wouldn’t appreciate. This simply covers all RFC1918 IPv4 addresses, so it doesn’t rely in any way on the rest of the company’s network.

#INTERFACE:DEST SOURCE ADDRESS PROTO PORT(S) IPSEC MARK USER/ SWITCH ORIGINAL
#                                                       GROUP        DEST
tun69           10.0.0.0/8,172.16.0.0/12,192.168.0.0/16

Finally, in shorewall.conf, I change a single line to ensure IP_FORWARDING is enabled

IP_FORWARDING=Yes

pfSense setup

Now we have a VPN routing machine, which in my case is situated on a dedicated VLAN to isolate it from the rest of the company network. You don’t have to do this, but if you don’t, anyone on the internal network can configure a route through the VPN machine and access whatever is on the other end.

In pfSense, I add a static IP for the OpenConnect client, then add a Gateway in System=>Routing=>Gateways. The interface should be the one the OpenConnect client is at, the IP should be the static IP of the client. Then, on the “Static Routes” page, add a route to the network at the end of the VPN and set the gateway to be the one you just created with the OpenConnect client IP. Now it’s up to your rules which machines you want to allow.

Any questions?

If you have any questions about this setup, let me know 🙂

1 Comment

  • Galactus says:

    I love this solution, and have been looking for something similar myself. I’m contracting and currently have something like 7 different VPN clients clogging up my PC. Even if I don’t have more than one running at the time, it still effs up routing, my own LAN and certainly DNS. So I will give this baby a try.

    Have you had any setups with overlapping IP-scopes? Like multiple small customers running stock routers on 192.168.1.1/24…… and there I saw the link at the bottom of the screen to your next article. Thanks ^_^

Leave a Reply

Your email address will not be published. Required fields are marked *