Пишем bash скрипт для подключения к OpenConnect VPN серверу

Пишем bash скрипт для подключения к OpenConnect VPN серверу

Приветствую!

Сегодня напишем и опробуем простой, но продуманный bash скрипт, выполняющий клиентское подключение к OpenConnect VPN серверу, про запуск и настройку которого рассказывалось в одной из прошлых статей. Как говориться, “Нам нужно больше bash’а!”. Будет интересно (или больно) 😉

Присоединяйтесь к нашему каналу в телеграм: t.me/r4ven_me, уведомления о новых постах приходят туда в день публикации. А если у вас есть вопросы или просто желание пообщаться по тематике – заглядывайте в чат: t.me/r4ven_me_chat.

Предисловие

Еще раз напомню, что ранее мы с вами развернули свой VPN сервер на базе OpenConnect SSL based сервера (ocserv): Поднимаем OpenConnect SSL VPN сервер (ocserv) в docker для внутренних проектов.

А сегодня продемонстрирую работу скрипта клиентского подключения, который я написал с учетом одной полезной статьи на Habr, пресловутого ChatGPT и какой-то там матери.

Данный способ подключения предпочтителен на серверах без GUI, но на десктопе также прекрасно работает.

И так, начнем с описания логики работы.

Описание логики работы скрипта

Тут всё просто. Скрипт проверяет наличие исполняемого файла openconnect, затем в фоновом режиме выполняет подключение к VPN серверу ocserv и переходит в режим перманентного ожидания/проверки доступности внутреннего шлюза VPN. В случае 3-х неудачных попыток проверки доступности шлюза по пингу, скрипт завершает свою работу.

Но т.к. в нашем случае запуск/автозапуск скрипта будет выполнятся с помощью systemd, то после “аварийного” завершения скрипта, systemd перезапустить его через 5 секунд. Таким образом, подразумевается (но не гарантируется ) бесперебойная работа скрипта подключения.

Сам скрипт + systemd unit

Вот содержимое скрипта occlient.sh:

#!/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

И systemd service unit occlient.service:

[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

Все файлы, как обычно, доступны в моём репо на GitHub.

Далее будет разбор скрипта, но если вам это не интересно, то сразу переходите к пункту “Установка и тестовый запуск“.

Разбор скрипта

Блок №1

## Enhanced error handling
set -Eeuo pipefail

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

В первом блоке устанавливаются специальные флаги обработки ошибок при работе скрипта. Является рекомендацией. Также тут универсальным образом задана переменная, которая содержит в себе путь до директории, из которой запускается скрипт. Её наличие тоже опционально, если вдруг вам понадобиться расширить функционал.

Блок №2

## 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"

В этом блоке определяются пользовательские переменные. Это единственный блок, который обязательно необходимо модифицировать перед использованием. Как видно, тут определяются следующие значения:

  • CERT_FILE – путь до файла пользовательского сертификата p12;
  • CERT_PASS – пароль от этого файла;
  • VPN_ADDRESS – адрес OpenConnect VPN сервера (ocserv);
  • VPN_PORT – порт подключения к серверу;
  • VPN_GATEWAY – адрес внутреннего VPN шлюза (необходим для проверки связи с сервером);
  • SSL_FLAG – переменная, определяющая использование валидного SSL. 1 – да, 0 – нет.

Прошу обратить внимание, что хранить чувствительную информацию, такую, как пароли, внутри скриптов считается не безопасным. В данной статье я лишь демонстрирую работу.

Как правильно использовать пароли внутри сценариев – это тема для отдельного поста. В будущем обязательно сделаю такой.

Блок №3

## 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

Этот блок определяет служебные переменные. Изменять их не требуется.

У нас тут:

  • Переменная OC_BIN, значение которой определяется результатом команды which openconnect. Если в системе не установлена утилита openconnect, то при определении переменной команда завершится ошибкой, что в свою очередь завершит работу всего скрипта;
  • Переменная VPN_COMMAND – используется функцией terminate_vpn (см. ниже) для идентификации процесса клиентского подключения, с целью его завершения;
  • Переменная CHECK_INTERVAL, определяющая период ожидания между попытками проверки связи с внутренним шлюзом VPN;
  • Переменная TIMEOUT – определяет время ожидания ответа при проверке связи с помощью утилиты ping в функции check_gateway (см. ниже);
  • RETRY_COUNT – количество проверок.

Блок №4

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

Это функция вывода информационных сообщений, с префиксом в виде текущей метки времени и перенаправлением вывода в поток ошибок >&2.

Блок №5

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

Тут у нас функция, выполняющая завершение процесса клиентского подключения. Реализована с помощью команды pkill, которой аргументом передается имя команды подключения (VPN_COMMAND).

По теме управления процессами в Linux на сайте также есть отдельная статья: Командная строка Linux, процессы: команды jobs, fg, bg, ps, pgrep, kill, pkill, htop.

Блок №6

## 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
}

Это функция проверки доступности внутреннего шлюза VPN сервера с помощью утилиты ping. При успешной проверке функция возвращает 0, в ином случае 1. Логично)

Блок №7

## 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=$!

В данном блоке выводится стартовое сообщение, затем выполняется проверка установленного флага SSL. В зависимости от его наличия, происходит соответствующее ветвление. В любом случае выполняется команда подключения к OpenConnect серверу, после чего, с помощью оператора контроля выполнения & она отправляется в фоновый режим, передавая работу следующему коду скрипта.

Обратите внимание, что тут присутствует закомментированная переменная VPN_PID, которая (если раскоментить) будет содержать в себе идентификатор (pid) последнего процесса, который был отправлен в фоновый режим, т.е. нашей команды подключения. Данный pid можно использовать вместо VPN_COMMAND. Тут уже на ваше усмотрение.

Блок №8

## 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

Последний блок представляет собой бесконечный цикл, который выполняет проверку доступности внутреннего шлюза VPN и в случае успешной проверки, переходит в режим ожидания длительностью в CHECK_INTERVAL, после чего вновь выполняет проверку.

В случае отсутствия связи, счетчик неудачных попыток (FAILED_COUNT) будет увеличиваться на единицу. Если значение этого счетчика превысит значение RETRY_COUNT, скрипт вызовет функцию остановки процесса подключения (terminate_vpn) и завершит работу.

Установка и тестовый запуск

При подключении клиентом будет выступать Ubuntu 22, а сервером Debian 12.

Скачивание скрипта

Хочу сразу обратить внимание, что при использовании подключения с помощью утилиты openconnect необходимы права суперпользователя.

Для установки скрипта вы можете скопировать его вручную из этой статьи или скачать из моего GitHub, например, такой командой:

sudo curl -fLo /root/occlient.sh https://raw.githubusercontent.com/r4ven-me/docker/main/openconnect/openconnect-src/occlient.sh

Она скопирует файл в домашнюю директорию пользователя root.

После скачивания, необходимо выдать скрипту права на исполнение:

sudo chmod 700 /root/occlient.sh

Про права на файлы я подробно рассказывал в статье: Командная строка Linux, права на файлы: команды id, chmod, chown.

Теперь открываем скрипт на редактирование любым редактором. Я предпочитаю Vim/Neovim:

sudo vim /root/occlient.sh

На моём сайте также есть цикл статей про этот консольный редактор. Посмотреть список можно по соответствующему тегу: Vim/Neoim.

Заменяем все значения в блоке Custom vars на свои, сохраняем файл и выходим.

ВАЖНО!

Не забудьте верно указать SSL флаг в переменной SSL_FLAG. Чтобы не возникло ошибок при запуске. Напомню, 1 – используется валидный SSL, 0 (или что угодно) – не используется.

В моём примере используется валидный SSL сертификат на сервере, поэтому переменная SSL_FLAG установлена в 1.

Запуск скрипта

Если вы используете подключение с помощью утилиты openconnect на Linux сервере, и планируете туннелировать весь его трафик, то необходимо добавить специальные правила маршрутизации, без которых вы потеряете доступ до своего сервера, в т.ч. и по SSH:

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>

Где:

  • 128 – имя новой таблицы маршрутизации;
  • <piblic-ip> – основной IP вашего сервера;
  • <public_ip_subnet> – подсеть основного IP сервера;
  • <interface_name> – имя физического интерфейса, к которому подключен основной IP;
  • <gateway> – шлюз сети, через который ходит основной IP.

Пробуем запуститься:

sudo /root/occlient.sh

Если вы все шаги проделали верно, то увидите примерно такую картину:

Подключение успешно.

Открываем соседнюю вкладку и проверяем доступ в интернет:

ping -c3 8.8.8.8

ip route get 8.8.8.8

Трафик идёт через виртуальный интерфейс, всё корректно.

Проверить внешний IP в терминале можно такой командой:

curl ifconfig.me

Возвращаемся на вкладку со скриптом и нажимаем Ctrl+c для выхода.

Активация автозапуска с помощью systemd

Теперь настроим автозапуск/перезапуск нашего скрипта. Для этого скачиваем systemd unit:

sudo curl -fLo /etc/systemd/system/occlient.service https://raw.githubusercontent.com/r4ven-me/docker/main/openconnect/openconnect-src/occlient.service

Затем открываем его на редактирование:

sudo vim /etc/systemd/system/occlient.service

И меняем в нём путь до файла скрипта:

Сохраняем, выходим.

Перечитываем конфигурацию systemd и активируем автозапуск нашего сервиса при старте системы, а также запускаем его:

sudo systemctl daemon-reload

sudo systemctl enable --now occlient

sudo systemctl status occlient

Готово.

Для просмотра вывода юнита occlient.service в реальном времени выполните:

sudo journalctl -fu occlient

Послесловие

Вот мы с вами и настроили автоматическое подключение/переподключение к OpenConnect серверу с проверкой доступности.

Вновь отмечу, что данный способ подключения предпочтителен на серверах без GUI. Потому что при использовании клиентов на десктопе, лучше заюзать плагин NetworkManager (если, конечно, за сеть у вас отвечает он). Процедура подключения такого клиента описана в соответствующем разделе статьи по настройке ocserv: 5.1 Настройка OpenConnect клиента для Linux.

Спасибо, что читаете. С подключением 😉

Полезные источники

Подписаться
Уведомить о
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии