Kodi server part 6: Always On VPN Client

Powered by Drupal
Submitted by Sam Hobbs on

This is the sixth part of a multi-part tutorial describing how to configure the "perfect" Kodi media centre running on top of ubuntu server. Other parts of the tutorial may be found here:

This section covers configuration of an openvpn client connection to an external server, to anonymise traffic to and from the Kodi box and prevent bandwidth throttling of torrent traffic by your ISP. You will also find some additional configuration to ensure the VPN does not break secure SSH connections from the public internet via your router, and some scripts to make sure Transmission binds to the local IP address of the VPN interface (so it doesn't try and send traffic out over your public connection if the VPN goes down).

Always on VPN connection

My ISP throttles torrent traffic, so I choose to run an always on VPN client connection on this box. The aim is to send all torrent traffic out through this interface. I personally use a VPN service called privateinternetaccess (PIA), which I have found to be very useful. First, install openvpn:

sudo apt-get update
sudo apt-get install openvpn

Now download the config files from your provider, into your home directory. For PIA, this can be achieved with:

cd ~
curl -O https://www.privateinternetaccess.com/openvpn/openvpn-strong-tcp.zip

Now unpack it (the unzip command will unpack it into a subdir called openvpn):

sudo apt-get install zip unzip
unzip -d openvpn openvpn-strong-tcp.zip

Now copy the files you need into the config dir. Here I've used the config file for Germany as an example, but you could use any of them:

sudo cp ~/openvpn/Germany.ovpn /etc/openvpn/
sudo cp ~/openvpn/ca.rsa.4096.crt /etc/openvpn/
sudo cp ~/openvpn/crl.rsa.4096.pem /etc/openvpn/

Openvpn only creates connections for files ending in .conf. I chose to create a symlink from the .ovpn config file. This way you can store many .ovpn configuration files in the directory and update the symlink if you want to use a different config:

cd /etc/openvpn
sudo ln -s Germany.ovpn client.conf

Ubuntu ships with a script that can be used to update which nameservers are used for DNS when the VPN is active, which is installed at /etc/openvpn/update-resolv-conf. We need to add to the openvpn config file (/etc/openvpn/client.conf) to make sure this script gets called when the VPN starts and stops. Add these lines:

up /etc/openvpn/update-resolv-conf
down /etc/openvpn/update-resolv-conf

If your provider lets you authenticate with a username and password, you need to change the config so that openvpn knows without having to prompt interactively. Add this line (or modify existing lines):

auth-user-pass userpass.txt

Then create a file to store the username and password. This needs to be owned by root:root with read and write permissions for root only:

sudo touch userpass.txt
chmod 600 userpass.txt

Edit the file and add the username on the first line and the password on the second line. Save and close the file. To enable user defined scripts to be called by openvpn, we also need to change the script security level. Add this to your client.conf file:

script-security 2

Now we can enable openvpn so it starts automatically at boot, and start it up (restart it instead if it's already running):

sudo systemctl enable openvpn
sudo systemctl start openvpn

Test your IP from the commandline to see if your IP address has changed :

curl icanhazip.com

Enabling SSH connections from the WAN when the VPN is running

The linux kernel uses routing tables to determine where it should send network traffic. When openvpn brings up the VPN interface, it changes the routing table so that the VPN becomes the default route for all traffic not destined for your local network. Unfortunately, this means that if you SSH to the server from the WAN, the packets will come in on the wifi interface and be sent out on the VPN interface. The SSH client will be listening for a reply from your global IP address, not the VPN server's global IP address, and you will get a connection error. The default routing table can be shown using the route command. Without the VPN running, the routing table will look something like this:

Kernel IP routing table                                                                                                                                                                                           
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface                                                                                                                                     
default         UG    0      0        0 wlp2s0                                                                                                                                *        U     0      0        0 wlp2s0

With the VPN, the routing table will have additional entries for the local network of the VPN server:

Kernel IP routing table                                                                                
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface                          
default       UG    0      0        0 tun0                           
default         UG    0      0        0 wlp2s0                 UGH   0      0        0 tun0                        *      UH    0      0        0 tun0                         UG    0      0        0 tun0                           
6d.3e.32a9.ip4. UGH   0      0        0 wlp2s0                     *        U     0      0        0 wlp2s0

The routing tables can look quite confusing, but basically the kernel looks down the list and chooses the most specific entry for the traffic that is being sent, and uses that route to send the packet. The default destination is a catch-all for packets with no specific entry. The "gateway" is the next hop for packets that can't be sent directly (for example packets destined for the WAN when you are behind a router need to be sent to the router and not directly to the WAN address). The "genmask" is the netmask of the destination - in the last line, the genmask of means that the rule applies to any 192.168.1.X address instead of just the single IP address. To make sure traffic that arrives from your router gets sent back out via the router and not over the VPN, we need to add a new routing table. The route command we used earlier shows the default table if no other arguments are given. However, we are free to define multiple routing tables. As before, the following commands still assume that your router is at and the IP address of your server is First, add a new rule to tell the kernel to use a new routing table for traffic from

sudo ip rule add from table 128

You can show the existing rules using this command:

sudo ip rule show

Output should look like this:

0:      from all lookup local 
32764:  from lookup 128 
32766:  from all lookup main 
32767:  from all lookup default

Now we can create some rules in table 128. These assume that you have configured wireless internet and your wifi interface is wlp2s0:

sudo ip route add table 128 to dev wlp2s0
sudo ip route add table 128 default via

You can see these rules in the new routing table if you use the ip route show table 128 command:

default via dev wlp2s0 dev wlp2s0  scope link

These entries specify that traffic to any 192.168.1.X address should be sent out using the wifi interface, and that the default route is via your router. Now test a SSH connection from the WAN to the server while the VPN client is running (e.g. using a mobile phone, or from another server outside your LAN). If the new static route has worked, we need to make it permanent, which can be achieved by listing up commands in /etc/network/interfaces:

auto wlp2s0
iface wlp2s0 inet dhcp
    wpa-ssid "yourSSID"
    wpa-psk secrethash
    up ip rule add from table 128 || true
    up ip route add table 128 to dev wlp2s0 || true
    up ip route add table 128 default via || true

In the above, the || true part is there to make sure that if any of the commands produce warnings (e.g. because the route already exists) then the interface configuration continues instead of exiting with an error. Double check your have the commands entered correctly, and then restart the server and verify that the table has been added:

sudo ip rule show
sudo ip route show table 128

And test to see if the rest of the outgoing traffic is still passing through the VPN:

curl icanhazip.com

Binding Transmission to the IP of the VPN interface

Transmission does not have an option to bind to a specific interface. By default, transmission binds to, which means that packets will use the default route. When the VPN is up, the default route is the VPN, but if the VPN goes down for any reason Transmission will continue to send traffic on your normal IP address. To get around this, we can bind transmission to the local IP address of the VPN interface. To see your current config, type:


Here's an example:

enp3s0    Link encap:Ethernet  HWaddr c0:3f:d5:69:6d:90  
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback  
          inet addr:  Mask:
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:118477 errors:0 dropped:0 overruns:0 frame:0
          TX packets:118477 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1 
          RX bytes:1364691847 (1.3 GB)  TX bytes:1364691847 (1.3 GB)

tun0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  
          inet addr:  P-t-P:  Mask:
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:2 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:100 
          RX bytes:0 (0.0 B)  TX bytes:195 (195.0 B)

wlp2s0    Link encap:Ethernet  HWaddr 80:86:f2:bd:91:f6  
          inet addr:  Bcast:  Mask:
          inet6 addr: fe80::8286:f2ff:febd:91f6/64 Scope:Link
          RX packets:9566008 errors:0 dropped:0 overruns:0 frame:0
          TX packets:2290526 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:12721694676 (12.7 GB)  TX bytes:260800582 (260.8 MB)

In this example, there are four interfaces:

  • enp3s0 is the ethernet connection. At the moment it's not connected.
  • lo is the loopback address (localhost)
  • tun0 is the VPN, which has local ip address
  • wlp2s0 is the wireless card, which has local ip address

What we need to do is run a script every time the VPN client starts, which will stop transmission, update the address it should listen to in /etc/transmission-daemon/settings.json and then restart it again, causing it to bind to the IP address of the VPN interface. The OpenVPN manual lists a number of events where you can define a script to run (jump to the section called SCRIPTING AND ENVIRONMENTAL VARIABLES). Not all of them are relevant to openvpn in client mode. The ones we care about are:

  • up - executed when openvpn has created the VPN tunnel
  • down - executed after the VPN tunnel is closed
  • ipchange - executed after the connection is first authenticated, or when the remote IP address of the server changes

We have already specified update-resolv-conf to change the nameservers for DNS when the vpn starts and stops. What's great about these scripts is that they have access to some environment variables set by openvpn. To see what these are, we can edit the update-resolv-conf script and add the following line to the end of the script:

printenv > /etc/openvpn/printenv-updown.log

This will print the environment variables to a file next time the script is called. Restart openvpn to make this happen:

sudo service openvpn restart

If you look in this file you will see some environment variables like:


You can see in update-resolv-conf that the $script_type variable is used to take certain actions depending on whether the interface is being brought up or taken down. The $ifconfig_local variable contains the local IP of the interface, which is exactly what we need for our script. This is much simpler than other solutions I have seen online, which try to parse the output of ifconfig and chop out all of the unwanted bits - sometimes minor changes in the output of ifconfig can cause such approaches to break. Open /etc/openvpn/update-resolv-conf again and add the following to the end of the file, which will allow us to write additional scripts without putting all of our changes in that file:

# added to call additional up and down scripts
case "$script_type" in

Now create two new files and make them executable:

sudo touch up down
sudo chmod +x up down

Paste the following into up:


# script to be run when the VPN is brought up

logger "Local IP of VPN interface is $ifconfig_local"

# rebind transmission to the local IP of the VPN interface
# transmission settings.json looks something like:
# {
# ...
#       "bind-address-ipv4": "",
# ...
# }
# settings.json is overwritten when transmission exits.
# If file is edited and transmission is restarted, the edit is lost because
# transmission dumps the current config to the file when it stops.

logger "stopping transmission"
systemctl stop transmission-daemon
wait 10

logger "replacing bind IP with current local IP of VPN interface"

# the following command could be used if the version of sed installed had the -c option, but it is only included in redhat versions
# see https://stackoverflow.com/questions/2610167/how-to-sed-search-and-replace-without-changing-ownership#2610198
#sed -i -r -c 's/("bind-address-ipv4": ")([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(",)/\1'$ifconfig_local'\3/g' /etc/transmission-daemon/settings.json
# instead execute sed without the -c option and then restore the proper permissions and ownership manually
sed -i -r 's/("bind-address-ipv4": ")([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(",)/\1'$ifconfig_local'\3/g' /etc/transmission-daemon/settings.json

chown debian-transmission:debian-transmission /etc/transmission-daemon/settings.json
chmod 660 /etc/transmission-daemon/settings.json

logger "restarting transmission-daemon"
systemctl restart transmission-daemon

exit 0

And the down script:

# script to be run when the VPN is brought down
logger "stopping transmission"
systemctl stop transmission-daemon

Now let's test. Before reloading anything, have a look and see which IP addresses transmission is currently bound to:

sudo netstat -tulpn | grep transmission

You should see output like the following but without the header, which I've added back in:

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0*               LISTEN      19885/transmission-
tcp        0      0  *               LISTEN      19885/transmission-
tcp6       0      0 :::51413                :::*                    LISTEN      19885/transmission-
udp        0      0*                           19885/transmission-

Now restart openvpn:

sudo service openvpn restart

The logger lines in the script should appear in the journal (you can view these as they are written in another SSH window with sudo journalctl -f) and if you run the netstat command again you should see:

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0*               LISTEN      19885/transmission-
tcp        0      0  *               LISTEN      19885/transmission-
tcp6       0      0 :::51413                :::*                    LISTEN      19885/transmission-
udp        0      0*                           19885/transmission-

So transmission has successfully bound to the IP address of the VPN interface. If you have IPv6 enabled, note that this hasn't stopped transmission from listening on all addresses for IPv6 connections (so you would need to make further changes). Also note that transmission is still listening on all interfaces for the web UI on port 9091 (although access is controlled using the whitelist as mentioned earlier). There's a useful tool called iftop that will show you a summary of traffic on a given interface. I find it quite useful for checking that all outgoing traffic is on the right interface. You can install it like this:

sudo apt-get update
sudo apt-get install iftop

And then use it to check traffic on the wifi like this:

sudo iftop

Or if you want to see the traffic being passed through the VPN tunnel, specify the interface:

sudo iftop -i tun0

Problems with this section? Let me know in the comments. Otherwise, continue to part 7 - firewall configuration.


Hello Sam,
I love your tutorials, they are to the point and explain well enough without covering all the linux basics.
I followed everything to the tee above and stopped where I had to check the ssh connection from a remote client with the VPN running (WAN to Server via SSH). I could not connect because the keys don't match and upon failing that the password does not match (checked with SSH -v host@VPNPubIP and I am 2000% my passwords are entered correctly). I can connect without a problem over WAN without the VPN running and router ports forwarded but not with the VPN running. I then did some digging and found that the PIA Aus server doesn't support port forwarding. So I then tried the CA Torronto one, as it did. Still no luck.
I then turned off my custom listening port in /etc/ssh/sshd_config and left it on Port 22 as PIA VPN left it open and still no luck (FYI, almost all the ports are blocked when I run the OpenVPN PIA connection). Is there anything you could recommend? I might even try installing the Ubuntu server / Germany PIA Server on my pi rather than Rasbian just so I have an almost identical setup to you (software wise) and try it all again.

Also for other people who ran into this, I had to add the following to my client.conf for Openvpn as there was an error with script security on execution:
"script-security 2"
The up / down additions for the DNS didn't work and the OpenVPN daemon crashes due to fatal error without the addition.

I really hope you might be able to shed some light on my issues and I look forward to your response,
Kind Regards,

Chrishan, Sounds like you're trying to SSH to the IP address of the VPN server, and the VPN provider's firewall is blocking the request? This tutorial assumes that you SSH to your server using the public IP address of the server (as you would have done if the VPN was not running), not the public IP address of the VPN server (i.e. the SSH daemon is still running on the normal IP address of your server, and all other traffic to and from the box is sent over the VPN). Sam


Sat, 07/14/2018 - 10:29

First of all, thanks for the instructions, these has guided me furthest with VPN settings combined with SSH remote connection (other guides has failed). I stumbled into some troubles and hoped you could help to get through them.

I'm new with Ubuntu so these problems may be too easy for me to google around. I am unable to get the printenv output to log which makes me suspect that the problem is with script running on up/down events.
This also causes the binding of the transmission to fail.

My network device is nep4s0 which I think I have noticed in needed places (ip rules, routes and in network interfaces (also leaving out wpa SSID and password commented out)). So the VPN connection is working and I am able to remote connect via SSH. What am I missing with OpenVPN config that causes possible script running to fail? Or could there be some other problem I'm missing?

Thank you so much for your detailed instructions.
Best regards,

Hmm... Is update-resolv-conf called in your /etc/openvpn/client.conf file? I noticed just now that there was a typo in the tutorial before, I wrote /etc/openssh/client.conf when I meant /etc/openvpn/client.conf, so I'm wondering if you made the changes to the wrong file? Sam

Thanks for the reply.
I did notice the typo with openssh/openvpn so it was as it should. And yes, update-resolv-conf was called in then client.conf file. I found some workaround, however, by calling the up and down scripts straight from the client.conf. This way the triggers worked. Is there any other need to use the update-resolv-conf in up/down VPN events?

Anyways, the VPN connection crashed at some point after running few hours. When figuring out the cause I noticed that there was two local IPs in the router: in addition to the original there was new IP with the same host name but with caps. Moreover, I noticed some strange behavior with tail /var/log/syslog which implied that I had another VPN service trying to restart over and over again with old configuration file that no longer existed. I guess this was some ghost service from the previous VPN with remote SSH attempts that I tried following other instructions. After disabling the service the syslog was ok and the VPN and transmission daemon has been running without problems.

So all in all, thank you for your assistance. If you have any advice on the double IP (why is this and is it good or bad) I'm truly thankful. I think the main thing is that the server works as it should so if double IP with server is no big deal then I'm good.
Thanks again for the tutorial!
Best regards,

Apparently something is still going wrong on the server. Just lost connection to the server (on the work atm) after about 20 hours uptime. I can see my router info remotely and it seems that the new local IP address is still there but the old one that has port forwarding on the router for remote connection has gone missing. Oh, by the way, instead of Kodi I'm using Plex to stream media (I don't suppose this causes problems in terms of these instructions) and Plex is now also unavailable through the web interface.

So if there are any approach to start digging the problem up I'd be super thankful!

Add new comment

The content of this field is kept private and will not be shown publicly.

Filtered HTML

  • Web page addresses and email addresses turn into links automatically.
  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.