On-demand VPN

4 minute read

For about a month now I’ve been travelling abroad and been surviving on dodgy public Wi-Fis and networks operated by my Airbnb hosts. Under such circumstances I prefer tunneling all my traffic through VPN to avoid eavesdropping.

For a long time I’ve had an always-on beefy VPS with OpenVPN running so this wasn’t an issue. However, a while back I’ve decided to save some money by scaling it down to a very small instance and running bigger workloads (VPN included) on ephemeral instances.

In this post I’ll describe how I ended up with an on-demand personal VPN controlled by a Discord bot.

The experience

Whenever I need a VPN connection I have to hop on Discord and message a bot running on my Discord server:

It immediately acknowledges the request with the working on it response and then replies within a few minutes with VPN started once the VPN is ready. It even adds a little check mark to your request, lovely!

I can also check the current status and list connected users via Discord:

Output is a little cryptic but it has the IP address of the connected hosts which is usually what I’m interested in.

How to shut down the VPN once I’m done with it? If you’ve guessed I need to message vpn-stop, you’re smart but wrong. Do you really believe I would remember to do that? The instance is paid by the minute so it is terminated once no one is using it! (Do you think I remember to disconnect from the VPN once I don’t need it anymore? 🤔)

Why is it called mc-bot?

This bot started off as a way to spin up/down my Minecraft server, but then VPN proved to be more important so I’ve implemented that first. 🙂

How does it work?

When you message the bot, Discord will notify a Go service backing the bot running on the always-on server. This service is capable of spinning up a new Scaleway compute instance to launch the VPN and then ask for its status later.

A cron job is configured to run every minute to check if there are users connected to the VPN. If no one used the VPN for 5 minutes, it will terminate the instance.

Now let’s see these components in a little more detail.

The Discord bot

The Discord bot is backed by a Go service that’s running on the always-on instance. You can find its source code here.

It relies on a 3rd party Go library (github.com/bwmarrin/discordgo) to handle the connection to the Discord servers. It receives every request on every channel where the bot is added and also private messages. On seeing certain keywords associated with its commands, it executes custom functions. For most commands it executes shell scripts where the infrastrucure operations are handled. For example:

1func VpnStatusCallback(s *discordgo.Session, m *discordgo.MessageCreate) {
2	out, err := exec.Command("/bin/bash", "/root/scw-automation/bin/scw-vpn-status.sh").CombinedOutput()
3	if err != nil {
4		s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("❌ %s", strings.TrimSpace(string(out))))
5		return
6	}
7	s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("✅ %s", strings.TrimSpace(string(out))))
8}

Scaleway automation

The infrastructure operations the bot is doing (starting, monitoring the instances) are written as shell scripts. In hindsight, they could have been implemented in Go, but I had them lying around before I’ve made the Discord bot in Go.

These scripts rely on the Scaleway CLI to interact with the instances. Starting a new instance is this simple:

1scw instance server create type=DEV1-S zone=fr-par-1 image=ubuntu_focal root-volume=l:20G  name=vpn -o template="{{ .ID }}"

SSH public keys of the always-on instance are automatically added to every new instance, so all scripts can SSH to the running instances.

OpenVPN

For installing OpenVPN, I’m relying on this project: https://github.com/angristan/openvpn-install

If I need to add/remove users or modify any OpenVPN configuration, I can manually SSH into the running instance and execute the openvpn-install.sh script again to do so. To persist the configuration, I download it from the instance to the location where it will be copied from to any new instances:

 1# Prepare config tgz on VPN server
 2cat << EOF | ssh -q -o BatchMode=yes -o "StrictHostKeyChecking=no" -o "ConnectTimeout=5" vpn.asdasd.hu 'bash -'
 3        rm -f /tmp/openvpn-conf.tar.gz
 4        tar -czf /tmp/openvpn-conf.tar.gz /etc/openvpn
 5EOF
 6
 7# Backup old config
 8mv $OWN_DIR/openvpn-conf.tar.gz $OWN_DIR/old-configs/openvpn-conf-`date +%Y-%m-%dT%H-%M`.tar.gz
 9
10# Save new
11scp -o "StrictHostKeyChecking=no" vpn.asdasd.hu:/tmp/openvpn-conf.tar.gz $OWN_DIR/

Termination

As mentioned above, I’d like to avoid forgetting to terminate the VPN instance, so I have a cron job in place that terminates the instance once no one’s using it anymore:

 1PREV_INACTIVE=`cat /tmp/vpn-inactive-since`
 2
 3CONN_COUNT=`$OWN_DIR/bin/scw-vpn-connection-count.sh`
 4if [ ! $? -eq 0 ]; then
 5        echo "unable to connect to VPN, probably not running"
 6        exit 3
 7fi
 8if [ $CONN_COUNT -eq 0 ]; then
 9        let PREV_INACTIVE=$PREV_INACTIVE+1
10else
11        let PREV_INACTIVE=0
12fi
13
14if [ $PREV_INACTIVE -ge 5 ]; then
15        $OWN_DIR/bin/scw-vpn-terminate.sh
16fi
17
18echo $PREV_INACTIVE > /tmp/vpn-inactive-since

The connection count was ugly enough to put into its own file:

1cat << EOF | ssh -q -o BatchMode=yes -o "StrictHostKeyChecking=no" -o "ConnectTimeout=5" vpn.asdasd.hu 'bash -'
2        { echo "load-stats"; sleep 1; } | telnet localhost 7505 2>/dev/null | grep SUCCESS | sed -E 's/^.*nclients=([0-9]+),.*$/\1/'
3EOF

Should you do this?

Probably not. It’s a lot easier to just buy the VPN service. I had a pleasant experience with NordVPN earlier and it has the added benefit of being able to select from a great variety of host countries to get around geo-blocking. There are two downsides. One is the price, but with intensive use it’s comparable to hosting your own. The other is the question of trust and privacy, but NordVPN seems to be delivering on that front.