[Full Guide] Set up PIA on Headless Ubuntu with Transmission and Port Forwarding with Auto-Reconnect

edited September 1 in Linux VPN Setup Posts: 1
This is my complete guide to my extremely over-complicated setup.  This will be very long in an attempt to be detailed and include as much code as I can.  I welcome feedback and improvements.  This should be mostly portable to any other version of linux. My preference is to use vi as an editor but use whatever you want. I'll leave my username as surfrock66...any time you see that change it to your username.

Before we begin, I have some components which may set the bar too high for this to be used by most people:
  1. A hosted website which is managed by cpanel.  From there, I can manage subdomains (I use 2..home.domain.com and tunnel.domain.com)
  2. My Primary server; this can be any linux device on your network.  The important requirements: a publically accessible web server supporting php (or any other dynamic language), nfs and ssh availability with password-less keyfile authentication.  I will refer to it as home-server.  For me, it also runs libresonic, pydio, minidlna, ttrss, dns, dhcp, and a bunch of other stuff so it's on all the time and very robust.
  3. My VPN computer, running ubuntu server headless.  I will refer to it as tunnel-server. 
On home-server, I utilize the following script from the cPanel forums to update home.domain.com with the dynamic IP from my ISP.  These run via a cronjob as my primary user (surfrock66) once an hour:
Thread: https://forums.cpanel.net/threads/can-cpanel-update-dynamic-ip-information-to-dns-records.261951/
Script (must be logged in): https://forums.cpanel.net/attachments/cpanel-dynamic-dns-sh-txt.16051/

On home-server I have my apache web root exported to tunnel-server via NFS as well as my home directory.  Those will be mounted on tunnel-server under /mnt/home-server.  I have also configured my Transmission download directory to go to home-server for easy file manipulation, but there's many ways to handle that task.

On tunnel-server, I installed PIA and openvpn using this guide:

sudo apt-get install openvpn
cd /etc/openvpn
sudo wget https://www.privateinternetaccess.com/openvpn/openvpn.zip
sudo unzip openvpn.zip
When choosing a gateway, because we'll be needing port-forwarding, you need to choose one of the ones listed here:

I then set up a systemd service called openvpnauto to automatically start the PIA VPN using this guide here: https://www.htpcguides.com/autoconnect-private-internet-access-vpn-boot-linux/

sudo vi /etc/init.d/openvpnauto
Then paste this...I used "CAToronto.ovpn" (which I renamed from "CA Toronto.ovpn" for filename simplicity) but so long as you're using a port forwarding enabled gateway, you'll be fine
# Provides:          OpenVPN Autoconnect
# Required-Start:    $local_fs $remote_fs $network
# Required-Stop:     $local_fs $remote_fs $network
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: OpenVPN Autoconnect
# Description:       OpenVPN Autoconnect

# Documentation available at
# http://refspecs.linuxfoundation.org/LSB_3.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptfunc.html
# Debian provides some extra functions though
. /lib/lsb/init-functions

DAEMON_OPTS="--config /etc/openvpn/CAToronto.ovpn --auth-user-pass /etc/openvpn/login.txt"
DAEMON_DESC=$(get_lsb_header_val $0 "Short-Description")

[ -r "/etc/default/${DAEMON_NAME}" ] && . "/etc/default/${DAEMON_NAME}"

do_start() {
  local result

    pidofproc -p "${DAEMON_PID}" "${DAEMON_PATH}" > /dev/null
    if [ $? -eq 0 ]; then
        log_warning_msg "${DAEMON_NAME} is already started"
        log_daemon_msg "Starting ${DAEMON_DESC}" "${DAEMON_NAME}"
        touch "${DAEMON_LOG}"
        chown $DAEMON_USER "${DAEMON_LOG}"
        chmod u+rw "${DAEMON_LOG}"
        if [ -z "${DAEMON_USER}" ]; then
            start-stop-daemon --start --quiet --oknodo --background \
                --nicelevel $DAEMON_NICE \
                --chdir "${DAEMON_PWD}" \
                --pidfile "${DAEMON_PID}" --make-pidfile \
                --exec "${DAEMON_PATH}" -- $DAEMON_OPTS
            start-stop-daemon --start --quiet --oknodo --background \
                --nicelevel $DAEMON_NICE \
                --chdir "${DAEMON_PWD}" \
                --pidfile "${DAEMON_PID}" --make-pidfile \
                --chuid "${DAEMON_USER}" \
                --exec "${DAEMON_PATH}" -- $DAEMON_OPTS
        log_end_msg $result
    return $result

do_stop() {
    local result

    pidofproc -p "${DAEMON_PID}" "${DAEMON_PATH}" > /dev/null
    if [ $? -ne 0 ]; then
        log_warning_msg "${DAEMON_NAME} is not started"
        log_daemon_msg "Stopping ${DAEMON_DESC}" "${DAEMON_NAME}"
        killproc -p "${DAEMON_PID}" "${DAEMON_PATH}"
        log_end_msg $result
        rm "${DAEMON_PID}"
    return $result

do_restart() {
    local result
    if [ $result = 0 ]; then
    return $result

do_status() {
    local result
    status_of_proc -p "${DAEMON_PID}" "${DAEMON_PATH}" "${DAEMON_NAME}"
    return $result

do_usage() {
    echo $"Usage: $0 {start | stop | restart | status}"
    exit 1

case "$1" in
start)   do_start;   exit $? ;;
stop)    do_stop;    exit $? ;;
restart) do_restart; exit $? ;;
status)  do_status;  exit $? ;;
*)       do_usage;   exit  1 ;;
Then run the following to get the service up and running:
sudo chmod +x /etc/init.d/openvpnauto
echo "IP Before VPN:"
wget http://ipinfo.io/ip -qO -
sudo systemctl daemon-reload
sudo systemctl start openvpnauto.service
echo "IP After VPN:"
wget http://ipinfo.io/ip -qO -
You SHOULD see 2 different IP's. That means the VPN is working. If it isn't, troubleshoot that before proceeding. If you want to test the VPN without all the systemd stuff, this command should work:
sudo /usr/sbin/openvpn --config /etc/openvpn/CAToronto.ovpn --auth-user-pass /etc/openvpn/login.txt
Then run the IP check in another terminal session.

Now that the VPN works, we need to set up Transmission. As a precursor, you should know a giant pain in the ass about this whole process...transmission doesn't respect setting changes unless the daemon is stopped, and every time the daemon is started it screws with the permissions and ownership of the settings file. There is no point doing anything with it as an unprivileged user, it has to be root. Other things in here shouldn't really be done as root, so separating tasks per-user is kind of important and a little nuanced. Use the following code to install transmission daemon on ubuntu server:
sudo add-apt-repository ppa:transmissionbt/ppa
sudo apt-get update
sudo apt-get -y install transmission-cli transmission-common transmission-daemon
sudo service transmission-daemon stop
sudo vi /var/lib/transmission-daemon/info/settings.json
You need to update transmission's settings.  The setting advice here is good: https://help.ubuntu.com/community/TransmissionHowTo but the important stuff is to disable whitelisting (for remote access from unknown IP's), change the umask to 2, change the username and password, and change your download directory.  You can change anything else you like (except the ports!).

Once finished, we need to do the following to start the daemon (which will obscure the password you typed), stop it again, make a copy of the settings file, and lax the permissions on it for later updating by a non-root user. I'm also adding my user account to the transmission group:
sudo usermod -a -G debian-transmission surfrock66
sudo service transmission-daemon start
sudo service transmission-daemon stop
sudo cp /etc/transmission-daemon/settings.json /etc/transmission-daemon/settings.json.edits
sudo chown surfrock66:debian-transmission /etc/transmission-daemon/settings.json.edits
sudo chmod 664 /etc/transmission-daemon/settings.json.edits
sudo service transmission-daemon start
We need to do some user permission nonsense. There needs to be some sudo rules in place so my unprivileged user can restart some services, and we also need to do some polkit work to enable access to the systemctl command. Here's the relevant lines from my /etc/sudoers file (to be edited with visudo):
Cmnd_Alias      RESTART_VPN = /bin/systemctl restart openvpnauto.service
If you run "pkaction --version" and get a value less than 0.106, you need to use localauthority syntax and create this file: /etc/polkit-1/localauthority/50-local.d/openvpnauto.pkla
[openvpnauto Permissions]
If you have a version equal to or higher than 0.106, make this file: /etc/polkit-1/rules.d/10-manage-openvpnauto.rules:
// Allow alice to manage openvpnauto.service;
// fall back to implicit authorization otherwise.
polkit.addRule(function(action, subject) {
    if (action.id == "org.freedesktop.systemd1.manage-units" &&
        action.lookup("unit") == "openvpnauto.service" &&
        subject.user == "surfrock66") {
        return polkit.Result.YES;
Based on the code in this post, we need to come up with a way to grab and document a new forwarded port upon VPN connection. Once you establish a VPN connection, the API documented here will give you a single port within a 2 minute window: https://www.privateinternetaccess.com/forum/discussion/3359/port-forwarding-without-application-pia-script-advanced-users#latest. One quick task, we need to make a write-able file for storing the port that my regular user can write to:
sudo touch /etc/openvpn/port
sudo chmod 666 /etc/openvpn/port
My version of this (located in /etc/openvpn/openvpn.portForward.sh) does a bunch more stuff. It notifies me if there's an issue via mail with updating the port (I have exim4 set up to send mail...if you don't, you can just remove that). This code requests the new port, validates that it's there, writes it to a file, updates the STAGING VERSION of the transmission settings with the new port (both the RPC port and the peer communication port, which can be the same), invokes the cpanel dynamic dns script for the tunnel subdomain, then lastly puts the port in a file on the home-server.  Because this script will be run as my regular user, this is the place to run the cpanel update script as it should NOT be run as root (and updating it within a 5 minute window triggered by a likely VPN reconnect makes more sense):
#!/usr/bin/env bash
# Enable port forwarding when using Private Internet Access
client_id=`head -n 100 /dev/urandom | sha256sum | tr -d " -"`
json=`curl "$client_id" 2>/dev/null`
echo "$json"
if [ "$json" == "" ]; then
        echo "Error updating the port forward rule for tunnel-server's VPN" | mail -s "Tunel-Server VPN Port Forwarding Process Failed" surfrock66@domain.com
        port=$( echo "$json" | cut -c9-13 )
        echo "$port" > /etc/openvpn/port
        sed -i -r "s|\"rpc-port\": [0-9][0-9]*,|\"rpc-port\": $port,|g" /etc/transmission-daemon/settings.json.edits
        sed -i -r "s|\"peer-port\": [0-9][0-9]*,|\"peer-port\": $port,|g" /etc/transmission-daemon/settings.json.edits
        echo "$port" > /mnt/home-server/.tunnel.port
exit 0
Now that we have this, we can manage the VPN connection testing and reconnect via a cronjob. I also have cronjobs for making sure tunnel.domain.com is updated every 10 minutes. The cronjob must be run as a normal user, so here is the output of my crontabs:
# DynDNS Setting
*/10 * * * * /bin/bash /etc/openvpn/cpanel-dynamic-dns-tunnel-domain-com.sh >/dev/null 2>&1
*/10 * * * * /bin/bash /etc/openvpn/cpanel-dynamic-dns-all-tunnel-domain-com.sh >/dev/null 2>&1
# Start VPN on boot
@reboot sudo /bin/systemctl restart openvpnauto.service; sleep 10; /bin/bash /etc/openvpn/openvpn.portForward.sh
# Notify if VPN is down
*/5 * * * * localip=$(wget http://ipinfo.io/ip -qO -); remoteip=$(ssh home-server "wget http://ipinfo.io/ip -qO -"); if [ "$localip" = "$remoteip" ]; then echo -e "Remote IP: $remoteip\nLocal IP: $localip" | mail -s "Tunnel-Server VPN is Potentially Down!" surfrock66@domain.com; sudo /bin/systemctl restart openvpnauto.service; sleep 10; /bin/bash /etc/openvpn/openvpn.portForward.sh; fi
This is a complex cronjob running every 5 minutes. First, it pulls the local IP and stores it in a variable (this should be a VPN-enabled IP). Then, it runs a remote ssh command on home-server to get the IP from there (non-VPN). It compares the 2. If they are different, send me an email (again, if you don't use mail/exim4, remove that) then restarts the VPN, waits 10 seconds, then refreshes the port forwarding.

One question at this point...why all the cron, why not run that in an openvpn up script so systemd can manage the service? Honestly...I couldn't get it to work and got fed up. My googling found a lot of evidence that up scripts are unreliable, especially with weird user account stuff. The cron job is a little more versatile and easier to debug, plus the longest downtime we're talking about is 5 minutes. Because this is how we're gonna manage that openvpnauto service, we need to disable it from starting on boot:
sudo systemctl disable openvpnauto.service
So, at this point, at will, you will get a new IP and port forwarding assignment automatically. Reboot your server, kill the service, test it out...it'll come back up (and notify you if you have that set up). Still, transmission doesn't know about the new port, and you won't really have a great way to navigate to the new URL. To update Transmission, I wrote the following script: /etc/openvpn/openvpn.refreshTransmissionSettings.sh
sum1=$(md5sum /etc/transmission-daemon/settings.json | awk '{print $1}')
sum2=$(md5sum /etc/transmission-daemon/settings.json.edits | awk '{print $1}')
if [[ "$sum1" != "$sum2" ]]; then
        systemctl stop transmission-daemon.service
        cp /etc/transmission-daemon/settings.json.edits /etc/transmission-daemon/settings.json
        systemctl start transmission-daemon.service
Basically, this takes the editable version of the transmission settings, hashes it, if it's different than the hash of the running version, stop the daemon, copy the editable one to the real one, then restart the daemon. Due to the way transmission handles and updates the permissions of that settings file, this has to be done by root. To that end, I have a root cronjob running every 5 minutes (offset by 1) to check this:
# Refresh Transmission Settings
1-59/5 * * * * /bin/bash /etc/openvpn/openvpn.refreshTransmissionSettings.sh
Ok, done. Tunnel-server will now keep itself up to date on all fronts. My subdomain tunnel.domain.com will update automatically, but how will I learn the new randomized port? Very simple...you'll remember we also have home.domain.com which is dynamically updated AND has a copy of the port in a file in the web root. I wrote the following php file and stuck it in there:
        $lines = file('/var/www/.tunnel.port', FILE_IGNORE_NEW_LINES);
        $port = $lines[0];
        header('Location: http://tunnel.domain.com:' . $port);
                <a href="http://tunnel.domain.com:<?php echo $port; ?>">http://tunnel.domain.com:<?php echo $port; ?></a>
If I navigate to "http://home.domain.com/tunnel.php" it should immediately redirect me to my transmission instance. This has worked flawlessly since I set it up; sometimes there's a DNS update lag, but with a 5 minute TTL set for that subdomain on my hosting provider, it's never very long.

So there it is. All the pieces here can be adapted to other solutions. I also had to do some other steps...by making the transmission download directory exist over the nfs mount i had to make sure my UID's were consistent across systems. Both of these systems have 2 1GB NICs so I bonded them for redundancy, that's out of scope here.  I would appreciate feedback or suggestions...I'm sure there's a bunch of stuff that would simplify this, but it's working well. Thanks for reading!
Post edited by surfrock66 on
Sign In or Register to comment.