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

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

Итак, имеем:

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

Логика на первый взгляд простая:

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

Вот только в реализации была поставлена цель сделать всё максимально гибко, например не ограничивать количество потенциальных провайдеров и впоследствии делать минимум телодвижений для перенастройки.
Для начала посмотрим какие плюшки уже есть в системе
В папке /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