pfSense: переключение туннеля OpenVPN

Даны два маршрутизатора. У обоих два провайдера. ТТК и РТК с обоих сторон. Когда связь падает скажем по ТТК на одном маршрутизаторе, то второй идет соединяться по интерфейсу РТК. Получается соединение ТТК -> РТК и связь по VPN сильно замедляется, а при нормальном подключении должно быть ТТК -> ТТК. У OpenVPN отсутствует возможность обратно переключиться к предыдущему серверу, когда он оживает, потому пришлось накидать скрипт. Надо учитывать, что данный скрипт не будет выживать при обновлении системы, потому лучше хранить где-нить в git.

Подготовительные работы

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

sockstat -4l | grep openvpn

Далее надо грепнуть по IP адресу ТТК интерфейса и убрать ненужные символы:

sockstat -4l | grep openvpn | grep <IP интерфейса ТТК> | wc -l | sed -E 's/^[[:space:]]*//' | tr -d '\n'

В итоге если клиент соединен по интерфейсу ТТК, то данная команда будет возвращать единицу и ноль в любом другом случае. Далее если соединение идет по интерфейсу РТК, то прежде чем перезапускать службу было бы неплохо проверить, а жив ли сервер. Тут рассматривал два варианта — это проверка доступности порта и пинг. В итоге остановился на пинге в 4 пакета поскольку тестирование порта может пройти успешно даже если на интерфейсе будут потери пакетов:

ping -S <IP интерфейса ТТК> -c 4 <внешний IP сервера> | grep 'packet loss' | sed -E 's:%::g' | awk '{print $7}' | cut -d. -f1 | tr -d '\n'

Кодинг

Накидаем скрипт, который будет запущен постоянно и каждые 5 секунд будет проводить проверки и в случае чего перезапускать OpenVPN. В итоге получилось нечто такое:

#!/usr/local/bin/python2.7

import sys
import time
import subprocess

def call(command):
        process = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell = True, universal_newlines = True)
        std_out, std_err = process.communicate()
        return process.returncode, std_out, std_err

def main():
        try:
                while True:
                        code, out, err = call("sockstat -4l | grep openvpn | grep <TTK interface IP> | wc -l | sed -E 's/^[[:space:]]*//' | tr -d '\n'")
                        if int(out) == 1:
                                print("OpenVPN: connection stable")
                        if int(out) != 1:
                                print("OpenVPN: connection unstable!!!")
                                code, out, err = call("ping -S <IP интерфейса ТТК> -c 4 <внешний IP сервера> | grep 'packet loss' | sed -E 's:%::g' | awk '{print $7}' | cut -d. -f1 | tr -d '\n'")
                                out.rstrip('\r\n')
                                if int(out) > 0:
                                        print("Packet loss: " + out + " %")
                                if int(out) == 0:
                                        print("OpenVPN: reloading...")
                                        call('/usr/local/sbin/pfSctl -c "service reload openvpn TTKGW"')
                        time.sleep(5)
        except KeyboardInterrupt:
                raise SystemExit("\nExit")

main()

TTKGW это название шлюза, которое можно посмотреть в Routing -> Gateways. Запускаем вручную и видим, что все работает. Но не будем же каждый раз вручную запускать. Значит надо накидать стартовый скрипт. Изучая уже имеющиеся в системе скрипты накидал такой вариант (/usr/local/etc/rc.d/vpn-reload.sh):

#!/bin/sh
# This file was automatically generated
# by the pfSense service handler.

rc_start() {
        nohup /root/bin/vpn-reload.py >/dev/null 2>&1 &

}

rc_stop() {
        PID=`cat /tmp/vpn-reload.pid`
        /bin/kill -9 ${PID}
        rm -f /tmp/vpn-reload.pid
}

case $1 in
        start)
                rc_start
                ;;
        stop)
                rc_stop
                ;;
        restart)
                rc_stop
                rc_start
                ;;
esac

Тут использовал утилиту nohup, чтобы скрипт в stdin не срал сообщениями (очень мешает), но сам nohup пишет лог и поэтому, чтобы не писал лог завернул его поток в /dev/null. Далее подумал, что понадобится сохранять Proccess ID скрипта, чтобы корректно убивать процесс при остановке службы (не дай бог убить чо нить лишнее). Обновляем код Python скрипта под новые реалии:

#!/usr/local/bin/python2.7

import os
import sys
import time
import subprocess

pid = str(os.getpid())
pidfile = "/tmp/vpn-reload.pid"

if os.path.isfile(pidfile):
        os.unlink(pidfile)

file(pidfile, 'w').write(pid)

def call(command):
        process = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell = True, universal_newlines = True)
        std_out, std_err = process.communicate()
        return process.returncode, std_out, std_err

def main():
        try:
                while True:
                        code, out, err = call("sockstat -4l | grep openvpn | grep <TTK interface IP> | wc -l | sed -E 's/^[[:space:]]*//' | tr -d '\n'")
                        if int(out) == 1:
                                print("OpenVPN: connection stable")
                        if int(out) != 1:
                                print("OpenVPN: connection unstable!!!")
                                code, out, err = call("ping -S <IP интерфейса ТТК> -c 4 <внешний IP сервера> | grep 'packet loss' | sed -E 's:%::g' | awk '{print $7}' | cut -d. -f1 | tr -d '\n'")
                                out.rstrip('\r\n')
                                if int(out) > 0:
                                        print("Packet loss: " + out + " %")
                                if int(out) == 0:
                                        print("OpenVPN: reloading...")
                                        call('/usr/local/sbin/pfSctl -c "service reload openvpn TTKGW"')
                        time.sleep(5)
        except KeyboardInterrupt:
                os.unlink(pidfile)
                raise SystemExit("\nExit")

main()

Делаем скрипты исполняемыми:

chmod +x /root/bin/vpn-reload.py
chmod +x /usr/local/etc/rc.d/vpn-reload.sh

И на этом все. Надеюсь кому-то будет полезно.

UPDATE

Что я могу сказать. Данный скрипт теперь уже подойдет либо в особо специфичных случаях либо как пример кодинга ибо вышел релиз pfSense 2.4.5 в котором указано "Fixed issues with OpenVPN resynchronizing when running on a gateway group #9595".