On-demand VPN
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.