Jump to content
Not connected, Your IP: 3.226.97.214
nwlyoc

Interactive Wrapper for Hummingbird (with boot script and default network lock)

Recommended Posts

Hello,
last year I had written a wrapper for Eddie's CLI version (in bash) to be able to use it more easily and extensively in the linux command line like the GUI, but with less resources. I have used it since then every day without problems, but now I have finally gotten to overhaul it and adjust it to Hummingbird because it is just so much faster! I also tried to make it more easy to configure (by having a separate configuration file) and added some new functionality like support (and automatic recognition) of iptables and nftables to lock down the system even without being connected to AirVPN and automatic connection at boot with a systemd unit.
Again, feel free to use this as you wish, I hope someone can benefit from this. I'm happy about any improvements and corrections and will update this if I find the time.

Features

  • graphical interface in the command line to connect to AirVPN with Hummingbird (no Eddie involved)
  • runs in background, the interface can be closed/opened anytime without affecting the running connection
  • possibility to connect to any server with just one ovpn configuration file
  • easily connect to a random server, to a recommended server, to the recommended server of a specific country or to a specific server
  • sortable list of all servers including info like used bandwidth, load and number of users
  • possibility to connect to other VPNs with openconnect
  • lock down system by default (permanently if you want), so even without AirVPN/Hummingbird running there won't be any unwanted network traffic
  • automatically establish connection at boot (which can later be controlled via the interface)
  • logging of Hummingbird's output (number of days to keep logs for can be adjusted)
  • system notifications to let you know what happens in the background

Some general notes
  • The default network lock determines, like Hummingbird itself, if iptables, iptables-legacy or nftables is available on your system and will use the first one found in that list. You can overwrite that by specifying which one to use in the configuration file. Once activated, the lock will stay in place until manually deactivated, so no internet connection will be possible unless connected to AirVPN or other whitelisted VPNs. You can make the lock permanent (or rather activate at boot) by enabling that option in the configuration file. AirVPN's network lock overwrites the default network lock, so there will be no interference.
  • IMPORTANT: If you have any frontend firewall for iptables/nftables running, you might to disable that or read up on how it might interfere with rule changes you make directly via iptables/nft. The same thing applies if you use just Hummingbird itself. If you enable the default permanent network lock, it will write the lock rules at boot, most likely overwriting rules by firewalld or the like, but other enabled firewalls might interfere later. Also important: If you have SELinux and you want to use nftables for Hummingbird starting at boot, you have to create a SELinux exception for nft bcause otherwise it will be denied and Hummingbird starts without setting up its own lock, thus leaving you unprotected (AirVPN staff is aware of this issue). You can do that with audit2allow. Follow for example this guide to troubleshoot the problem and fix it with the solution given by sealert.
  • Check your /etc/resolv.conf file while not running Hummingbird (because Hummingbird's network lock replaces that file temporarily) to make sure your router is not set as a nameserver (so no 192.168... address). Some routers will push themselves on that list by DHCP whenever you connect to their network. Since communication with the router is allowed in the lock rules, DNS requests will be handled by the router and sent to whatever DNS server is configured there even when network traffic should be blocked. There are ways to prevent that file from being changed by DHCP, best configure network manager for that if you use it.
  • To connect to other VPNs, their IPs must be whitelisted and DNS requests for their domains must be allowed in the default network lock rules (netfilter_ipbatles.rulesipv4/ipv6 and/or netfilter_nftables.rules). Only edit those files with the default network lock deactivated. The rules for airvpn.org can be copied and adjusted.
  • You can set custom options for Hummingbird in the interface or the configuration file. All the possible options can be found in the Hummingbird manual or with sudo hummingbird --help
  • Apart from dialog I tried to only use basic system tools. The scripts will check if everything needed is present, if not they will exit. At least bash 4 is needed. The scripts rely mostly on dialog, awk and curl (and iptables/nft as described and openconnect if needed), so it should work on most systems. I wrote and tested this on Fedora 32 with Hummingbird 1.0.3.
  • It should be possible to use any ovpn config file generated by the AirVPN's config generator. Even with the file for one specific server it should be possible to connect to any other server because the server override function is used here. I haven't tested that extensively though and just use the config file for earth.
  • AirVPN's API seems to be a little unreliable sometimes as in not correctly reporting the connection status. Sometimes the API reports me not being connected although I am connected to an AirVPN server. This is no big deal, it just means that the connection status sometimes may be shown falsely as disconnected. If you have the default network lock activated, no traffic would be possible if you were actually disconnected.
  • And, lastly, VERY IMPORTANT: I am still no programmer and do this only on this on the side, so even though I tried my best to make these scripts secure and error free, there might very well be some bad practice, never-ever-do-this mistakes or other hiccups in there. It works very well for me (and has for quite a while by now), but better check it yourself.

Installation
  • Make sure you have the prerequisites installed: dialog, bash >=4, curl and awk.
  • Copy the content of all the files to separate files on your computer, name them accordingly, and put them in the appropriate folders. It says where they belong above the file contents. Make sure to change the ownership of the systemd unit file to root:root and give the scripts execute permissions. Alternatively download the VPNControl.tar, cd into the directory where VPNControl.tar has been downloaded to and enter the following commands:
    tar -xvf VPNControl.tar
    mkdir -p "$HOME/.vpncontrol/config" && mkdir "$HOME/.vpncontrol/logs"
    mv vpncontrol.conf "$HOME/.vpncontrol/config/"
    mv netfilter_* "$HOME/.vpncontrol/config/"
    mv VPNControl.sh "$HOME/.local/bin/"
    sudo mv airvpn_boot.sh "/usr/local/bin/"
    sudo chown root:root airvpn.service
    sudo mv airvpn.service "/etc/systemd/system/"
  • Generate a config file with AirVPN's OpenVPN Config Generator (I use the one for "Earth", but theoretically it should work with any) and put it in the config directory. Adjust the path name in the configuration file.
  • The script assumes you have all the configuration files in the folder $HOME/.vpncontrol/config/, logs in $HOME/.vpncontrol/logs/ and the boot script in /usr/local/bin/. In the airvpn_boot.sh-script you have to adjust the path for the source command (line 34) to point to the configuration file. If you want to use different locations, you have to change them in the configuration file and (for the boot script) in the systemd unit file. If you want to use a different location for the configuration file itself, you have the change the VPNCONTROL_CONFIG variable in the VPNControl.sh-script and again the source path in the airvpn_boot.sh-script.
  • The VPNControl.sh-script is meant to be run as a regular user. If you want to run it as root, you have to for sure change the VPNCONTROL_CONFIG variable from the previous step. Otherwise it should be possible to run it as root without problems (except notifications), but I haven't tested it.
  • You need to insert your own API key in the configuration file. It can be found in your account under Client Area -> API. Without this, connections will still work, but user info and connection status in the main window will not be properly updated.
  • Enable the systemd unit with
    sudo systemctl daemon-reload
    sudo systemctl enable airvpn.service
  • If you use a setup where bash cannot be found at '/usr/bash', you have to change the path in the unit accordingly. Also I ran into a problem where systemctl complained that it couldn't find the unit. I don't know what the cause was (pretty sure the ownership and permissions were right), but it worked after I duplicated another .service file already present in /etc/systemd/system (with sudo cp) and then renamed it and exchanged the contents.
  • Disable firewall frontends if needed (e. g. firewalld) and if you want to use the default network lock. (Firewall daemons don't necessarily interfere, but have the ability to overwrite the lock any time.)
  • DONE! Now Hummingbird will try to establish a connection after boot. You can call the control script at any time with VPNControl.sh and disconnect/reconnect/do whatever. The script can be exited without affecting the running connection.

Files
These are all the necessary files (scripts and configuration files) with their default locations. Be aware that there might be a problem when manually copying the contents from here to text files where unwanted characters are inserted. If that's the case, please download the attached archive (at the very bottom of this post) and use the files from there. For the future I plan to put this somewhere more easily accessible like Gitlab.

This is the main script, the interface.
$HOME/.local/bin/VPNControl.sh
#!/bin/bash
# interactive shell script to control the command line version of the AirVPN Hummingbird client and openconnect more comfortably and extensively
# originally created in January 2019 for Eddie, updated for use with Hummingbird in June 2020
 
# check if at least bash 4 is used
if [ "${BASH_VERSINFO[0]}" -lt "4" ]
then
    echo "This sript can only be run with bash 4 or higher."
    exit
fi
 
# check if necessary programs are installed
PROGRAMS=( hummingbird dialog curl awk )
MISSING="false"
for p in "${PROGRAMS[@]}"
do
    command -v $p $> /dev/null
    if [ ! $? = "0" ]
    then
        echo "Please install $p to use this script."
        MISSING="true"
    fi
done
if [ "$MISSING" = "true" ]
then
    exit
fi
 
# check which network filter is available (determined NETFILTER will be overriden if set in config file)
NETFILTERS_AVAILABLE=( iptables iptables-legacy nft )
NETFILTER="none"
for n in "${NETFILTERS_AVAILABLE[@]}"
do
    command -v $n $> /dev/null
    if [ $? = "0" ]
    then
        NETFILTER="$n"
        break
    fi
done
 
# source variables which are subject to change from config file
VPNCONTROL_CONFIG="$HOME/.vpncontrol/config/vpncontrol.conf"
source "$VPNCONTROL_CONFIG"
 
# variables which probably won't have to be changed
DIALOG_OK=0
DIALOG_CANCEL=1
DIALOG_HELP=2
DIALOG_EXTRA=3
DIALOG_ITEM_HELP=4
DIALOG_ESC=255
HEIGHT=0
WIDTH=0
BACKTITLE="VPN Control"
FORMAT="text"
URL="https://airvpn.org/api/"
COLS=$( tput cols )
ROWS=$( tput lines )
PID=$$
# set network-lock argument for hummingbird depending on available backends
if [ "$NETFILTER" = "nft" ]
then
    NETFILTER_HUM="nftables"
elif [ "$NETFILTER" = "iptables" -o "$NETFILTER" = "iptables-legacy" ]
then
    NETFILTER_HUM="iptables"
else
    NETFILTER_HUM="on"
fi
 
function check_sudo {
# check if user has sudo privileges
sudo -vn &> /dev/null
# gain sudo privileges for commands that need it (better than running everything with sudo)
if [ $? = "1" ]
then
    unset EXIT_STATUS_SUDO
    #PASS_PROMPT="Establishing OpenVPN connections and checking and changing network traffic rules requires root privileges. Please enter your password:"
    until [ "$EXIT_STATUS_SUDO" = "0" ]
    do
        dialog \
        --backtitle "$BACKTITLE" \
        --title "Password Needed" \
        --output-fd 1 \
        --insecure \
        --passwordbox "$PASS_PROMPT" 11 35 | xargs printf '%s\n' | sudo -Svp '' &> /dev/null
        EXIT_STATUS_PIPE=( "${PIPESTATUS[@]}" )
        EXIT_STATUS_DIALOG="${EXIT_STATUS_PIPE[0]}"
        EXIT_STATUS_SUDO="${EXIT_STATUS_PIPE[2]}"
        EXIT_SUDO_TEST="${EXIT_STATUS_PIPE[2]}"
        PASS_PROMPT="The password you entered is incorrect. Please try again:"
        case $EXIT_STATUS_DIALOG in
            $DIALOG_CANCEL|$DIALOG_ESC)
                return 1
                ;;
        esac
    done
    # keep sudo permission until script exits or permissions are revoked (e.g. when computer goes to sleep)
    while [ "$EXIT_SUDO_TEST" = "0" ]; do sudo -vn; EXIT_SUDO_TEST=$?; sleep 60; kill -0 "$PID" || exit; done &> /dev/null &
fi
 
return 0
}
 
function get_list {
    SERVICE_NAME="status"
    timeout --signal=SIGINT 10 curl -s "$URL$SERVICE_NAME/?format=$FORMAT" > "/tmp/.airvpn_server_list.txt"
}
 
function sort_list_servers {
    # pipe server status list to awk, filter out unnecessary stuff,
    # combine lines that relate to same server into single lines which are saved as array,
    # loop through array to format info,
    # print array and sort according to options,
    # add numbers to list for menu
    LIST_SERVERS=$(awk -F '[.]' \
        'BEGIN{OFS=";"} \
        /^servers/ && !/ip_/ && !/country_code/ {c=$2; \
        if (c in servers) servers[c]=servers[c] OFS $3; \
        else servers[c]=$3; \
        for (k in servers) gsub(/;bw=/, "  :", servers[k]); \
        for (k in servers) gsub(/;bw_max=/, "/", servers[k]); \
        for (k in servers) gsub(/;currentload=/, "  :", servers[k]); \
        for (k in servers) gsub(/;health=/, "%:", servers[k]); \
        for (k in servers) gsub(/;.*=/, ":", servers[k]); \
        for (k in servers) gsub(/^.*=/, "", servers[k])} \
        END{for (c in servers) print servers[c]}' "/tmp/.airvpn_server_list.txt" | sort -t ":" $1)
        LIST_SERVERS=$( echo "$LIST_SERVERS" | sed 's/:/;/' )
}
 
function sort_list_countries {
    LIST_COUNTRIES=$(awk -F '[.]' \
        'BEGIN{OFS=";"} \
        /^countries/ && (/country_name/ || /country_code/) {c=$2; \
        if (c in countries) countries[c]=countries[c] OFS $3; \
        else countries[c]=$3; \
        for (k in countries) gsub(/;.*=/, ":", countries[k]); \
        for (k in countries) gsub(/^.*=/, "", countries[k])} \
        END{for (c in countries) print countries[c]}' "/tmp/.airvpn_server_list.txt" | sort -t ":" -d)
}
 
function get_userinfo {
    SERVICE_NAME="userinfo"
    # filter specific lines, save values (after "=") to variables after protecting whitespace
    read U_LOGIN U_EXP U_CONNECTED U_DEVICE U_SERVER_NAME U_SERVER_COUNTRY U_SERVER_LOCATION U_TIME <<< $( \
    timeout --signal=SIGINT 10 curl -s "$URL$SERVICE_NAME/?key=$API_KEY&format=$FORMAT" | \
    awk -F '[=]' \
        'BEGIN{ORS=";"} \
        /^user.login|^user.expiration_days|^user.connected|^sessions.*device_name|^connection.server_name|^connection.server_country=|^connection.server_location|^connection.connected_since_date/ \
        {print $2}' | \
    sed 's/\ /\\\ /g' | sed 's/;/\ /g' \
    )
    if [ "$U_CONNECTED" = "true" ]
    then
        U_CONNECTED="connected"
        U_SERVER_FULL="$U_SERVER_NAME ($U_SERVER_LOCATION, $U_SERVER_COUNTRY)"
        U_TIME=$(date -d "$U_TIME UTC" +"%a %d. %b %Y %H:%M:%S")
    else
        U_CONNECTED="not connected"
        U_SERVER_FULL="--"
        U_TIME="--"
    fi
}
 
function connect_server {
    if [ "$KILLED" = "true" ]
    then
        DATE=$( date +%Y%m%d )
        LOG_NAMES=($( ls "$LOG_PATH" | grep hummingbird.*log | sort -d ))
        LOG_NR=${#LOG_NAMES[@]}
 
        LOG_CURRENT="$LOG_PATH/hummingbird_current_$DATE.log"
        # if no log files should be kept, discard current logfile after process finishes, otherwise append to log file of current date
        if [ "$LOG_DAYS" = "0" ]
        then
            LOG_FINISH="/dev/null"
        else
            LOG_FINISH="$LOG_PATH/hummingbird_$DATE.log"
        fi
 
        if [ "$LOG_NR" -gt "0" ]
        then
            # check if newest log file is from today and if not, increase counter, so with the upcoming logfile the file limit will be kept
            if [ ! $( echo ${LOG_NAMES[-1]/#hummingbird_/} | cut -d "." -f 1 ) = "$DATE" ]
            then
                LOG_NR=$(( $LOG_NR+1 ))
            fi
 
            # check if more logs (including the upcoming one) are present than there should be and if so, remove oldest ones
            if [ "$LOG_NR" -gt "$LOG_DAYS" ]
            then
                cd "$LOG_PATH"
                rm "${LOG_NAMES[@]:0:(( $LOG_NR-$LOG_DAYS ))}"
                cd -
            fi
        fi
 
        # run hummingbird in background and detached from current window, write output to logfile, read it from there to dialog and catch sign of successful connection
        # hummingbird's timeout option is used, so it has enough time after sleep to recover without trying forever; TIMEOUT variable is used to try another server after some time when it is reasonable to not expect a successful connection anymore
        (sudo hummingbird $HUM_OPTIONS --network-lock "$NETFILTER_HUM" --timeout "$TIMEOUT_REC" --server "$1"."$DOMAIN" "$CONFIG_PATH" &> "$LOG_CURRENT"; notify-send "AirVPN" "Hummingbird process has finished."; sleep 1; cat "$LOG_CURRENT" >> "$LOG_FINISH"; rm "$LOG_CURRENT") & 
        tail -f -n 5 "$LOG_CURRENT"  | dialog --backtitle "$BACKTITLE" --title "Connecting to AirVPN  (Server: $1) ..." --progressbox 20 80 &
        tail -f -n 5 "$LOG_CURRENT"  | timeout --signal=SIGINT "$TIMEOUT_CON" grep -q -m 1 "EVENT: CONNECTED"
        INIT_EXIT=$?
        pkill -f tail.*hummingbird_current
        if [ "$INIT_EXIT" = "0" ]
        then
            sleep 1
            get_userinfo
            notify-send "AirVPN" "VPN connection successfully established to AirVPN's server $U_SERVER_FULL."
        else
            U_CONNECTED="error during connection attempt"
            U_SERVER_FULL="--"
            U_TIME="--"
            sudo pkill -2 hummingbird
            notify-send "AirVPN" "Connection attempt to an AirVPN server has failed."
            # need to wait long enough, so "current" log file is deleted before next connection attempt, otherwise file counter will be too high and delete other log files (takes arount +20ms, but sometimes more, so better to add 1s)
            sleep 2
        fi
    else
        U_CONNECTED="error during disconnection"
        U_SERVER_FULL="--"
        U_TIME="--"
    fi
}
 
function connect_openconnect {
    if [ "$KILLED" = "true" ]
    then
        DATE=$( date +%Y%m%d )
        LOG_NAMES=($( ls "$LOG_PATH" | grep openconnect.*log | sort -d ))
        LOG_NR=${#LOG_NAMES[@]}
 
        # if no log files should be kept, discard current logfile after process finishes, otherwise append to log file of current date
        if [ "$LOG_DAYS" = "0" ]
        then
            LOG_FINISH="/dev/null"
        else
            LOG_FINISH="$LOG_PATH/openconnect_$DATE.log"
        fi
 
        if [ "$LOG_NR" -gt "0" ]
        then
            # check if newest log file is from today and if not, increase counter, so with the upcoming logfile the file limit will be kept
            if [ ! $( echo ${LOG_NAMES[-1]/#openconnect_/} | cut -d "." -f 1 ) = "$DATE" ]
            then
                LOG_NR=$(( $LOG_NR+1 ))
            fi
 
            # check if more logs (including the upcoming one) are present than there should be and if so, remove oldest ones
            if [ "$LOG_NR" -gt "$LOG_DAYS" ]
            then
                cd "$LOG_PATH"
                rm "${LOG_NAMES[@]:0:(( $LOG_NR-$LOG_DAYS ))}"
                cd -
            fi
        fi
 
        ALT_SERVER=$(echo -n "$CONNECT_INFO" | cut -d$'\n' -f 1)
        ALT_GROUP=$(echo -n "$CONNECT_INFO" | cut -d$'\n' -f 2)
        ALT_USER=$(echo -n "$CONNECT_INFO" | cut -d$'\n' -f 3)
        ALT_PASS=$(echo -n "$CONNECT_INFO" | cut -d$'\n' -f 4)
        ALT_OPTS=$(echo -n "$CONNECT_INFO" | cut -d$'\n' -f 5)
        echo "$ALT_PASS" | (sudo openconnect $ALT_OPTS --authgroup=$ALT_GROUP --user=$ALT_USER --passwd-on-stdin $ALT_SERVER &> "$LOG_PATH/openconnect_current_$DATE.log"; notify-send "Openconnect" "Openconnect process has finished."; sleep 1; cat "$LOG_PATH/openconnect_current_$DATE.log" >> "$LOG_FINISH"; rm "$LOG_PATH/openconnect_current_$DATE.log") & 
        timeout --signal=SIGINT 3 tail -f -n 20 "$LOG_PATH/openconnect_current_$DATE.log" | dialog --backtitle "$BACKTITLE" --title "Connecting via openconnect ..." --timeout 5 --programbox 20 80
        U_CONNECTED="connected"
        U_SERVER_FULL="$ALT_SERVER"
        U_TIME=$(date +"%a %d. %b %Y %H:%M:%S")
    else
        U_CONNECTED="error during disconnection"
        U_SERVER_FULL="--"
        U_TIME="--"
    fi
}
 
function disconnect_server {
    # check for running instance of hummingbird
    HUM_PID=$( pgrep hummingbird )
    if [ $? = 0 ]
    then
        # kill process and wait for confirmation from process output
        # check if running instance of hummingbird is writing to logfile and if so, listen there for confirmation
        sudo ls -l "/proc/$HUM_PID/fd" | grep hummingbird_current &> /dev/null
        if [ $? = 0 ]
        then
            sudo pkill -2 hummingbird &
            tail -f -n 5 "$LOG_PATH/hummingbird_current_"* | dialog --backtitle "$BACKTITLE" --title "Disconnecting from AirVPN ..." --progressbox 20 80 &
            tail -f -n 5 "$LOG_PATH/hummingbird_current_"* | timeout --signal=SIGINT 3 grep -q -m 1 "Thread finished"
            pkill -f tail.*hummingbird_current
        else
            # in case connection was started without this script
            sudo pkill -2 hummingbird
            sleep 2
        fi
        # give some time to completely close process, without sleep it's too early for new connection
        sleep 1
        pgrep hummingbird &> /dev/null
        if [ $? = 1 ]
        then
            KILLED1="true"
            notify-send "AirVPN" "VPN connection has been stopped successfully."
        else
            KILLED1="false"
            notify-send "AirVPN" "An error has occured during the disconnection attempt."
        fi
    else
        KILLED1="true"
    fi
 
    # check for running instance of openconnect
    pgrep -f "openconnect.*--" &> /dev/null
    if [ $? = 0 ]
    then
        pkill -2 -f "openconnect.*--"
        sleep 1
        pgrep -f "openconnect.*--" &> /dev/null
        if [ $? = 1 ]
        then
            KILLED2="true"
            notify-send "AirVPN" "VPN connection to openconnect has been stopped successfully."
            # somehow openconnect doesn't receive SIGINT and shuts down improperly,
            # so vpnc can't restore resolv.conf by itself
            sudo cp "/var/run/vpnc/resolv.conf-backup" "/etc/resolv.conf"
        else
            KILLED2="false"
            notify-send "AirVPN" "An error has occured during the attempt to disconnect from openconnect."
        fi
    else
        KILLED2="true"
    fi
 
    if [ "$KILLED1" = "true" -a "$KILLED2" = "true" ]
    then
        KILLED="true"
    else
        KILLED="false"
    fi
}
 
function toggle_lock {
    if [ "$1" = "activate" ]
    then
        if [ "$NETFILTER" = "iptables-legacy" ]
        then
            sudo iptables-legacy-save  > "${NETFILTER_RULES_IPTABLES}ipv4.backup"
            sudo ip6tables-legacy-save > "${NETFILTER_RULES_IPTABLES}ipv6.backup"
            sudo iptables-legacy-restore < "${NETFILTER_RULES_IPTABLES}ipv4"
            sudo ip6tables-legacy-restore < "${NETFILTER_RULES_IPTABLES}ipv6"
        elif [ "$NETFILTER" = "iptables" ]
        then
            sudo iptables-save  > "${NETFILTER_RULES_IPTABLES}ipv4.backup"
            sudo ip6tables-save > "${NETFILTER_RULES_IPTABLES}ipv6.backup"
            sudo iptables-restore < "${NETFILTER_RULES_IPTABLES}ipv4"
            sudo ip6tables-restore < "${NETFILTER_RULES_IPTABLES}ipv6"
        elif [ "$NETFILTER" = "nft" ]
        then
            # put command to flush ruleset at top of backup file, so when it is loaded to restore the old rules, all previous rules are deleted in the same transaction (would take 2 transacions otherwise)
            echo "flush ruleset" > "${NETFILTER_RULES_NFTABLES}.backup"
            sudo nft list ruleset  >> "${NETFILTER_RULES_NFTABLES}.backup"
            sudo nft -f "${NETFILTER_RULES_NFTABLES}"
        fi
    elif [ "$1" = "deactivate" ]
    then
        if [ "$NETFILTER" = "iptables-legacy" ]
        then
            if [ -s "${NETFILTER_RULES_IPTABLES}ipv4.backup" ]
            then
                sudo iptables-legacy-restore < "${NETFILTER_RULES_IPTABLES}ipv4.backup"
                sudo rm "${NETFILTER_RULES_IPTABLES}ipv4.backup"
            else
                sudo iptables-legacy -F
                sudo iptables-legacy -t nat -F
            fi
            if [ -s "${NETFILTER_RULES_IPTABLES}ipv6.backup" ]
            then
                sudo ip6tables-legacy-restore < "${NETFILTER_RULES_IPTABLES}ipv6.backup"
                sudo rm "${NETFILTER_RULES_IPTABLES}ipv6.backup"
            else
                sudo ip6tables-legacy -F
                sudo ip6tables-legacy -t nat -F
            fi
        elif [ "$NETFILTER" = "iptables" ]
        then
            if [ -s "${NETFILTER_RULES_IPTABLES}ipv4.backup" ]
            then
                sudo iptables-restore < "${NETFILTER_RULES_IPTABLES}ipv4.backup"
                sudo rm "${NETFILTER_RULES_IPTABLES}ipv4.backup"
            else
                sudo iptables -F
                sudo iptables -t nat -F
            fi
            if [ -s "${NETFILTER_RULES_IPTABLES}ipv6.backup" ]
            then
                sudo ip6tables-restore < "${NETFILTER_RULES_IPTABLES}ipv6.backup"
                sudo rm "${NETFILTER_RULES_IPTABLES}ipv6.backup"
            else
                sudo ip6tables -F
                sudo ip6tables -t nat -F
            fi
        elif [ "$NETFILTER" = "nft" ]
        then
            if [ -s "${NETFILTER_RULES_NFTABLES}.backup" ]
            then
                sudo nft -f "${NETFILTER_RULES_NFTABLES}.backup"
                sudo rm "${NETFILTER_RULES_NFTABLES}.backup"
            else
                sudo nft flush ruleset
            fi
        fi
    else
        return 1
    fi
 
    check_lock
    if [ "$LOCK_ACTIVE" = "inactive" ]
    then
        dialog --backtitle "$BACKTITLE" --title "Default Network Lock Inactive" --msgbox "$MISSINGRULES" $HEIGHT $WIDTH
    elif [ "$LOCK_ACTIVE" = "active" ]
    then
        dialog --backtitle "$BACKTITLE" --title "Default Network Lock Active" --timeout 3 --msgbox "The default network lock is active." $HEIGHT $WIDTH
    else
        return 1
    fi
}
 
function check_lock {
    if [ "$NETFILTER" = "iptables-legacy" ]
    then
        # load rules from ruleset file into array (only -A rules (append) are loaded), prefix with iptables or ip6tables command as fitting, change -A (append) to -C (check)
        mapfile -t IPRULESIPV4 < <(grep -e "-A " "${NETFILTER_RULES_IPTABLES}ipv4" | sed -e 's/^\(.*\)/sudo iptables-legacy \1/' -e 's/\ -A / -C /')
        mapfile -t IPRULESIPV6 < <(grep -e "-A " "${NETFILTER_RULES_IPTABLES}ipv6" | sed -e 's/^\(.*\)/sudo ip6tables-legacy \1/' -e 's/\ -A / -C /')
        LOCK_ACTIVE="error while checking"
        # only checks for presence of rules, not for order; if not present in default table (filter), check in other tables
        MISSINGRULES="The following rules are not present:\n"
        for i in "${IPRULESIPV4[@]}"
        do
            eval "$i" &> /dev/null
            if [ ! $? = "0" ]
            then
                eval "${i/legacy/legacy -t nat}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/legacy/legacy -t mangle}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/legacy/legacy -t raw}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/legacy/legacy -t security}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                MISSINGRULES="$MISSINGRULES\nIPv4: $i"
                LOCK_ACTIVE="inactive"
            fi
        done
        for i in "${IPRULESIPV6[@]}"
        do
            eval "$i" &> /dev/null
            if [ ! $? = "0" ]
            then
                eval "${i/legacy/legacy -t nat}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/legacy/legacy -t mangle}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/legacy/legacy -t raw}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/legacy/legacy -t security}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                MISSINGRULES="$MISSINGRULES\nIPv6: $i"
                LOCK_ACTIVE="inactive"
            fi
        done
         
        if [ "$LOCK_ACTIVE" = "inactive" ]
        then
            MISSINGRULES="${MISSINGRULES//sudo iptables -C /}\n\nPlease check manually."
            MISSINGRULES="${MISSINGRULES//sudo ip6tables -C /}"
        else
            LOCK_ACTIVE="active"
        fi
    elif [ "$NETFILTER" = "iptables" ]
    then
        # load rules from ruleset file into array (only -A rules (append) are loaded), prefix with iptables or ip6tables command as fitting, change -A (append) to -C (check)
        mapfile -t IPRULESIPV4 < <(grep -e "-A " "${NETFILTER_RULES_IPTABLES}ipv4" | sed -e 's/^\(.*\)/sudo iptables \1/' -e 's/\ -A / -C /')
        mapfile -t IPRULESIPV6 < <(grep -e "-A " "${NETFILTER_RULES_IPTABLES}ipv6" | sed -e 's/^\(.*\)/sudo ip6tables \1/' -e 's/\ -A / -C /')
        LOCK_ACTIVE="error while checking"
        # only checks for presence of rules, not for order; if not present in default table (filter), check in other tables
        MISSINGRULES="The following rules are not present:\n"
        for i in "${IPRULESIPV4[@]}"
        do
            eval "$i" &> /dev/null
            if [ ! $? = "0" ]
            then
                eval "${i/tables/tables -t nat}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/tables/tables -t mangle}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/tables/tables -t raw}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/tables/tables -t security}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                MISSINGRULES="$MISSINGRULES\nIPv4: $i"
                LOCK_ACTIVE="inactive"
            fi
        done
        for i in "${IPRULESIPV6[@]}"
        do
            eval "$i" &> /dev/null
            if [ ! $? = "0" ]
            then
                eval "${i/tables/tables -t nat}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/tables/tables -t mangle}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/tables/tables -t raw}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                eval "${i/tables/tables -t security}" &> /dev/null
            fi
            if [ ! $? = "0" ]
            then
                MISSINGRULES="$MISSINGRULES\nIPv6: $i"
                LOCK_ACTIVE="inactive"
            fi
        done
        if [ "$LOCK_ACTIVE" = "inactive" ]
        then
            MISSINGRULES="${MISSINGRULES//sudo iptables -C /}\n\nPlease check manually."
            MISSINGRULES="${MISSINGRULES//sudo ip6tables -C /}"
        else
            LOCK_ACTIVE="active"
        fi
    elif [ "$NETFILTER" = "nft" ]
    then
        # only checks if named tables from netfilter config file are present
        NFT_LOCK_TABLES=$( sudo nft list ruleset | grep "_lock" | wc -l )
        if [ "$NFT_LOCK_TABLES" -ge "3" ]
        then
            LOCK_ACTIVE="active"
        else
            LOCK_ACTIVE="inactive"
            MISSINGRULES="The default network lock is deactivated. The nft tables with rules for the default network lock are not loaded."
        fi
    else
        return 1
    fi
}
 
function yesno {
dialog \
    --backtitle "$BACKTITLE" \
    --title "$1" \
    --clear \
    --yesno "$2" \
    $HEIGHT $WIDTH
EXIT_STATUS=$?
}
 
get_userinfo
# if currently connected by openconnect, set status to unknown (connection could have been established outside of this script)
pgrep -f "openconnect.*--" &> /dev/null
if [ $? = 0 ]
then
    U_CONNECTED="connected (openconnect)"
    U_SERVER_FULL="unknown"
    U_TIME="unknown"
fi
 
# set default message for network lock status, so password doesn't have to be entered when starting the script to check status
if [ "$NETFILTER" = "none" ]
then
    LOCK_ACTIVE="None of the supported network filters are available, so the default network lock cannot be used."
else
    LOCK_ACTIVE="Select option 8 to check lock status."
fi
 
while true; do
    exec 3>&1
    selection=$(dialog \
        --cr-wrap \
        --backtitle "$BACKTITLE" \
        --title "Main Menu" \
        --clear \
        --cancel-label "Quit" \
        --menu "This is a control script for VPN connections, primarily for AirVPN's Hummingbird client.\nThis script can be exited and re-entered without affecting a running connection.\n\nUser: $U_LOGIN\nDays Until Expiration: $U_EXP\n\nDefault Network Lock: $LOCK_ACTIVE\n\nStatus: $U_CONNECTED\nServer: $U_SERVER_FULL\nConnected Since: $U_TIME\n\nPlease select one of the following options:" $HEIGHT $WIDTH 9 \
        "0" "Connect to Recommended Server" \
        "1" "Connect to Recommended Server of Country" \
        "2" "Connect to Specific Server" \
        "3" "Connect to Random Server" \
        "4" "Set Options for Hummingbird" \
        "5" "Connect via Openconnect" \
        "6" "Disconnect" \
        "7" "Refresh User Info" \
        "8" "Check Default Network Lock Status" \
        "9" "Toggle Default Network Lock" \
        2>&1 1>&3)
    EXIT_STATUS=$?
    exec 3>&-
    case $EXIT_STATUS in
        $DIALOG_CANCEL|$DIALOG_ESC)
            yesno "Quit" "Exit Script?"
            case $EXIT_STATUS in
                $DIALOG_CANCEL|$DIALOG_ESC)
                    ;;
                $DIALOG_OK)
                    break
                    ;;
            esac
            ;;
    esac
    case $selection in
        0 )
            PASS_PROMPT="Connecting to and disconnecting from AirVPN with hummingbird requires root privileges. Please enter your password:"
            check_sudo
            if [ $? = "0" ]
            then
                disconnect_server
                #get_list
                DOMAIN="vpn.airdns.org"
                INIT_EXIT="1"
                connect_server "earth"
                if [ ! "$INIT_EXIT" = "0" ]
                then
                    count="1"
                    for s in "${SERVERS_BEST_EU[@]}"
                    do
                        connect_server "$s"
                        if [ "$INIT_EXIT" = "0" ]
                        then
                            break
                        else
                            (( count++ ))
                        fi
                        if [ "$count" -ge 5 ]
                        then
                            break
                        fi
                    done
                fi
                if [ ! "$INIT_EXIT" = "0" ]
                then
                    for s in "${SERVERS_BEST_REST[@]}"
                    do
                        connect_server "$s"
                        if [ "$INIT_EXIT" = "0" ]
                        then
                            break
                        else
                            (( count++ ))
                        fi
                        if [ "$count" -ge 7 ]
                        then
                            notify-send "AirVPN" "Connection unsuccessful after $count failed attempts."
                            break
                        fi
                    done
                fi
            fi
            ;;
        1 )
            if [ ! -s "/tmp/.airvpn_server_list.txt" ]
            then
                get_list
            fi
            while true
            do
                sort_list_countries
                IFS=$':\n'
                exec 3>&1
                COUNTRY_NAME=$(dialog \
                    --backtitle "$BACKTITLE" \
                    --title "Country List" \
                    --colors \
                    --no-collapse \
                    --column-separator ":" \
                    --menu "Choose a country from the list to connect to.\n\n\Zb              Country    Country Code\ZB" \
                    30 50 31 $LIST_COUNTRIES 2>&1 1>&3)
                EXIT_STATUS=$?
                exec 3>&-
                IFS=$' \t\n'
                case $EXIT_STATUS in
                    $DIALOG_CANCEL|$DIALOG_ESC)
                        break
                        ;;
                    $DIALOG_OK)
                        PASS_PROMPT="Connecting to and disconnecting from AirVPN with hummingbird requires root privileges. Please enter your password:"
                        check_sudo
                        if [ $? = "0" ]
                        then
                            SELECTED_COUNTRY=$(printf -- '%s\n' "${LIST_COUNTRIES[@]}" | grep "^$COUNTRY_NAME" | cut -d ":" -f 2 )
                            disconnect_server
                            DOMAIN="vpn.airdns.org"
                            connect_server "$SELECTED_COUNTRY"
                            break
                        fi
                        ;;
                esac
            done
            ;;
        2 )
            while true; do
            exec 3>&1
            SERVER_SORT=$(dialog \
                --backtitle "$BACKTITLE" \
                --title "Sort Server List" \
                --no-collapse \
                --ok-label "sort ascending" \
                --extra-button \
                --extra-label "sort descending" \
                --menu "Please choose how you want to sort the server list." \
                14 0 7 \
                "1" "Name" \
                "2" "Country" \
                "3" "Location" \
                "4" "Continent" \
                "5" "Bandwidth" \
                "6" "Users" \
                "7" "Load" \
                2>&1 1>&3)
            EXIT_STATUS=$?
            exec 3>&-
            case $EXIT_STATUS in
                $DIALOG_CANCEL|$DIALOG_ESC)
                    break
                    ;;
                $DIALOG_EXTRA)
                    SERVER_SORT_OPTION="r"
                    ;;
                $DIALOG_OK)
                    SERVER_SORT_OPTION=""
                    ;;
            esac
            if [ "$SERVER_SORT" = "5" -o "$SERVER_SORT" = "6" -o "$SERVER_SORT" = "7" ]
            then
                SERVER_NUM_OPTION="n"
            else
                SERVER_NUM_OPTION=""
            fi
            if [ ! -s "/tmp/.airvpn_server_list.txt" ]
            then
                get_list
            fi
            while true
            do
                sort_list_servers "-k$SERVER_SORT,$SERVER_SORT$SERVER_SORT_OPTION$SERVER_NUM_OPTION"
                IFS=$';\n'
                exec 3>&1
                SELECTED_SERVER=$(dialog \
                    --backtitle "$BACKTITLE" \
                    --title "Server List" \
                    --colors \
                    --no-collapse \
                    --extra-button \
                    --extra-label "Refresh List" \
                    --column-separator ":" \
                    --menu "Choose a server from the list to connect to it. (Press ESC to go back.)\n\n\Zb Name            Country        Location                Continent Bandwidth Users Load Health\ZB" \
                    40 102 31 $LIST_SERVERS 2>&1 1>&3)
                EXIT_STATUS=$?
                exec 3>&-
                IFS=$' \t\n'
                case $EXIT_STATUS in
                    $DIALOG_CANCEL)
                        break 2
                        ;;
                    $DIALOG_ESC)
                        break
                        ;;
                    $DIALOG_EXTRA)
                        get_list
                        ;;
                    $DIALOG_OK)
                        PASS_PROMPT="Connecting to and disconnecting from AirVPN with hummingbird requires root privileges. Please enter your password:"
                        check_sudo
                        if [ $? = "0" ]
                        then
                            disconnect_server
                            DOMAIN="airvpn.org"
                            connect_server "$SELECTED_SERVER"
                            break 2
                        fi
                        ;;
                esac
            done
            done
            ;;
        3 )
            PASS_PROMPT="Connecting to and disconnecting from AirVPN with hummingbird requires root privileges. Please enter your password:"
            check_sudo
            if [ $? = "0" ]
            then
                disconnect_server
                get_list
                INIT_EXIT="1"
                count="0"
                while [ ! "$INIT_EXIT" = "0" ]
                do
                    i="0"
                    while [ $i -le 20 ]
                    do
                        RAN_SERVER_NM=$( grep -E "servers\..+\.public_name" /tmp/.airvpn_server_list.txt | shuf -n1 | cut -d "." -f 2 )
                        RAN_SERVER_HEALTH=$( grep "servers\.$RAN_SERVER_NM\.health" /tmp/.airvpn_server_list.txt | cut -d "=" -f 2 )
                        if [ "$RAN_SERVER_HEALTH" = "ok" ]
                        then
                            RAN_SERVER=$( grep "servers\.$RAN_SERVER_NM\.public_name" /tmp/.airvpn_server_list.txt | cut -d "=" -f 2 )
                            break
                        fi
                        (( i++ ))
                    done
                    if [ "$i" -eq 20 ]
                    then
                        break
                    elif [ "$count" -ge 7 ]
                    then
                        notify-send "AirVPN" "Connection unsuccessful after $count failed attempts."
                        break
                    fi
                    DOMAIN="airvpn.org"
                    connect_server "$RAN_SERVER"
                    (( count++ ))
                done
            fi
            ;;
        4 )
            exec 3>&1
            HUM_OPTIONS=$(dialog \
                --backtitle "$BACKTITLE" \
                --title "Set custom Hummingbird options" \
                --extra-button \
                --extra-label "Make options permanent" \
                --form "If you want to use custom options for hummingbird, you can enter them here.\nType them like you would in the command line, separated by a space (e. g. --proto tcp --ignore-dns-push).\nNote that the options --timeout, --network-lock and --server are already used and can't be set here.\nThese options will override the ones you might have set in configuration file and will only be used for connections you make until you close the script. You can make them permanent with the button below (navigate with <TAB>)." $HEIGHT $WIDTH 5 \
                "Options:" 5 1 "$HUM_OPTIONS" 5 10 50 100 \
                2>&1 1>&3)
            EXIT_STATUS=$?
            exec 3>&-
            case $EXIT_STATUS in
                $DIALOG_CANCEL|$DIALOG_ESC)
                    ;;
                $DIALOG_EXTRA)
                    sed -i -e '/^HUM_OPTIONS/d' "$VPNCONTROL_CONFIG"
                    echo "HUM_OPTIONS=\"$HUM_OPTIONS\"" >> "$VPNCONTROL_CONFIG"
                    ;;
                $DIALOG_OK)
                    ;;
            esac
            ;;
        5 )
            exec 3>&1
            # adjust field lengths if necessary
            CONNECT_INFO=$(dialog \
                --backtitle "$BACKTITLE" \
                --title "VPN via openconnect" \
                --insecure \
                --mixedform "Please provide your login credentials to connect to a VPN via openconnect:\n(Leave unneeded fields blank and type options as in command line, separated by space.)" $HEIGHT $WIDTH 6 \
                "Server:" 1 1 "" 1 21 25 0 0 \
                "Group:" 2 1 "" 2 21 25 0 0 \
                "User:" 3 1 "" 3 21 25 0 0 \
                "Password:" 4 1 "" 4 21 25 0 1 \
                "Additional Options:" 5 1 "--no-dtls" 5 21 25 0 0 \
                2>&1 1>&3)
            EXIT_STATUS=$?
            exec 3>&-
            case $EXIT_STATUS in
                $DIALOG_CANCEL|$DIALOG_ESC)
                    ;;
                $DIALOG_OK)
                    PASS_PROMPT="Establishing OpenVPN connections requires root privileges. Please enter your password:"
                    check_sudo
                    if [ $? = "0" ]
                    then
                        disconnect_server
                        connect_openconnect
                    fi
                    ;;
            esac
            ;;
        6 )
            PASS_PROMPT="Disconnecting from AirVPN with hummingbird requires root privileges. Please enter your password:"
            check_sudo
            if [ $? = "0" ]
            then
                disconnect_server
                if [ "$KILLED" = "true" ]
                then
                        get_userinfo
                else
                        U_CONNECTED="error during disconnection"
                        U_SERVER_FULL="--"
                        U_TIME="--"
                fi
            fi
            ;;
        7 )
            get_userinfo
            ;;
        8 )
            if [ "$NETFILTER" = "none" ]
            then
                dialog --backtitle "$BACKTITLE" --title "Network Lock Not Available" --timeout 3 --msgbox "$LOCK_ACTIVE" 10 35
            else
                pgrep hummingbird &> /dev/null
                if [ $? = 0 ]
                then
                    dialog --backtitle "$BACKTITLE" --title "Check Default Network Lock" --timeout 8 --msgbox "Default network lock can only be checked when hummingbird is not running since it has it's own network lock overriding the default one." 10 35
                else
                    PASS_PROMPT="Checking network traffic rules requires root privileges. Please enter your password:"
                    check_sudo
                    check_lock
                    if [ "$LOCK_ACTIVE" = "inactive" ]
                    then
                        dialog --backtitle "$BACKTITLE" --title "Default Network Lock Inactive" --msgbox "$MISSINGRULES" $HEIGHT $WIDTH
                    elif [ "$LOCK_ACTIVE" = "active" ]
                    then
                        dialog --backtitle "$BACKTITLE" --title "Default Network Lock Active" --timeout 3 --msgbox "The default network lock is active." $HEIGHT $WIDTH
                    else
                        return 1
                    fi
                fi
            fi
            ;;
        9 )
            if [ "$NETFILTER" = "none" ]
            then
                dialog --backtitle "$BACKTITLE" --title "Network Lock Not Available" --timeout 3 --msgbox "$LOCK_ACTIVE" 10 35
            else
                pgrep hummingbird &> /dev/null
                if [ $? = 0 ]
                then
                    dialog --backtitle "$BACKTITLE" --title "Toggle Network Lock" --timeout 3 --msgbox "You need to be disconnected to change network traffic rules." 10 35
                else
                    check_lock
                    if [ "$LOCK_ACTIVE" = "inactive" ]
                    then
                        yesno "Toggle Network Lock" "Are you sure you want to activate the default network lock and block all connections while not connected to (any) VPN?"
                        case $EXIT_STATUS in
                            $DIALOG_CANCEL|$DIALOG_ESC)
                                ;;
                            $DIALOG_OK)
                                PASS_PROMPT="Changing network traffic rules requires root privileges. Please enter your password:"
                                check_sudo
                                if [ $? = "0" ]
                                then
                                    toggle_lock "activate"
                                fi
                                ;;
                        esac
                    elif [ "$LOCK_ACTIVE" = "active" ]
                    then
                        yesno "Toggle Network Lock" "Are you sure you want to deactivate the default network lock and allow all connections, even when not connected to a VPN?"
                        case $EXIT_STATUS in
                            $DIALOG_CANCEL|$DIALOG_ESC)
                                ;;
                            $DIALOG_OK)
                                PASS_PROMPT="Changing network traffic rules requires root privileges. Please enter your password:"
                                check_sudo
                                if [ $? = "0" ]
                                then
                                    toggle_lock "deactivate"
                                fi
                                ;;
                        esac
                    else
                        return 1
                    fi
                fi
            fi
            ;;
    esac
done
 
clear

This is the script that tries to establish a connection at boot.
/usr/local/bin/airvpn_boot.sh
#!/bin/bash
# script to connect to recommended AirVPN server, created to be used in systemd unit at boot
 
# check if necessary programs are installed
PROGRAMS=( hummingbird curl )
MISSING="false"
for p in "${PROGRAMS[@]}"
do
    command -v $p $> /dev/null
    if [ ! $? = "0" ]
    then
        MISSING="true"
    fi
done
if [ "$MISSING" = "true" ]
then
    exit
fi
 
# check which network filter is available (determined NETFILTER will be overriden if set in config file)
NETFILTERS_AVAILABLE=( iptables iptables-legacy nft )
NETFILTER="none"
for n in "${NETFILTERS_AVAILABLE[@]}"
do
    command -v $n $> /dev/null
    if [ $? = "0" ]
    then
        NETFILTER="$n"
        break
    fi
done
 
# source variables which are subject to change from config file
source "/home/<USER>/.vpncontrol/config/vpncontrol.conf"
 
# set network-lock argument for hummingbird depending on available backends
if [ "$NETFILTER" = "nft" ]
then
    NETFILTER_HUM="nftables"
elif [ "$NETFILTER" = "iptables" -o "$NETFILTER" = "iptables-legacy" ]
then
    NETFILTER_HUM="iptables"
else
    NETFILTER_HUM="on"
fi
 
# in order to correctly send notifications via notify-send as root, DISPLAY variable must be set (only on X, not on Wayland) and DBUS_SESSION_BUS_ADDRESS (automatically set based on username)
#DISPLAY=:0
USER_ID=$( id -u $SCRIPT_USER )
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus"
 
function activate_lock {
    # use detour with cat because SELinux denies direct read/write access for iptables and nft (actually nft was only denied write access, couldn't read for other permission reason, but this way it works)
    if [ "$NETFILTER" = "iptables-legacy" ]
    then
        iptables-legacy-save | cat  > "${NETFILTER_RULES_IPTABLES}ipv4.backup"
        ip6tables-legacy-save | cat > "${NETFILTER_RULES_IPTABLES}ipv6.backup"
        cat "${NETFILTER_RULES_IPTABLES}ipv4" | iptables-legacy-restore
        cat "${NETFILTER_RULES_IPTABLES}ipv6" | ip6tables-legacy-restore
    elif [ "$NETFILTER" = "iptables" ]
    then
        iptables-save | cat  > "${NETFILTER_RULES_IPTABLES}ipv4.backup"
        ip6tables-save | cat > "${NETFILTER_RULES_IPTABLES}ipv6.backup"
        cat "${NETFILTER_RULES_IPTABLES}ipv4" | iptables-restore
        cat "${NETFILTER_RULES_IPTABLES}ipv6" | ip6tables-restore
    elif [ "$NETFILTER" = "nft" ]
    then
        # put command to flush ruleset at top of backup file, so when it is loaded to restore the old rules, all previous rules are deleted in the same transaction (would take 2 transacions otherwise)
        echo "flush ruleset" > "${NETFILTER_RULES_NFTABLES}.backup"
        nft list ruleset  | cat >> "${NETFILTER_RULES_NFTABLES}.backup"
        cat "${NETFILTER_RULES_NFTABLES}" | nft -f -
    fi
}
 
function connect_server {
        DATE=$( date +%Y%m%d )
 
        # names and number of currently present logs
        LOG_NAMES=($( ls "$LOG_PATH" | grep hummingbird.*log | sort -d ))
        LOG_NR=${#LOG_NAMES[@]}
 
        # if no log files should be kept, discard current logfile after process finishes, otherwise append to log file of current date
        if [ "$LOG_DAYS" = "0" ]
        then
            LOG_FINISH="/dev/null"
        else
            LOG_FINISH="$LOG_PATH/hummingbird_$DATE.log"
        fi
 
        # check if newest log file is from today and if not, increase counter, so with the upcoming logfile the file limit will be kept and create final log file as user, so the user can write to it later
        if [ "$LOG_NR" -gt "0" ]
        then
            if [ ! $( echo ${LOG_NAMES[-1]/#hummingbird_/} | cut -d "." -f 1 ) = "$DATE" ]
            then
                LOG_NR=$(( $LOG_NR+1 ))
                if [ ! "$LOG_DAYS" = "0" ]
                then
                    su "$SCRIPT_USER" -c "touch $LOG_FINISH"
                fi
            fi
 
            # check if more logs (including the upcoming one) are present than there should be and if so, remove oldest ones
            if [ "$LOG_NR" -gt "$LOG_DAYS" ]
            then
                cd "$LOG_PATH"
                rm "${LOG_NAMES[@]:0:(( $LOG_NR-$LOG_DAYS ))}"
                cd -
            fi
        else
            su "$SCRIPT_USER" -c "touch $LOG_FINISH"
        fi
 
        su "$SCRIPT_USER" -c "notify-send 'AirVPN' 'Connecting to AirVPN ...'"
 
        # run hummingbird in background (and send notification when process finishes), pipe output to log
        (hummingbird $HUM_OPTIONS --network-lock "$NETFILTER_HUM" --timeout "$TIMEOUT_REC" --server "$1".vpn.airdns.org "$CONFIG_PATH" &>> "$LOG_PATH/hummingbird_current_$DATE.log"; su "$SCRIPT_USER" -c "notify-send.sh 'AirVPN' 'Hummingbird process has finished.'"; sleep 1; cat "$LOG_PATH/hummingbird_current_$DATE.log" >> "$LOG_FINISH"; rm "$LOG_PATH/hummingbird_current_$DATE.log") &
        # monitor log to catch sign of successful connection
        tail -f -n 5 "/$LOG_PATH/hummingbird_current_$DATE.log" | timeout --signal=SIGINT "$TIMEOUT_CON" grep -q -m 1 "EVENT: CONNECTED"
        INIT_EXIT=$?
        pkill -f tail.*hummingbird_current
 
        if [ "$INIT_EXIT" = "0" ]
        then
            # send notification as regular user for it to be sent and displayed correctly
            su "$SCRIPT_USER" -c "notify-send 'AirVPN' 'VPN connection successfully established.'"
            exit
        else
            pkill -2 hummingbird
            su "$SCRIPT_USER" -c "notify-send 'AirVPN' 'Connection attempt to an AirVPN server has failed.'"
            # need to wait long enough, so "current" log file is deleted before next connection attempt, otherwise file counter will be too high and delete other log files (takes around +20ms, but sometimes more, so better to add 1s)
            sleep 2
        fi
 
 
}
 
INIT_EXIT="1"
 
if [ "$DEFAULT_NETLOCK" = "enabled" ]
then
    activate_lock
fi
 
# try to connect to recommended servers (first EU, then rest of the world; change order/adjust server lists if desired)
connect_server "earth"
if [ ! "$INIT_EXIT" = "0" ]
then
    # count connection attempts in order to stop after certain number
    count="1"
    for s in "${SERVERS_BEST_EU[@]}"
    do
        connect_server "$s"
        if [ $INIT_EXIT = "0" ]
        then
            break
        else
            (( count++ ))
        fi
        if [ "$count" -ge 3 ]
        then
            break
        fi
    done
fi
if [ ! "$INIT_EXIT" = "0" ]
then
    for s in "${SERVERS_BEST_REST[@]}"
    do
        connect_server "$s"
        if [ $INIT_EXIT = "0" ]
        then
            break
        else
            (( count++ ))
        fi
        if [ "$count" -ge 5 ]
        then
            su "$SCRIPT_USER" -c "notify-send 'AirVPN' 'Connection unsuccessful after '$count' failed attempts."
            break
        fi
    done
fi
exit

This is the configuration file for both of the scripts. Most necessary adjustments can be made here, so the scripts don't have to be edited (except for correctly pointing at this file).
$HOME/.vpncontrol/config/vpncontrol.conf
#!/bin/bash
# This file is part of the VPNControl configuration.
# settings for AirVPN control scripts (airvpn_boot.sh and VPNControl.sh)
 
# user in whose directory all the necessary files are stored (usually you); this is just used for this configuration file to make paths easier to change, but paths can also be changed individually
SCRIPT_USER="<USER>"
 
# path to ovpn configuration file; make sure to use absolute path without variables like $HOME since boot script is run as root
CONFIG_PATH="/home/$SCRIPT_USER/.vpncontrol/config/AirVPN_All-servers_UDP-443.ovpn"
 
# path to directory for log files (don't put a trailing slash); make sure to use absolute path without variables like $HOME since boot script is run as root
LOG_PATH="/home/$SCRIPT_USER/.vpncontrol/logs"
 
# number of days for which logs are being kept (last days with connections via hummingbird, don't have to be consecutive); if "0" there will still be a log for the current connection which will be deleted after the connection ends
LOG_DAYS="3"
 
# seconds for which the connection to a server should be attempted before aborting (and when not trying to connect to a specific server moving on to the next one)
TIMEOUT_CON="12"
# seconds for which hummingbird should try to restore the connection in case connectivity is lost (mostly relevant after computer wakes up from sleep; this uses hummingbird's own --timeout option, but not sure how it handles it: if it applies to dropped VPN connection itself, network interface being down or only pausing the process e.g. by sleep)
TIMEOUT_REC="60"
 
# order of countries (and continents to try overall recommended server first) when trying recommended servers
SERVERS_BEST_EU=( europe nl be at bg ch cz de ee es gb lv no ro rs se ua )
SERVERS_BEST_REST=( america asia ca us jp br hk sg )
 
# backend for default network lock, will by default use (just like hummingbird) the first available of iptables, iptables-legacy or nft; uncomment if you want to use a specific one of those
#NETFILTER="nft"
 
# uncomment if you want to lock down the system by default (applies the default network lock at boot)
#DEFAULT_NETLOCK="enabled"
 
# path to file with rules for default network lock (needs to be present only for used backend, but both can be specified)
NETFILTER_RULES_IPTABLES="/home/$SCRIPT_USER/.vpncontrol/config/netfilter_iptables.rules"
NETFILTER_RULES_NFTABLES="/home/$SCRIPT_USER/.vpncontrol/config/netfilter_nftables.rules"
 
# API key to access user specific AirVPN info
API_KEY="<YOUR PERSONAL API KEY>"
 
# set custom options for hummingbird like in the commented example; will be temporarily overwritten if you enter new ones in the control script
#HUM_OPTIONS="--proto tcp --ignore-dns-push"

This is the systemd unit file that integrates the boot script into the system's boot process. It has to be owned by root.
/etc/systemd/system/airvpn.service
[Unit]
Description=AirVPN Client (hummingbird)
Wants=network-online.target
After=network-online.target
 
[Service]
Type=forking
ExecStart=/usr/bash /usr/local/bin/airvpn_boot.sh
Restart=no
 
[Install]
WantedBy=multi-user.target

These are the configuration files for the default network lock using iptables. These rules block all IPv4 traffic by default except some things like local traffic and traffic to airvpn.org. The rules in the second file block all IPv6 traffic.
$HOME/.vpncontrol/config/netfilter_iptables.rulesipv4
# This file is part of the VPNcontrol configuration.
# default network lock iptables rules for IPv4 traffic
# nat table: optional masquerade rule (NAT/ports)
*nat
:PREROUTING ACCEPT
:INPUT ACCEPT
:OUTPUT ACCEPT
:POSTROUTING ACCEPT
-A POSTROUTING -o tun+ -j MASQUERADE
COMMIT
 
# mangle table: no rules applied
*mangle
:PREROUTING ACCEPT
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
:POSTROUTING ACCEPT
COMMIT
 
# raw table: no rules applied
*raw
:PREROUTING ACCEPT
:OUTPUT ACCEPT
COMMIT
 
# security table: no rules applied
*security
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
COMMIT
 
# filter table: all traffic blocked with some exceptions:
*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
# allow loopback IN
-A INPUT -i lo -j ACCEPT
# allow broadcastin/dhcp IN
-A INPUT -s 255.255.255.255/32 -j ACCEPT
# allow communication for established connections (that were allowed with these rules)
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# block all other incoming traffic
-A INPUT -j DROP
# allow tun device to communicate (so any VPN connection should be possible, also without Air, but respective DNS requests must be allowed))
-A FORWARD -o tun+ -j ACCEPT
-A FORWARD -i tun+ -j ACCEPT
# allow loopback OUT
-A OUTPUT -o lo -j ACCEPT
# allow LAN OUT
-A OUTPUT -s 192.168.0.0/16 -d 192.168.0.0/16 -j ACCEPT
# allow link-local OUT
-A OUTPUT -s 169.254.0.0/16 -d 169.254.0.0/16 -j ACCEPT
# allow broadcastin/dhcp OUT
-A OUTPUT -d 255.255.255.255/32 -j ACCEPT
# allow IPv4 traffic via UDP and TCP only to airvpn.org for status update
#    allow DNS query to resolve hostname (hex string reads "06 airvpn 03 org" - numbers are counting bits),
#    restrict packet length to length of this specific request package (might change?) to avoid hijacking of query (very unlikely I guess, but who cares if we're already being paranoid for the fun of it),
#    whitelist destination IP for TCP handshake
-A OUTPUT -p udp -m udp --dport 53 -m string --hex-string "|0661697276706e036f7267|" --algo bm --to 65535 -m length --length 0:126 -m recent --set --name DEFAULT --mask 255.255.255.255 --rsource -j ACCEPT
-A OUTPUT -p tcp -m tcp --dport 53 -m string --hex-string "|0661697276706e036f7267|" --algo bm --to 65535 -m length --length 0:126 -m recent --set --name DEFAULT --mask 255.255.255.255 --rsource -j ACCEPT
#    allow SYN request to whitelisted IP to initiate handshake, remove IP from whitelist
-A OUTPUT -p tcp -m tcp --dport 53 --tcp-flags FIN,SYN,RST,ACK SYN -m recent --remove --name DEFAULT --mask 255.255.255.255 --rsource -j ACCEPT
#    allow outgoing connection to Air's IP
-A OUTPUT -d 5.196.64.52/32 -j ACCEPT
# allow tun device to communicate (so any VPN connection should be possible, also without Air, but respective DNS requests must be allowed))
-A OUTPUT -o tun+ -j ACCEPT
# block all other outgoing traffic
-A OUTPUT -j DROP
COMMIT

$HOME/.vpncontrol/config/netfilter_iptables.rulesipv6
# This file is part of the VPNcontrol configuration.
# iptables rules for IPv6 traffic
# nat table: no rules applied
*nat
:PREROUTING ACCEPT
:INPUT ACCEPT
:OUTPUT ACCEPT
:POSTROUTING ACCEPT
COMMIT
 
# mangle table: no rules applied
*mangle
:PREROUTING ACCEPT
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
:POSTROUTING ACCEPT
COMMIT
 
# raw table: no rules applied
*raw
:PREROUTING ACCEPT
:OUTPUT ACCEPT
COMMIT
 
# security table: no rules applied
*security
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
COMMIT
 
# filter table: block all traffic
*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
-A INPUT -j DROP
-A OUTPUT -j DROP
COMMIT

This is the configuration file for the default network lock using nftables. These rules block all IPv4 traffic by default except some things like local traffic and traffic to airvpn.org. They also block all IPv6 traffic.
$HOME/.vpncontrol/config/netfilter_nftables.rules
flush ruleset
table inet nat_lock {
    chain PREROUTING {
        type nat hook prerouting priority dstnat; policy accept;
    }
 
    chain INPUT {
        type nat hook input priority 100; policy accept;
    }
 
    chain OUTPUT {
        type nat hook output priority -100; policy accept;
    }
 
    chain POSTROUTING {
        type nat hook postrouting priority srcnat; policy accept;
        oifname "tun*" masquerade comment "optional masquerade rule (NAT/ports)"
    }
}
table ip filter_lock {
    set whitelist { type ipv4_addr; flags timeout;
    }
    chain INPUT {
        type filter hook input priority filter; policy drop;
        iifname "lo" accept comment "allow loopback IN"
        ip saddr 255.255.255.255/32 accept comment "allow broadcastin/dhcp IN"
        ct state established,related accept comment "allow communication for established connections (that were allowed with these rules)"
    }
 
    chain FORWARD {
        type filter hook forward priority filter; policy drop;
        oifname "tun*" accept
        iifname "tun*" accept
    }
 
    chain OUTPUT {
        type filter hook output priority filter; policy drop;
        oifname "lo" accept comment "allow loopback OUT"
        ip saddr 192.168.0.0/16 ip daddr 192.168.0.0/16 accept comment "allow LAN OUT"
        ip saddr 169.254.0.0/16 ip daddr 169.254.0.0/16 accept comment "allow link-local OUT"
        ip daddr 255.255.255.255/32 accept comment "allow broadcastin/dhcp OUT"
        # allow DNS query to resolve hostname (hex string reads "06 airvpn 03 org" (prefixed with 0x, suffixed with 00) - numbers are counting bits), whitelist destination IP for TCP handshake"
        udp dport 53 @th,160,120 0x0661697276706e036f726700 meta length 0-126 update @whitelist { ip saddr } accept comment "allow DNS query to resolve hostname"
        tcp dport 53 @th,160,120 0x0661697276706e036f726700 meta length 0-126 update @whitelist { ip saddr } accept comment "allow DNS query to resolve hostname"
        tcp dport 53 tcp flags & (fin|syn|rst|ack) == syn update @whitelist { ip saddr timeout 1s } accept comment "allow SYN request to whitelisted IP to initiate handshake, remove IP from whitelist"
        ip saddr @whitelist accept comment "allow outgoing traffic from addresses in whitelist"
        ip daddr 5.196.64.52/32 accept comment "allow outgoing connection to Air's IP"
        # allow tun device to communicate (so any VPN connection should be possible, also without Air, but respective DNS requests must be allowed)
        oifname "tun*" accept comment "allow tun device to communicate"
    }
}
table ip6 filter_lock {
    chain INPUT {
        type filter hook input priority filter; policy drop;
    }
 
    chain FORWARD {
        type filter hook forward priority filter; policy drop;
    }
 
    chain OUTPUT {
        type filter hook output priority filter; policy drop;
    }
}

VPNConrol.tar

 

 

 

 

 

 

 

 

 

VPNControl1.png

VPNControl2.png

VPNControl3.png

VPNControl4.png

VPNControl5.png

 

 

 

VPNControl.tar

Share this post


Link to post

Hi, and first of all thanks for sharing your work!

If I may comment a couple of thoughts:
 

3 hours ago, nwlyoc said:

AirVPN's API seems to be a little unreliable sometimes as in not correctly reporting the connection status. Sometimes the API reports me not being connected although I am connected to an AirVPN server.

This very same thing happened to me. hummingbirds logs showed some errors while this behavior. There's a hummingbirds beta version (that uses a newer openvpn version) that might have addressed this, but it's in beta stage yet.

For "Country list" and "Server list"  If you omit the numbering before the entries the first word that you use for each line will become the key of the line and you will be able to find things by writing (at least the first character of) the name of the server, or the name of the country or whatever word you put in first place, for faster moving through the menus.

For a later version you might want to offer some menus to chose the protocol and port to use too.

Nice job anyway!! thanks.

Share this post


Link to post

Thank you for your feedback! I added your suggestions.

On 6/14/2020 at 10:45 AM, eburom said:

For "Country list" and "Server list"  If you omit the numbering before the entries the first word that you use for each line will become the key of the line and you will be able to find things by writing (at least the first character of) the name of the server, or the name of the country or whatever word you put in first place, for faster moving through the menus.

I removed the numbers, you can now navigate by typing the server name.
 
On 6/14/2020 at 10:45 AM, eburom said:

For a later version you might want to offer some menus to chose the protocol and port to use too.

There is a menu now to set custom options. They will be used for connections made until you exit the script. There is also an option to make them permanent. Apart from that it's now possible to set custom options directly in the configuration file.

other UPDATES:
  • added permanent default network lock
    • Activating the default network lock in the interface will only have effect until reboot.
    • There is a new option in the configuration file to make the default network lock permanent: Lock rules will be written by the systemd unit at boot.
    • While working that out I noticed that when starting Hummingbird with the systemd unit and using nftables for the network lock, Hummingbird failed to set up its network lock. This was because of a SELinux denial for nft. So if you want to use that combination, you have to create an exception (more info in the original post).
  • fixed bug which led to deleting older log files while cycling through failing connection attempts

UPDATE 2:
  • fixed the country selection

Share this post


Link to post

Many thanks for the effort to get this script written.

2 3 4 5* issues I've encountered so far.

1) Cutting and pasting from the install instructions above doesn't work for some bizarre reason. It looks like some extra characters are being copied:

pi@pidown:~/.vpncontrol/config $ od -bc
sudo systemctl daemon-reload
0000000 163 165 144 157 357 273 277 040 163 171 163 164 145 155 143 164
          s   u   d   o 357 273 277       s   y   s   t   e   m   c   t
0000020 154 040 144 141 145 155 157 156 357 273 277 055 162 145 154 157
          l       d   a   e   m   o   n 357 273 277   -   r   e   l   o
Although I appreciate that this probably isn't your issue.

2) On my system, the VPNcontrol.sh initially complains about dialog not being installed. Possibly need to add this to the install instructions.

3) In the VPNcontrol.sh script, you set the variable VPNCONTROL_CONFIG to be "$HOME/...". If you run the script with sudo then that defaults to /root while you want it to be your user directory.

4) In airvpn.service you are making the assumption that bash lives at /usr/bin/bash. On my system (Ubuntu) it's in /bin/bash.

5) When I try to start the script as a service I get the error message:
Hummingbird - AirVPN OpenVPN 3 Client 1.1.0 - 23 June 2020

Thu Jun 25 10:47:50.082 2020 System and service manager in use is systemd
ERROR: --network-lock option must be on, iptables, nftables, pf or off

* No-one expects the Spanish Inquisition

Share this post


Link to post

Thanks for the feedback!
 

13 hours ago, dL4l7dY6 said:
2 3 4 5* issues I've encountered so far.
I'm surprised and afraid that there were so many!
 
13 hours ago, dL4l7dY6 said:

1) Cutting and pasting from the install instructions above doesn't work for some bizarre reason. It looks like some extra characters are being copied:

That's indeed bad. Probably wouldn't happen if I inserted it as a code block, but I used a spoiler block because only those are collapsable and without that the post would have been very long to scroll past. And it seems you can only download the attached file when you're logged into this forum. I'll put everything on Gitlab or something when I have the time.
 
13 hours ago, dL4l7dY6 said:

2) On my system, the VPNcontrol.sh initially complains about dialog not being installed. Possibly need to add this to the install instructions.

I put this under the 'basic system tools', but it really isn't one. I'll add it to the instructions.
 
13 hours ago, dL4l7dY6 said:

3) In the VPNcontrol.sh script, you set the variable VPNCONTROL_CONFIG to be "$HOME/...". If you run the script with sudo then that defaults to /root while you want it to be your user directory.

I forgot to mention that this script was meant to be run as a regular user, didn't think of the possible need to run it as root. But good point, I'll point that out. Otherwise it should run fine as root except the notifications maybe (and all the sudo stuff wouldn't be needed anymore, but that shouldn't be a problem).
 
13 hours ago, dL4l7dY6 said:

4) In airvpn.service you are making the assumption that bash lives at /usr/bin/bash. On my system (Ubuntu) it's in /bin/bash.

I wasn't aware that might differ. I looked it up and it seems '/bin/bash' should be the best way to specify it (also better than '/usr/bin/env bash' it seems). Changed it.
 
13 hours ago, dL4l7dY6 said:

5) When I try to start the script as a service I get the error message: 


Hummingbird - AirVPN OpenVPN 3 Client 1.1.0 - 23 June 2020

Thu Jun 25 10:47:50.082 2020 System and service manager in use is systemd
ERROR: --network-lock option must be on, iptables, nftables, pf or off

 

Damn that was a bad typo. It only happens when custom options are set and only with the airvpn_boot.sh-script. On line 116 '$HUM_OPTIONS' needs to be before '--network-lock'. I changed it above and in the .tar-file.

Share this post


Link to post

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Security Check
    Play CAPTCHA Audio
    Refresh Image

×
×
  • Create New...