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