Переключения между провайдерами интернета на Debian 7

Источник: habrahabr
woooody

В последних дистрибутивах Linux довольно много всяких полезных плюшек в папке /etc, однако мало кто ими грамотно пользуется. Здесь я расскажу про часть из них применительно к последней стабильной версии Debian, и приведу пример реализации переключения на резервный канал.
Все попытки найти банальную автопереключалку приводили к связке из 1-2 скриптов, написанным "под себя" и мало поддающимися настройке. К dhcp не был привязан ни один из них, а значит любые манипуляции на стороне провайдера требовали вмешательство в настройки. Сам писал такие в свое время, но вот теперь решил на новой системе оформить это красиво - так, как это задумывали разработчики Debian, а именно - меняем файлы конфигурации, добавляем свои скрипты и не трогаем те, что нам предоставила система.

Итак, имеем:
- два кабеля от двух провайдеров, оба выдают IP по dhcp
- свежесобранный сервер под управлением debian wheezy с тремя сетевухами (возможно потом добавлю еще)
- желание чтоб инет не пропадал (работа не ждет!). Балансировку и т.п. оставим на потом.

Логика на первый взгляд простая:
- пингуем какой-нибудь хост по очереди через разные интерфейсы
- если пинг нестабильный, переключаемся на резервный канал.
Вот только в реализации была поставлена цель сделать всё максимально гибко, например не ограничивать количество потенциальных провайдеров и впоследствии делать минимум телодвижений для перенастройки.
Для начала посмотрим какие плюшки уже есть в системе
В папке /etc/network нас интересует файл interfaces и папки if-down.d, if-post-down.d, if-pre-up.d, if-up.d.
root@ns:/etc/network# cat interfaces # This file describes the network interfaces available on your system # and how to activate them. For more information, see interfaces(5). # The loopback network interface iface lo inet loopback # The primary network interface iface eth0 inet static address 192.168.104.1 netmask 255.255.255.0 iface eth1 inet dhcp iface eth3 inet dhcp
При манипуляциях с интерфейсами через ifup/ifdown на папочки натравливается run-parts, и скриптам в них доступны следующие переменные окружения: IFACE, LOGICAL, ADDRFAM, METHOD, MODE, PHASE, MODE, VERBOSITY, PATH
При старте системы сначала запускаются скрипты из папки if-pre-up.d (по одному разу для каждого интерфейса, но перед ними идет IFACE="--all", потом поднимаются интерфейсы и запускаются скрипты из папки if-up.d и IFACE="--all" идет уже в конце. При ifup ethX запускается по одному разу только для ethX (без "-all"). Аналогично if-down.d и if-post-down.d при ifdown и выключении системы.
Система позволяет назначить для каждой операции и для каждого интерфейса свой скрипт, но вносить похожие изменения каждый раз в 10 из 20 скриптов в мои планы не входило, поэтому будем писать один большой скрипт и расставим на него симлинки изо всех четырех папок. Понять, откуда он запущен, можно по переменным окружения.

Однако нам еще надо узнать информацию о шлюзах, которая пришла по dhcp. На этот случай тоже есть папки со скриптами /etc/dhcp/dhclient-enter-hooks.d и /etc/dhcp/dhclient-exit-hooks.d
Последовательность запуска такая:
- опросили сервер dhcp
- запустили содержимое папки dhclient-enter-hooks.d
- настроили сетевые параметры (ip, DNS, шлюз,…)
- запустили содержимое папки dhclient-exit-hooks.d
Скриптам тоже доступны разные полезные переменные (параметры, которые пришли от dhcp сервера), часть из которых нам надо будет сохранять.

После нескольких вечеров получилось следующее:
Все скрипты лежат в папке /etc/network/scripts. Из разных папок туда ведут симлинки
Настройки - /etc/default/network-scripts
Временые файлы кладем /var/lib/dhcp
Логи пишутся в файл /var/log/network-scripts.log
Настройки
# cat /etc/default/network-scripts # Configuration file for /etc/network/scripts/* # Host to ping for autoroute HOST_TO_PING="4.2.2.1" # Number of pings to check the connection PING_COUNT=5 # list of LAN interfaces IFLAN="eth0" # WAN prio from first to last IFWAN="eth1 eth3" # open ports from WAN zone WAN_PORTS_OPEN=""
Тут всё должно быть понятно. Интерфейсы LAN, WAN можно дописывать сколько угодно. В списке WAN первый - самый приоритетный, далее по убыванию.
Отдельно файл с функциями.
# cat /etc/network/scripts/functions #!/bin/sh DHCPLIB="/var/lib/dhcp" LOGDIR="/var/log" LOGFILE="$LOGDIR/network-scripts.log" HOST_TO_PING="4.2.2.1" PING_COUNT=3 SQUID_PORT="3128" IFLAN="" IFWAN="" WAN_PORTS_OPEN="" . /etc/default/network-scripts # Local variables DEFAULTWAN=${IFWAN% *} log() { DATE=`date` echo "$DATE $@" >> $LOGFILE } warn() { log "WARNING: $@" echo "WARNING: $@" } cmd() { $@ RES=$? log "$RES - $@" return $RES } get_ip() { IP=`ip addr list $1 / grep " inet " / head -n 1 / cut -d " " -f 6 / cut -d / -f 1` } update_local_redirect() { for i in $IFLAN; do cmd iptables -t nat $INS PREROUTING -i $i -p tcp --dport 80 -d $1 -j ACCEPT done } update_squid() { case $1 in start) ADD="-A" INS="-I" ;; stop) ADD="-D" INS="-D" ;; *) ADD="-C" INS="-C" esac for i in $IFLAN; do # transparent proxy cmd iptables -t nat $ADD PREROUTING -i $i -p tcp --dport 80 -j REDIRECT --to-port $SQUID_PORT done }
Тут мы имеем:
- импорт настроек из /etc/default/network-scripts
- ведение логов (log, warn),
- запуск команд с записью в лог параметров и результатов работы
- update_local_redirect() добавляет маршруты на 80 порт мимо transparent proxy
- update_squid() добавляет правило для самого transparent proxy (запускается в /etc/init.d/squid3 - это единственный системный скрипт, в который пришлось влезть)
Тут и далее используется технология, придуманная мной несколько лет назад с переменными $ADD и $INS для iptables. Позволяет писать правило только в одном месте, и потом его добавлять-удалять, изменяя только эти переменные.
# cat /etc/network/scripts/route-enter #!/bin/sh . /etc/network/scripts/functions log "$0 route-enter ${interface} ${reason} ${new_routers}" # security bugfix new_host_name=${new_host_name//[^-.a-zA-Z0-9]/} # save routers to special file echo -n ${new_routers} > $DHCPLIB/routers.${interface} echo -n ${new_ip_address} > $DHCPLIB/ip_address.${interface} case ${interface} in $DEFAULTWAN) # by default enable routers only for first WAN interface ;; *) # and clear it for others unset new_routers ;; esac
- Сохраняем new_routers и new_ip_address в файл (потом понядобятся)
- default route разрешаем только для приоритетного интерфейса

# cat /etc/network/scripts/route-exit #!/bin/sh . /etc/network/scripts/functions log "$0 route-exit ${interface} ${reason}" update_routes() { cmd route $ADD -host $HOST_TO_PING gw ${routers} # identyfy providers by DNS addresses case $DNS in *82.193.96*) DESTIP=`resolveip -s stat.ipnet.ua` cmd route $ADD -host $DESTIP gw ${routers} ;; *193.41.63*/*192.168.11.1*) DESTIP=`resolveip -s my.kyivstar.ua` cmd route $ADD -host $DESTIP gw ${routers} ;; *) warn "route-exit - unknown DNS ${new_domain_name_servers} specified" ;; esac } case ${reason} in BOUND) ADD="add" DNS=${new_domain_name_servers} # use saved-to-file value due to $old_routers can be cleared for some interfaces by other script routers=`cat $DHCPLIB/routers.${interface}` update_routes ;; RELEASE) # No need to delete routes during release # ADD="del" # routers=${old_routers}` # update_routes ;; PREINIT) ;; RENEW) if [ "$old_routers" != "$new_routers" ]; then ADD="del" DNS=${old_domain_name_servers} routers=${old_routers} update_routes ADD="add" DNS=${new_domain_name_servers} routers=`cat $DHCPLIB/routers.${interface}` update_routes fi if [ "$old_ip_address" != "$new_ip_address" ]; then ADD="-D" INS="-D" update_local_redirect ${old_ip_address} ADD="-A" INS="-I" update_local_redirect ${new_ip_address} fi ;; *) warn "route-exit - unknown reason ${reason} used" ;; esac
- Добавляем static route для сайтов с биллингом провайдеров. Локалка провайдера мне не нужна, но её тоже можно добавить. Идентификация по DNS серверам.
- для режима RENEW добавил перенастройку (если вдруг у провайдера что-то изменится), но пока не тестировал.

# cat /etc/network/scripts/firewall #!/bin/bash . /etc/network/scripts/functions get_ip $IFACE log "$0 $IFACE $LOGICAL $ADDRFAM $METHOD $MODE $PHASE $VERBOSITY $IP" case $MODE in start) INS="-I" ADD="-A" echo -n $IP > $DHCPLIB/ip_address.$IFACE ;; stop) INS="-D" ADD="-D" echo -n > $DHCPLIB/ip_address.$IFACE ;; *) INS="-C" ADD="-C" warn "Wrong MODE:$MODE" ;; esac case $IFACE in --all) case $PHASE in pre-down/post-up) # skip proxy for local addresses for j in $IFLAN $IFWAN; do get_ip $j update_local_redirect $IP done ;; post-up/pre-down) ;; esac ;; lo) ;; *) if [[ "$IFLAN" == *$IFACE* ]]; then # LAN case $PHASE in pre-up/post-down) cmd iptables $INS INPUT -p tcp -i $IFACE --dport 22 -j ACCEPT ;; post-up/pre-down) ;; *) warn "Wrong PHASE:$PHASE" ;; esac fi if [[ "$IFWAN" == *$IFACE* ]]; then # WAN case $PHASE in pre-up/post-down) # by default close all input connections cmd iptables $ADD INPUT -p tcp -i $IFACE --dport 1:10000 -j DROP cmd iptables $ADD INPUT -p udp -i $IFACE --dport 1:10000 -j DROP # open ports from list for PORT in $WAN_PORTS_OPEN; do cmd iptables $INS INPUT -p tcp -i $IFACE --dport $PORT -j ACCEPT done ;; post-up/pre-down) cmd iptables -t nat $ADD POSTROUTING -o $IFACE -j MASQUERADE ;; *) warn "Wrong PHASE:$PHASE" ;; esac fi ;; esac
Правила firewall. Для общих таблиц пишем в момент pre-up и post-down, для NAT - в post-up и pre-down.

# cat /etc/network/scripts/autoroute #!/bin/sh # Script for cron to monitor WAN interfaces # and (in future) SQUID status . /etc/network/scripts/functions CURRENT_ROUTE_DEV=`ip route show / grep default / awk '{print $5}'` unset ROUTE_GOOD PING_RESULTS="" for i in $IFWAN; do if [ -z $ROUTE_GOOD ]; then PING_RESULT=`ping -c$PING_COUNT -q $HOST_TO_PING -I $i / grep 'packet loss' / awk '{print $6}'` # If no route t host then set to 100% loss if [ -z $PING_RESULT ]; then warn "$0 No route to host $HOST_TO_PING on $i" PING_RESULT='100%' fi if [ $PING_RESULT = '0%' ]; then ROUTE_GOOD=$i if [ -z $CURRENT_ROUTE_DEV ]; then log "$0 Adding default route to $i" cmd route add default gw `cat $DHCPLIB/routers.$i` elif [ $CURRENT_ROUTE_DEV != $i ]; then log "$0 Change default route from $CURRENT_ROUTE_DEV to $i" cmd route del default cmd route add default gw `cat $DHCPLIB/routers.$i` fi else log "$0 loss $PING_RESULT on $i" fi fi PING_RESULTS="$PING_RESULTS $PING_RESULT" done if [ -z $ROUTE_GOOD ]; then warn "$0 lost all internet connections ($PING_RESULTS loss)" fi
Тут всё просто: пингуем в порядке приоритета. Нашли лучший - переключаемся. Если что, пишем в лог.

Ну и напоследок
# cat /etc/cron.d/autoroute PATH="/usr/bin:/bin:/usr/sbin:/sbin" */5 * * * * root /etc/network/scripts/autoroute
# cat /etc/logrotate.conf / tail # system-specific logs may be configured here /var/log/network-scripts.log { weekly missingok rotate 7 compress }

Симлинки
/etc/dhcp/dhclient-enter-hooks.d/route-enter -> ../../network/scripts/route-enter /etc/dhcp/dhclient-exit-hooks.d/route-exit -> ../../network/scripts/route-exit /etc/network/if-pre-up.d/firewall -> ../scripts/firewall /etc/network/if-down.d/firewall -> ../scripts/firewall /etc/network/if-up.d/firewall -> ../scripts/firewall /etc/network/if-post-down.d/firewall -> ../scripts/firewall


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=35262