В последних дистрибутивах 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