Writing a bash script to connect to an OpenConnect VPN server
Greetings!

Today we’ll write and try out a simple but well-thought-out bash script that performs a client connection to an OpenConnect VPN server, which we covered setting up and running in one of the previous articles. As they say, “We need more bash!”. It’ll be interesting (or painful) 😉

Preface

Let me remind you once again that earlier we deployed our own VPN server based on the OpenConnect SSL based server (ocserv): Setting up an OpenConnect SSL VPN server (ocserv) in docker for internal projects.

And today I’ll demonstrate a client connection script that I wrote taking into account a useful article on Habr, the notorious ChatGPT, and a bit of trial and error.

This connection method is preferable on servers without a GUI, but it also works perfectly well on the desktop.

So, let’s start with a description of how it works.

Description of the script’s logic

It’s simple here. The script checks for the presence of the openconnect executable, then connects to the ocserv VPN server in the background and enters a mode of permanently waiting for/checking the availability of the internal VPN gateway. After 3 failed attempts to check the gateway’s availability via ping, the script terminates.

But since in our case the script will be started/autostarted using systemd, after an “emergency” termination of the script, systemd will restart it after 5 seconds. This way, uninterrupted operation of the connection script is assumed (but not guaranteed).

The script itself + systemd unit

Here is the content of the occlient.sh script:

BASH
#!/usr/bin/env bash

## Enhanced error handling
set -Eeuo pipefail

## Var that defines working directory of script
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)

## Custom vars
CERT_FILE="/path/to/exampleuser.p12"
CERT_PASS="examplepassword"
VPN_ADDRESS="vpn.example.com"
VPN_PORT="43443"
VPN_GATEWAY="10.10.10.1"
SSL_FLAG="1"

## System vars
OC_BIN="$(which openconnect)"
VPN_COMMAND="$OC_BIN -c $CERT_FILE $VPN_ADDRESS:$VPN_PORT"
CHECK_INTERVAL=10
TIMEOUT=30
RETRY_COUNT=3

## Function to print message
msg() {
    echo -e "[$(date '+%F %T')] ${1-}" >&2
}

## Function to terminate VPN command process
terminate_vpn() {
    msg "Terminating VPN connection" 
    pkill -SIGINT -f "${VPN_COMMAND}"
    exit 1
}

## Function to check availability of VPN gateway
check_gateway() {
    if ping -c 1 -W $TIMEOUT $VPN_GATEWAY &> /dev/null; then
        # msg "Gateway $VPN_GATEWAY is reachable."
        return 0
    else
        msg "Gateway $VPN_GATEWAY is not reachable."
        return 1
    fi
}

## Connecting to VPN
msg "Connecting to VPN..."
if [[ "$SSL_FLAG" == "1" ]]; then
    "$OC_BIN" -c "$CERT_FILE" "${VPN_ADDRESS}":"${VPN_PORT}" <<< "$(echo ${CERT_PASS}$'\n')" &
else
    "$OC_BIN" -c "$CERT_FILE" "${VPN_ADDRESS}":"${VPN_PORT}" <<< "$(echo ${CERT_PASS}$'\n'yes$'\n')" &
fi
# VPN_PID=$!

## Checking availability of gateway and exiting if there is no connection
while true; do
    FAILED_COUNT=0
    for (( i=0; i<RETRY_COUNT; i++ )); do
        if check_gateway; then
            break
        else
            FAILED_COUNT=$((FAILED_COUNT+1))
        fi
        if [[ $FAILED_COUNT -ge $RETRY_COUNT ]]; then
            msg "Gateway $VPN_GATEWAY unreachable after $RETRY_COUNT attempts."
            msg "Terminating VPN connection..."
            terminate_vpn
        fi
        sleep $CHECK_INTERVAL
    done
    sleep $CHECK_INTERVAL
done
Click to expand and view more

And the systemd service unit occlient.service:

PLAINTEXT
[Unit]
Description=OpenConnect VPN Client
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/path/to/occlient.sh
KillSignal=SIGINT
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
Click to expand and view more

The current version of the script is available in my repo on GitHub.

Next comes a breakdown of the script, but if you’re not interested in that, jump straight to the section « Installation and test run ».

Script breakdown

Block #1

BASH
## Enhanced error handling
set -Eeuo pipefail

## Var that defines working directory of script
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
Click to expand and view more

The first block sets special error-handling flags for the script’s execution. This is a recommendation. Also, a variable is defined here in a universal way that contains the path to the directory from which the script is run. Its presence is also optional, in case you need to extend the functionality.

Block #2

PLAINTEXT
## Custom vars
CERT_FILE="/path/to/exampleuser.p12"
CERT_PASS="examplepassword"
VPN_ADDRESS="vpn.example.com"
VPN_PORT="43443"
VPN_GATEWAY="10.10.10.1"
SSL_FLAG="1"
Click to expand and view more

This block defines user variables. This is the only block that absolutely must be modified before use. As you can see, the following values are defined here:

Please note that storing sensitive information, such as passwords, inside scripts is considered unsafe. In this article I’m only demonstrating how it works.

How to properly use passwords inside scripts is a topic for a separate post. I’ll definitely write one in the future.

Block #3

PLAINTEXT
## System vars
OC_BIN="$(which openconnect)"
VPN_COMMAND="$OC_BIN -c $CERT_FILE $VPN_ADDRESS:$VPN_PORT"
CHECK_INTERVAL=10
TIMEOUT=30
RETRY_COUNT=3
Click to expand and view more

This block defines service variables. There’s no need to change them.

Here we have:

Block #4

PERL
## Function to print message
msg() {
    echo -e "[$(date '+%F %T')] ${1-}" >&2
}
Click to expand and view more

This is a function for printing informational messages, with a prefix in the form of the current timestamp, and output redirection to the error stream >&2.

Block #5

PLAINTEXT
## Function to terminate VPN command process
terminate_vpn() {
    msg "Terminating VPN connection" 
    pkill -SIGINT -f "${VPN_COMMAND}"
    exit 1
}
Click to expand and view more

Here we have a function that terminates the client connection process. It’s implemented using the pkill command, which is passed the name of the connection command (VPN_COMMAND) as an argument.

On the topic of process management in Linux, the site also has a separate article: Linux command line, processes: jobs, fg, bg, ps, pgrep, kill, pkill, htop commands.

Block #6

BASH
## Function to check availability of VPN gateway
check_gateway() {
    if ping -c 1 -W $TIMEOUT $VPN_GATEWAY &> /dev/null; then
        # msg "Gateway $VPN_GATEWAY is reachable."
        return 0
    else
        msg "Gateway $VPN_GATEWAY is not reachable."
        return 1
    fi
}
Click to expand and view more

This is a function to check the availability of the VPN server’s internal gateway using the ping utility. On a successful check the function returns 0, otherwise 1. Makes sense)

Block #7

POWERSHELL
## Connecting to VPN
msg "Connecting to VPN..."
if [[ "$SSL_FLAG" == "1" ]]; then
    "$OC_BIN" -c "$CERT_FILE" "${VPN_ADDRESS}":"${VPN_PORT}" <<< "$(echo ${CERT_PASS}$'\n')" &
else
    "$OC_BIN" -c "$CERT_FILE" "${VPN_ADDRESS}":"${VPN_PORT}" <<< "$(echo ${CERT_PASS}$'\n'yes$'\n')" &
fi
# VPN_PID=$!
Click to expand and view more

This block prints a startup message, then checks the set SSL flag. Depending on its value, the corresponding branch is taken. In any case, the command to connect to the OpenConnect server is executed, after which, using the execution control & operator, it is sent to the background, passing control to the next part of the script’s code.

Note that there is a commented-out VPN_PID variable here, which (if uncommented) will contain the identifier (pid) of the last process sent to the background, i.e. our connection command. This pid can be used instead of VPN_COMMAND. It’s up to you.

Block #8

BASH
## Checking availability of gateway and exiting if there is no connection
while true; do
    FAILED_COUNT=0
    for (( i=0; i<RETRY_COUNT; i++ )); do
        if check_gateway; then
            break
        else
            FAILED_COUNT=$((FAILED_COUNT+1))
        fi
        if [[ $FAILED_COUNT -ge $RETRY_COUNT ]]; then
            msg "Gateway $VPN_GATEWAY unreachable after $RETRY_COUNT attempts."
            msg "Terminating VPN connection..."
            terminate_vpn
        fi
        sleep $CHECK_INTERVAL
    done
    sleep $CHECK_INTERVAL
done
Click to expand and view more

The last block is an infinite loop that checks the availability of the internal VPN gateway, and on a successful check, waits for a duration of CHECK_INTERVAL, after which it performs the check again.

If there’s no connectivity, the failed-attempts counter (FAILED_COUNT) will be incremented by one. If this counter’s value exceeds RETRY_COUNT, the script will call the connection termination function (terminate_vpn) and exit.

Installation and test run

For the connection, Ubuntu 22 will act as the client, and Debian 12 as the server.

Downloading the script

I want to point out right away that using the openconnect utility to connect requires superuser privileges.

To install the script you can copy it manually from this article, or download it from my GitHub, for example with this command:

BASH
sudo curl -fLo /root/occlient.sh https://raw.githubusercontent.com/r4ven-me/docker/main/openconnect/openconnect-src/occlient.sh
Click to expand and view more

It will copy the file to the root user’s home directory.

After downloading, the script needs to be given execute permissions:

BASH
sudo chmod 700 /root/occlient.sh
Click to expand and view more

I discussed file permissions in detail in the article: Linux command line, file permissions: id, chmod, chown commands.

Now let’s open the script for editing in any editor. I prefer Vim/Neovim:

BASH
sudo vim /root/occlient.sh
Click to expand and view more

My site also has a series of articles about this console editor. You can view the list by the corresponding tag: .

Let’s replace all the values in the Custom vars block with our own, save the file, and exit.

IMPORTANT!

Don’t forget to correctly set the SSL flag in the SSL_FLAG variable, so that no errors occur at startup. As a reminder, 1 — a valid SSL is used, 0 (or anything else) — it isn’t.

In my example a valid SSL certificate is used on the server, so the SSL_FLAG variable is set to 1.

Running the script

If you’re using a connection via the openconnect utility on a Linux server, and plan to tunnel all of its traffic, you need to add special routing rules, without which you’ll lose access to your server, including over SSH:

SQL
ip rule add table 128 from <public_ip>
ip route add table 128 to <public_ip_subnet> dev <ineteface_name>
ip route add table 128 default via <gateway>
Click to expand and view more

Where:

Let’s try to start it:

BASH
sudo /root/occlient.sh
Click to expand and view more

If you did all the steps correctly, you’ll see something like this:

The connection succeeded.

Let’s open a neighboring tab and check internet access:

BASH
ping -c3 8.8.8.8

ip route get 8.8.8.8
Click to expand and view more

Traffic goes through the virtual interface, everything’s correct.

You can check your external IP in the terminal with this command:

BASH
curl ifconfig.me
Click to expand and view more

Let’s go back to the tab with the script and press Ctrl+c to exit.

Enabling autostart with systemd

Now let’s configure autostart/restart for our script. To do this, download the systemd unit:

BASH
sudo curl -fLo /etc/systemd/system/occlient.service https://raw.githubusercontent.com/r4ven-me/docker/main/openconnect/openconnect-src/occlient.service
Click to expand and view more

Then let’s open it for editing:

BASH
sudo vim /etc/systemd/system/occlient.service
Click to expand and view more

And change the path to the script file in it:

Save, exit.

Let’s reload the systemd configuration and enable autostart for our service on system boot, and also start it:

BASH
sudo systemctl daemon-reload

sudo systemctl enable --now occlient

sudo systemctl status occlient
Click to expand and view more

Done.

To view the occlient.service unit’s output in real time, run:

BASH
sudo journalctl -fu occlient
Click to expand and view more

Afterword

So we’ve set up automatic connection/reconnection to the OpenConnect server with availability checking.

Once again I’ll note that this connection method is preferable on servers without a GUI. Because when using clients on the desktop, it’s better to use the NetworkManager plugin (if, of course, it’s responsible for your network). The connection procedure for such a client is described in the corresponding section of the ocserv setup article: 5.1 Configuring the OpenConnect client for Linux.

Thanks for reading. Enjoy the connection 😉

Useful sources

Copyright Notice

Author: Ivan Cherniy

Link: https://r4ven.me/en/automation/pishem-bash-skript-dlya-podklyucheniya-k-openconnect-vpn-serveru/

License: CC BY-NC-SA 4.0

Blog materials may be used with attribution to the author and source, for non-commercial purposes, and under the same license.

Start searching

Enter keywords to search articles

↑↓
ESC
⌘K Shortcut