Due to growing mistrust of powerful actors within the realm of Cyber Security, and how it is becoming almost impossible to identify any agencies involved in InfoSec whose mission statement and/or modus operandi can be considered ethical or serving the interests of the vast majority of Internet users. Unscrupulous surveillance programs are conducted flagrantly by democratically elected Governments who have compromised their core ideologies - those that protect the freedom and the privacy of the individual, as if willfully pushing from their collective minds the horrific lessons history has taught us about such uninhibited surveillance.

The Internet is too uncomfortable and unpredictable for policymakers - who yearn for some degree of control, or oversight. Simultaneously, the rewards in terms of data haulage are orders of magntiude greater than anything we have historical precedent for. Such power and such fear, tightly coupled - does not bode well for rational decisions being made in good faith by those with power and influence. Nation states, ISPs and other telecommunication behemoths have grown noticeably indistinct from one another - in a kind of augmented Military-Industrial Complex, this is more of a wholesale Communication-Network-Intelligence Complex. In times when we can’t know the extent of these far reaching actions, or their short-medium term consequences in terms of legislation, retrospective investigations and uninformed response to fear. In these kind of times, it seems prudent to take personal, technological control over mitigating the mysterious yet ubiquitous risks to our privacy. Anonymity online is a fabled concept, but simply decreasing your transparency with a small number of changes to how we use the Internet, is arguably eminently worthwhile as it would conceivablely raise the cost of employing dragnet surveillance on you personally into the sorts of regions where it just isn’t worth it. VPNs are a good place to start - and the pre-eminent protocol / tool-suite for connecting to VPNs is arguably OpenVPN. For this reason, it seemed shrewd to create some kind of API or framework for controlling OpenVPN at a higher level - one that enabled the automation of otherwise logistically complicated (and fraught) procedures - such as switching VPN server on the fly, without broadcasting your real IP address at any point during the re-negotiation of the VPN connection.

The OpenVPN controller script attached below, is implemented in bash - and includes a facility to stop particular services (with checks) for the duration of the connection changeover. In this case, it will stop the transmission-daemon service, before restarting it upon determining a successful connection has been established to the new VPN server. The controller also includes a function designed to be called by cron, which checks the IP address periodically - and if it is found that the connection has dropped / or that the IP address has reverted to the last known real IP address, it will automatically rotate the VPN configuration and connect to a new VPN server.

Fairly verbose logging is standard in this script, due to the critical nature of the order of operations - it pays to have detailed logs to check during setup.


$HOME/.bash.d/.bash_vpn :

#!/bin/bash

BASH_HOME=$HOME/.bash.d
source $BASH_HOME/.bash_geo
source $BASH_HOME/.bash_parse

IPLOG_PATH="$HOME/.iplog"
VPN_CONF_PATH="/etc/openvpn"
VPN_MAIN_CONF_FILE="vpnx.conf"
VPN_MAIN_CONF_FILE_PATH="${VPN_CONF_PATH}/${VPN_MAIN_CONF_FILE}"
DEBUG_LOG_FILE_PATH="$HOME/.iplogcrondebug.log"
GEOLOC_LOG_FILE_PATH="$HOME/.gloc.log"
LAST_NATURAL_IP_FILE="$HOME/.last_known_natural_ip"
if [ -f "$LAST_NATURAL_IP_FILE" ]; then SXIP=$(cat $LAST_NATURAL_IP_FILE) && export SXIP; fi;

TRDAEMON="transmission-daemon"
TRDAEMONBIN=/usr/bin/${TRDAEMON}
TRUSER=debian-transmission

VPNLIST=( france germany sweden switzerland luxembourg finland nl1 nl2 nl3 italy lithuania poland portugal romania spain coventry london portsmouth )

if [ -z $CURRENT_VPN_INDEX ]
then
    if [ -f "$VPN_MAIN_CONF_FILE_PATH" ]
    then;
        current_vpn_target_path=$(readlink "$VPN_MAIN_CONF_FILE_PATH")
        current_vpn_file=$(basename "$current_vpn_target_path")
        current_vpn_name="${current_vpn_file%.ovpn}"
        arr_count=0

        for vpn_entry in "${VPNLIST[@]}"
        do;
            if [ "${vpn_entry}" == "${current_vpn_name}" ]
            then;
                CURRENT_VPN_INDEX=$arr_count ; export CURRENT_VPN_INDEX;
                CURRENT_VPN_NAME="${VPNLIST[$CURRENT_VPN_INDEX]}" ; export CURRENT_VPN_NAME;
            else;
                arr_count="$(( ++arr_count ))"
            fi;

        done;

    else;
        CURRENT_VPN_INDEX=0 ; export CURRENT_VPN_INDEX;
        CURRENT_VPN_NAME="" ; export CURRENT_VPN_NAME;
    fi;
fi;


function getExtIp {
    echo "$(curl -s icanhazip.com)";
}

function storeNaturalExtIpInFile {

    naturalExtIp="$(getNaturalExtIp)"
    if [ "$?" -eq 0 ]
    then;
        echo "$naturalExtIp" > $LAST_NATURAL_IP_FILE;
        return 0;
    else;
        return 1;
    fi;
}

function getNaturalExtIp {

    naturalExtIp="$(ssh other@machine.on.your.network curl -s icanhazip.com)"

    if $(testArgumentIsIPv4Address "$naturalExtIp")
    then;
        echo "$naturalExtIp"
        return 0;
    else;
        return 1;
    fi;
}

function iplogcron {

    local ipnow="$(getExtIp)"

    storeNaturalExtIpInFile
    SXIP="$(cat $LAST_NATURAL_IP_FILE)"
    if [ -n "$SXIP" ]
    then;
        export SXIP;
    fi;

    echo -e "$(date) ::\t ${ipnow} ::\t Natural IP:[${SXIP}]" >> "${IPLOG_PATH}";

    if [ $(chkvpnargs ${ipnow}) -ne 0 ]
    then;
        vpnDownMsg=$(echo "VPN DOWN:\t IP=${ipnow} \t VPN_INDEX=${CURRENT_VPN_INDEX} \t VPN_NAME=${VPNLIST[$CURRENT_VPN_INDEX]}");
        echo -e "$vpnDownMsg" >> "$DEBUG_LOG_FILE_PATH";
        echo -e "$vpnDownMsg";

        forceSwitchVpnConnection;
    else;
        vpnOkMsg=$(echo "VPN OK:\t IP=${ipnow} \t VPN_INDEX=${CURRENT_VPN_INDEX} \t VPN_NAME=${VPNLIST[$CURRENT_VPN_INDEX]}");
        echo -e "$vpnOkMsg";
    fi;
}

function parseForceSwitchArgs {

    local changeIndex

    if [ "$#" -eq 1 ]
    then;

        if [ $(testArgumentIsNumeric "$1") ]
        then;
            changeIndex=$1;
        else;
            inputArg="$1"
            CLEANSTRING="$(sanitizeArgument ${inputArg})"
            element_index=$(getElementIndex "$CLEANSTRING" "${VPNLIST[@]}")

            if [ "$?" -eq 0 ]
            then;
                changeIndex="$element_index";
            else;
                echo " > > > INPUT [$CLEANSTRING] INVALID CHOICE :: Choose value from [${VPNLIST[@]}] < < <";
                return 1;
            fi;
        fi;

        if [ -n "$changeIndex" ]
        then;
            if [ "$changeIndex" -eq 0 ]
            then;
                CURRENT_VPN_INDEX="$(( ${#VPNLIST[@]} - 1 ))" ; export CURRENT_VPN_INDEX ; echo " > > > CHANGING VPN TO [${VPNLIST[$changeIndex]}] :: index [$changeIndex] > > >";
            elif [ "$changeIndex" -lt "${#VPNLIST[@]}" ]
            then;
                CURRENT_VPN_INDEX="$(( $changeIndex - 1 ))" ; export CURRENT_VPN_INDEX ; echo " > > > CHANGING VPN TO [${VPNLIST[$changeIndex]}] :: index [$changeIndex] > > >";
            else;
                echo " > > > INDEX [$changeIndex] OUT OF RANGE :: CHOOSE VALUE FROM 0 TO $(( ${#VPNLIST[@]} - 1 )) < < <";
                return 1;
            fi;
        fi;
    fi;

    return 0;

}

function forceSwitchVpnConnection {

    local numArgs="$#"
    local calledByUser
    local calledStateCode="$(ps -o stat= -p $$)" &>/dev/null

    if [[ "$calledStateCode" =~ .*\+ ]] && [ "$numArgs" -eq 0 ]
    then;
        calledByUser="true";
        local nextVpnIndex;

        [[ $CURRENT_VPN_INDEX -ne $(( ${#VPNLIST[@]} - 1 )) ]] && nextVpnIndex=$(( $CURRENT_VPN_INDEX + 1 )) || nextVpnIndex=0;

        echo " > > > CHANGING VPN TO [${VPNLIST[$nextVpnIndex]}] :: index [$nextVpnIndex] > > >";
    fi;

    if [ -n "$numArgs" ] && [ "$numArgs" -gt 0 ]
    then;
        parseForceSwitchArgs $@;
        if [ $? -ne 0 ]
        then
            return 1;
        fi;

        calledByUser="true";
    fi;

    echo -e "\n\n:\t:\t:\t:\t FORCE SWITCH VPN CONNECTION\t :\t:\t:\t:\n" >> "$DEBUG_LOG_FILE_PATH";
    echo "$(date) :: *** Enter vpnRotateConfig" >> "$DEBUG_LOG_FILE_PATH";

    vpnRotateConfig;

    echo "$(date) :: *** Enter vpnReload" >> "$DEBUG_LOG_FILE_PATH";

    if [ "$(vpnReload)" = 0 ]
    then;
        echo "$(date) :: > > > SUCCESS *** vpnReload returned 0, so we can start $TRDAEMON again" >> "$DEBUG_LOG_FILE_PATH";

        sudo service "$TRDAEMON" start >> "$DEBUG_LOG_FILE_PATH" 2>&1;

        local extIp="$(getExtIp)";
        geoLocMsg=$(echo "New VPN Location: $(getGeoLocFromIp $extIp) :: [IP Address: $extIp]");
        echo "$geoLocMsg" >> "$DEBUG_LOG_FILE_PATH";

        if [ "$calledByUser" = "true" ]
        then;
            echo "$geoLocMsg";
        fi;
    fi;
}

function stopTransmissionDaemon {

    local retval=2
    local counter=0

    while [ $retval -gt 1 ]
    do;
        echo "$(date) :: *** Enter isServiceRunning: $TRDAEMON ... count = $counter" >> "$DEBUG_LOG_FILE_PATH";
        result="$(isServiceRunning $TRDAEMON)";

        if [ "$result" = 0 ]
        then;
            echo "$(date) :: > > > isServiceRunning returned 0 (not running) - so now we return 0" >> "$DEBUG_LOG_FILE_PATH";
            retval=0;
        elif [ $counter -gt 4 ]
        then;
            echo "$(date) :: > > > isServiceRunning returned 1 (running) 5 times in a row - so now we return 1" >> "$DEBUG_LOG_FILE_PATH";
            retval=1;
        else;
            echo "$(date) :: > > > isServiceRunning returned 1 (running) - so we try to stop $TRDAEMON and then sleep for 1 second (count = $counter)" >> "$DEBUG_LOG_FILE_PATH";

            sudo service "$TRDAEMON" stop >> "$DEBUG_LOG_FILE_PATH" 2>&1;
            sleep 1;

            counter="$(( ++counter ))";
        fi;

    done;

    echo $retval;
}

function vpnReload {

    echo "$(date) :: *** Enter stopTransmissionDaemon" >> "$DEBUG_LOG_FILE_PATH";

    stopTDResult="$(stopTransmissionDaemon)";

    echo "$(date) :: --- Leaving stopTransmissionDaemon. retval = $stopTDResult" >> "$DEBUG_LOG_FILE_PATH";

    if [ "$stopTDResult" = 0 ]
    then;
        echo "$(date) :: > > > stopTransmissionDaemon returned 0 (successfully stoppped) so now we restart openvpn service..." >> "$DEBUG_LOG_FILE_PATH";

        local retval=2;
        local counter=0;

        while [ $retval -gt 1 ]
        do;
            echo "$(date) :: > > > Restart openvpn and then wait 10 seconds before checking IP Address (attempt no. $counter)" >> "$DEBUG_LOG_FILE_PATH";

            sudo service openvpn restart >> "$DEBUG_LOG_FILE_PATH" 2>&1;
            sleep 16;

            echo "$(date) :: *** Enter checkvpn [check for non domestic IP Address...]" >> "$DEBUG_LOG_FILE_PATH";

            if [ "$(chkvpn)" = 0 ]
            then;
                echo "$(date) :: > > > SUCCESS *** IP Address is: [$(getExtIp)] *** return 0" >> "$DEBUG_LOG_FILE_PATH";
                retval=0;
            elif [ $counter -gt 4 ]
            then;
                echo "$(date) :: ~ ~ ~ FAILURE --- IP Address inaccessible/unchanging [$SXIP] after 5 attempts --- return 1" >> "$DEBUG_LOG_FILE_PATH";
                retval=1;
            else;
                sleep 4;
                counter="$(( ++counter ))";
            fi;

        done;

        echo $retval;
    fi;
}

# Check ext IP isnt $SXIP
# Human readable
function checkvpn {

    if [ -f "$LAST_NATURAL_IP_FILE" ]; then SXIP=$(cat $LAST_NATURAL_IP_FILE) && export SXIP; fi;
    local xip="$(getExtIp)";

    if [ -z "$xip" ]
    then;
        echo "$(date) :: *** VPN Momentary Failure - External IP Cannot be Acquired";
    elif [[ "$xip" == "$SXIP" ]]
    then;
        echo "$(date) :: *** VPN Failure - External IP = [$xip]";
    else;
        echo "$(date) :: *** VPN Success - External IP = [$xip]";
    fi;
}

# Returns 0 (success) or 1 (fail)
function chkvpn {

    if [ -f "$LAST_NATURAL_IP_FILE" ]; then SXIP=$(cat $LAST_NATURAL_IP_FILE) && export SXIP; fi;
    local xip="$(getExtIp)";

    if  [ -n "$xip" ] && [[ "$xip" != "$SXIP" ]]
    then;
        echo 0;
    else;
        echo 1;
    fi;
}

# As above but takes IP argument to compare with
function chkvpnargs {

    if [ -n "$1" ]
    then;
        if [ -f "$LAST_NATURAL_IP_FILE" ]; then SXIP=$(cat $LAST_NATURAL_IP_FILE) && export SXIP; fi;
        local xip="$1";

        if [[ "$xip" != "$SXIP" ]]
        then;
            echo 0;
        else;
            echo 1;
        fi;

    else;
        echo 1;
    fi;
}

function vpnRotateConfig {

    if [ -n $CURRENT_VPN_INDEX ]
    then;
        echo "$(date) :: > > > CURRENT_VPN_INDEX: $CURRENT_VPN_INDEX" >> "$DEBUG_LOG_FILE_PATH";

        (( ++CURRENT_VPN_INDEX )); export CURRENT_VPN_INDEX;

        if [ $CURRENT_VPN_INDEX -eq ${#VPNLIST[@]} ]
        then;
            CURRENT_VPN_INDEX=0; export CURRENT_VPN_INDEX;
        fi;

        echo "$(date) :: > > > NEW_VPN_INDEX: $CURRENT_VPN_INDEX" >> "$DEBUG_LOG_FILE_PATH";

        nextVpn="${VPNLIST[$CURRENT_VPN_INDEX]}";
        echo "$(date) :: > > > nextVpn: $nextVpn" >> "$DEBUG_LOG_FILE_PATH";

        nextVpnConfPath="${VPN_CONF_PATH}/${nextVpn}.ovpn";
        echo "$(date) :: > > > nextVpnConfPath: $nextVpnConfPath" >> "$DEBUG_LOG_FILE_PATH";

        rm -f "${VPN_MAIN_CONF_FILE_PATH}";
        ln -s "${nextVpnConfPath}" "${VPN_MAIN_CONF_FILE_PATH}";

        CURRENT_VPN_NAME=${nextVpn}; export CURRENT_VPN_NAME;

    fi;
}

function isServiceRunning {

    if [ -n $1 ]
    then;
        local service="$1";
        echo "$(date) :: > > > isServiceRunning? service: $service" >> "$DEBUG_LOG_FILE_PATH";

        if (( $(ps -ef | grep -v grep | grep $service | wc -l) > 0 ))
        then;
            echo "$(date) :: > > > YES *** $service IS running." >> "$DEBUG_LOG_FILE_PATH";
            echo 1;
        else;
            echo "$(date) :: ~ ~ ~ NO --- $service is NOT running." >> "$DEBUG_LOG_FILE_PATH";
            echo 0;
        fi;
    else;
        >&2 echo "$(date) :: *** Usage: $0 <name_of_service>";
    fi;
}

###############################################################################################
#       ALIASES                                                                               #
###############################################################################################

alias extip='curl -s icanhazip.com'
alias gextip='curl freegeoip.net/json/$(extip)'
alias fsvpn='forceSwitchVpnConnection'


$HOME/.bash.d/.bash_parse :

#########################################################################
# >>>   .bash_parse                                                     #
#-----------------------------------------------------------------------#
# bash utility functions to aid parsing input / manipulating arrays     #
#########################################################################

function getElementIndex {

    local e
    local arr_count=0

    for e in "${@:2}"
    do;
        if [[ "$e" == "$1" ]]
        then;
            echo $arr_count;
            return 0;
        else;
            arr_count="$(( ++arr_count ))";
        fi;
    done;

    echo "";
    return 1;
}

function sanitizeArgument {

    #-----------------------------------------------------------#
    # NB:        ${string//substring/replacement}               #
    # Replace all matches of $substring with $replacement.      #
    #-----------------------------------------------------------#

    local retval

    if [ "$#" -ne 1 ]
    then;
        return 1;
    fi;

    suspectArgument="$1";
    cleanArgument="${suspectArgument//[^a-zA-Z0-9]/}";
    echo "$cleanArgument";

    return 0;
}

function testArgumentIsNumeric {

    local retval;

    if [ "$#" -ne 1 ]
    then;
        retval=1;
    fi;

    if [ "$1" -eq "$1" ] 2> /dev/null
    then;
        retval=0;
    else;
        retval=2;
    fi;

    return $retval;
}

function testArgumentIsIPv4Address {

    if [ "$#" -ne 1 ]
    then;
        return 1;
    fi;

    local ip=$1;
    local retval=1;

    if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]
    then;
        OIFS=$IFS;
        IFS='.';
        ip=($ip);
        IFS=$OIFS;

        [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]];
        retval=$?;

    fi;

    return $retval;
}


$HOME/.bash.d/.bash_geo :

#!/bin/bash

###############################################################################################
#     IP UTILITY FUNCTIONS                                                                    #
###############################################################################################

function validip()
{
    local  ip=$1;
    local  stat=1;

    if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]
    then;
        OIFS=$IFS;
        IFS='.';
        ip=($ip);
        IFS=$OIFS;
        [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]];
        stat=$?;
    fi;
    return $stat;
}

function getWanIp() {
  echo "$(curl -s icanhazip.com)";
}

###############################################################################################
#     GEOCODING FUNCTIONS                                                                     #
###############################################################################################

function getGeoLocFromIp()
{
    local print_lat_lon

    if [ "$#" -eq 1 ] && [ "$1" != "-h" ] && $(validip $1)
    then;
        print_lat_lon=false;
    elif [ "$#" -eq 2 ] && [ "$2" = "-l" ] && $(validip $1)
    then;
        print_lat_lon=true;
    else;
        echo -e "getGeoLocFromIp (alias: glocip)\nReturns: reverse geocoded city and country of IP address\nUsage: getGeoLocFromIp <valid ipv4 address> [-h] [-l]\n -h: Print this usage stub\n -l: Additionally output latitude and longitude" >&2;
        return 1;
    fi;

    local ip="$1";

    local lat_lon=$(getLatLonFromIp ${ip});
    local stat=$?;
    if [ $stat -ne 0 ]; then return $stat; fi;

    local city_country=$(getCityCountryFromLatLon "${lat_lon}");
    local stat=$?;
    if [ $stat -ne 0 ]; then return $stat; fi;

    if [ "$print_lat_lon" = true ]
    then;
        echo "${city_country} [${lat_lon}]";
    else;
        echo "${city_country}";
    fi;

    return 0;
}

function getLatLonFromIp()
{
    if [ "$#" -ne 1 ] || [ $(validip $1) ]
    then;
        echo -e "Usage: getLatLonFromIp <valid ipv4 address>" >&2;
        return 1;
    fi;

    local ip="$1";
    local lat_lon=$(curl -s -m 1 "freegeoip.net/json/${ip}");

    if [ -z "${lat_lon}" ] || [[ "${lat_lon}" =~ Backend\ not\ available ]] || [[ ! "${lat_lon}" =~ .*latitude.* ]]
    then;
        echo -e "Failure to access geocoding API; either:\n - Network connectivity is compromised\n - API calls to freegeoip.net are being blocked\n - freegeoip.net have changed their API\nInvestigate." >&2;
        return 1;
    else;
        lat_lon="$(echo ${lat_lon} | jq -r '.latitude, .longitude')";
        lat_lon="$(echo ${lat_lon} | tr ' ' ',')";
    fi;

    echo "${lat_lon}";

    return 0;
}

function getCityCountryFromLatLon()
{
    local latlon="";

    if [ "$#" -eq 1 ] && [ -n "$1" ]
    then;
        latlon="$1";
    elif [ "$#" -eq 2 ] && [ -n "$1" ] && [ -n "$2" ]
    then;
        local lat="$1";
        local lon="$2";
        latlon="${lat},${lon}";
    else;
        echo -e "Usage: getCityCountryFromLatLon {<latitude> <longitude>|<latitude,longitude>}" >&2;
        return 1;
    fi;

    mapfile -t city_country < <(curl -G -k --data "latlng=$latlon&sensor=false" http://maps.googleapis.com/maps/api/geocode/xml 2>/dev/null | xmlstarlet sel -t -v '/GeocodeResponse/result[1]/address_component[type="political"][type="country"]/long_name' -n -t -v '/GeocodeResponse/result[1]/address_component[type="political"][type="locality"]/long_name');

    if [ "${#city_country[@]}" -lt 2 ]
    then;
        echo -e "Failed to parse geocoding API, either:\n - Network connectivity is compromised\n - API calls to maps.googleapis.com are being blocked\n - maps.googleapis.com has changed its API\nInvestigate." >&2;
        return 1;
    fi;

    local country="${city_country[0]}";
    local city="${city_country[1]}";

    echo "${city}, ${country}";

    return 0;
}

###############################################################################################
#       ALIASES                                                                               #
###############################################################################################

alias gloc='getGeoLocFromIp $(getExtIp)'
alias glocip='getGeoLocFromIp'
alias latlonip='getLatLonFromIp'

These should all be sourced in your $HOME/.bashrc or $HOME/.bash_profile initialisation scripts, for example:


$HOME/.bashrc :

if [ -d ~/.bash.d ]
then;
    . ~/.bash.d/.bash_aliases;
    . ~/.bash.d/.bash_vpn;
    . ~/.bash.d/.bash_parse;
    . ~/.bash.d/.bash_geo;
fi;

Then, running crontab -e as a regular user, the cron function in the .bash_vpn source file can be called (in this example, every 5 minutes) by inserting the following:

SHELL=/bin/bash
###############################################################
# m     h       dom     mon     dow     command
#==============================================================

# very frequent (every minute or 5)
*/5     *       *       *       *       . $HOME/.bash.d/.bash_vpn; iplogcron

Next, for the script to work completely, a couple of changes need to be made to your sudoers file, which should always be edited using the visudo command. Once this editor is opened, the following entries need to be inserted:

%vpnadmin       ALL=(root) NOPASSWD: /usr/sbin/openvpn
%vpnadmin       ALL=(ALL) NOPASSWD: /usr/sbin/service openvpn *
vpnuser         ALL=(ALL) NOPASSWD: /usr/sbin/service transmission-daemon *

%vpnadmin is a group that must be created - and then vpnuser is the user who will be running all these scripts, and they must be a member of the %vpnadmin group, this can be done with the following commands:

root@srv:~# adduser vpnuser
root@srv:~# addgroup vpnadmin
root@srv:~# usermod -a -G vpnadmin vpnuser

We will also need acl (Access Control List) utilities for manipulating extended attributes of /etc/openvpn and it’s contents (specifically allowing vpnuser to access them with elevated privileges). First:

root@srv:~# apt update
root@srv:~# apt install acl libacl

Then:

root@srv:~# cd /etc
root@srv:~# getfacl --all-effective openvpn
# file: openvpn/
# owner: root
# group: root
user::rwx
group::r-x
other::r-x

root@srv:~# setfacl -dm group:vpnadmin:rwx /etc/openvpn
root@srv:~# setfacl -dm group:vpnadmin:rwx /run/openvpn
root@srv:~# getfacl --all-effective openvpn
# file: openvpn/
# owner: root
# group: root
user::rwx
group::r-x                      #effective:r-x
group:vpnadmin:rwx              #effective:rwx
mask::rwx
other::r-x
default:user::rwx
default:group::r-x              #effective:r-x
default:group:vpnadmin:rwx      #effective:rwx
default:mask::rwx
default:other::r-x

At this point, everything is well set up. All that remains to be done is to populate your /etc/openvpn directory with a set of placename.ovpn files, where each placename maps directly to an element from the VPNLIST array declared at the top of $HOME/.bash.d/.bash_vpn:

VPNLIST=( france germany sweden switzerland luxembourg finland nl1 nl2 nl3 italy lithuania poland portugal romania spain coventry london portsmouth )

So, for instance, in my /etc/openvpn directory:

dev@srv:/etc/openvpn$ ls -l
total 84
-rwxrwx--- 1 root    root    2992 Sep  1 02:45 coventry.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:15 finland.ovpn
-rwxrwx--- 1 root    root    3099 Apr 10 02:31 france.ovpn
-rwxrwx--- 1 root    root    3110 Apr 10 02:32 germany.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:38 italy.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:38 lithuania.ovpn
-rwxrwx--- 1 root    root    2992 Sep  1 02:45 london.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:15 luxembourg.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:15 nl1.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:15 nl2.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:16 nl3.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:38 poland.ovpn
-rwxrwx--- 1 root    root    2992 Sep  1 02:46 portsmouth.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:39 portugal.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:39 romania.ovpn
-rwxrwx--- 1 root    root    2992 Jun 25 03:39 spain.ovpn
-rwxrwx--- 1 root    root    2981 Jun  8 04:51 sweden.ovpn
-rwxrwx--- 1 root    root    3014 Jun  8 04:52 switzerland.ovpn
lrwxrwxrwx 1 vpnuser vpnuser   25 Sep 13 19:44 vpnx.conf -> /etc/openvpn/poland.ovpn

And contained within each of these ovpn files are:

  • connection parameters, including:
  •     - cipher
        - auth
        - auth-user-pass /path/to/vpn.credentials
  • ca certificate (inside <ca> </ca> tags)
  • tls key (inside <tls-auth> </tls-auth> tags)

  • Thus each ovpn file is self-contained and contains all necessary information to create the openvpn connection.

    In the Linux shell, you can run:

    dev@srv:~$ fsvpn portugal
     > > > CHANGING VPN TO [portugal] :: index [12] > > >
    New VPN Location: Lisboa, Portugal :: [IP Address: 94.1.2.3]

    Finally, to use the geoCoding / location capabilities, you’ll need to install jq and xmlstarlet which are used to parse the API responses:

    dev@srv:~$ sudo apt update
    dev@srv:~$ sudo apt install jq xmlstarlet

    Then try out the functions by themselves:

    dev@srv:~$ gloc
    Stockholm, Sweden
    dev@srv:~$ glocip 8.8.8.8
    Mount Hope, United States

    Any questions, just drop a comment below.