Переключатель резервный/запасной канал на прозрачном мосте, не забирающий публичный IP

#SNAT, #ebtables, #mtu, #busybox, #qemu

Автор: Владимир Олейник <dzo@simtreas.ru> (C) 2018

Содержание

Предисловие

Моя предыдущая статья о совмещенном брандмауэре с маршрутизатором натолкнула на мысль, а что если броутеру не давать персональный IP вовсе? Ведь через него проходит весь трафик от внешнего маршрутизатора в локальную сеть. Пусть он поделит один публичный адрес между двумя хостами: для собственных нужд и для остальных сервисов на публичном адресе в "белой" сети. Обычно такие задачи решают заведением демилитаризованной зоны с серыми адресами. Но тогда нам необходимо убедиться, что все публичные сервисы поддерживаются на уровне протоколов пограничным маршрутизатором с сетевой трансляцией для поддержания сессий. Это и не всегда возможно, требует значительных ресурсов и появляются трудности внутри сети с доступом к соседним публичным адресам изнутри самой демилитаризованной зоны.

А у нас всё ещё сложнее:

  1. В сети установлены сертифицированные закрытые криптошлюзы, где как минимум главный шлюз — их центр управления должен быть на публичном адресе. Но и тогда, если остальные шлюзы за NAT, то весь трафик пойдёт через этот шлюз, что тоже крайне не желательно.
  2. Сеть на каждой точке имеет два резервных канала, предоставляемых провайдером. Всё было б замечательно, если б не сильное уменьшение MTU при работе по резервной схеме. Провайдер для организации такой схемы забрал ранее выданный публичный IP у филиалов и поднимает туннель до публичного IP,  который он занял из выделенных ранее организации для маршрутизатора корпоративной сети.  Криптошлюзы имеют признанную ошибку в своём программном обеспечении и не работают при таком сильно заниженным MTU.
  3. И, более того, ко всему вышеизложенному в центральной точке не только появился дефицит публичных IP, но даже больше нет адресов в корпоративной сети — уж так получилось.

Вроде бы проста идея

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

Исходя из вышеизложенного алгоритма данную задачу уже можно решить, воспользовавшись реализацией из предыдущей статьи. Скрипт по переключению каналов для корпоративной сети будет управлять режимом броутинга. Выключение моста и переход на маршрутизацию делается командой:

ebtables -t broute -A BROUTING -i eth-in -p ipv4 -j redirect --redirect-target DROP

где eth-in — интерфейс моста к шлюзу crypto-gw*

Особенность такого решения будет в том, что маршрут до удаленной стороны можно сделать сразу. Вы можете сколько угодно проверять на самих хостах Brouter* доступность удалённой стороны через туннель, это не помешает проходить пакетам по зеленому пути, так как включенный мост срабатывает ранее маршрутизации. Вот только вышеприведённое решение требует адресов как в корпоративной сети, так и в публичной сети для сервера туннелей.

Экономим IP адреса

Нарисуем схему сети исходя из имеющихся адресов:

Экономим MTU

Ох уж эта вечная проблема... Почему она вообще возникает? При обмене пакетами, если по пути встретится интерфейс, у которого максимальный размер пакетов сконфигурирован менее, чем у всех ранее пройденных, то ядро такого хоста в зависимости от флага допустимости или нет фрагментации может либо разбить пакет, если оно пожелает и флаг запрещения фрагментации DF не установлен либо должно сгенерировать ICMP пакет об превышении размера допустимых пакетов. А вот далее возникает проблема прохождения таких ответов до отправителя и правильное реагирование с уменьшением размера пакета, особенно если на той стороне пакеты инкапсулируются в туннель, который уменьшает как и любая инкапсуляция размер пакета уже сама по себе.

У уменьшения MTU есть ещё одна неприглядная сторона — это приводит к увеличению паразитного трафика, когда доля основной информации к общему трафику падает из-за увеличения доли вспомогательной информации: заголовков пакетов. Особенно это проявляется, если мы будем бороться с mtu туннелями, которые будут не информировать стороны о реальных размерах после инкапсуляции, а самовольно разбивать пакет на два и более частей, а потом последовательно их собирать на удаленной стороне, тем самым оставляя первоначальное mtu.

Что же собственно я решал? У меня была проблема с прекращением работы закрытого криптосредства после уменьшения до определенного значения mtu. Я подозреваю, что это у них произошло по следующей причине. Для борьбы с первой проблемой mtu по разрешению прохождения ответов по коррекции mtu и правильной интерпретации и вычисления корректного требуемого размера авторы скорее всего применили грязный хак: фиксировано ограничили размер пакетов через туннель. Это работает до определенного значения mtu на внешних к туннелю интерфейсах и очень трудно диагностируется в виду сжатия пакетов в примененном ими туннеле. Решение, увы, напрашивается одно — не выходить ниже определённого значения по внешнему (открытому) к туннелю пути.

Какие есть варианты по экономии mtu:

  1. Как уже выше упоминалось, можно сделать работоспособным туннель методом разбивки каждого большого пакета на два и более. Это можно сделать на пользовательском уровне, установив размер mtu у интерфейса, который отправляет пакеты из ядерного буфера в программный буфер (для Linux это — tun/tap) выше, чем получается пакет после инкапсуляции. Программа туннелирования должна это понимать и разбивать/собирать такие пакеты. Теоретически это не сложно сделать, но практически это обычно не делается по причине почти в двое увеличения нагрузки на всё: канал и процессор для работы такого туннеля.
  2. Оставим за скобками изменение протокола соединения между узлами. Для протокола IPv4 без сложных ухищрений минимальное изменение mtu будет для инкапсуляции IPIP на размер IPv4 заголовка в 20 байт. Предыдущая статья этот метод туннелирования и использовала. Этот метод туннелирования невозможно реализовать по принципу клиент-сервер, так как нет идентификационного поля (порта) номера соединения. Потому оба хоста должны иметь публичные адреса. Следующим по минимальному изменению mtu идёт протокол инкапсуляции GRE. Этот протокол уже можно пропустить через NAT, но этого делать провайдеры не любят. Далее следуют туннели с инкапсуляцией по UDP (и даже экзотически по ICMP, хотя это ничего кроме как пролезть через дырку в брандмауэре другого не даёт) и уже потом через TCP. Помимо заголовка самого протокола инкапсуляции скорее всего клиент-серверные туннели с поддержкой множества подключений задействуют под собственные нужды ещё несколько байт.
  3. Покопавшись и поработав со всем этим и ещё много с чем, я решил таки написать свой собственный туннель с минимальным изменением mtu.

Общая идея всех туннелей, что мне попадались в следующем: не залезая во внутренние данные пакетов, все действия с ними производятся как с единым блоком данных. Пакеты могут вначале сжать для увеличения энтропии, потом зашифровать, потом даже нарезать. Как следствие, ничего хорошего в результате не получается. Сжатие пакета целиком производит небольшой выигрыш в размере: заголовок пакета бинарен, плохо поддаётся сжатию общими методами увеличения энтропии, эти бинарные данные как бы загрязняют вложенные полезные данные, которые если не зашифрованы и передают какой-либо текст, то могут быть хорошо сжаты. Хорошо сжимает заголовки VJ-компрессор. Но он имеет свои недостатки: работает только с TCP-пакетами, имеет своё состояние и кеш... Как вы уже поняли, я могу долго и нудно говорить на эту тему и пора бы уже перейти к интересной части статьи.

Моя идея по всеми силами уменьшить размер пакета после инкапсуляции состоит в следующем: давайте таки разберем заголовок вложенного IP пакета и

  1. уберём сдвоенные данные, получающиеся при инкапсуляции, такие как размер пакета и crc;
  2. воспользуемся данными о допустимых адресах, которые мы собираемся пропускать через туннель и зашифруем только ради уменьшения длинны адресов наиболее популярные, а может и все указанные в опциях вызова программы туннелирования. Ведь в самом деле, по каналу не могут передаваться адреса с первым нулевым и даже 240-255 значениями байтов, а так же 127. Такими байтами можно закодировать в сокращенном виде как локальный так и удалённый адреса;
  3. если мы ограничимся IPv4, то ни номер версии протокола, ни резервные биты передавать не надо, но можно задействовать для сокращения длинны пакетов, так установив резервный бит под флаг, что этот пакет имеет адреса туннель-туннель, мы сможем для таких пакетов сэкономить 8 байт, так как уже не будет кодировать сами адреса;
  4. можно пойти в ухищрениях еще дальше: если упростить схему сети, считая, что все пакеты с адресами туннель-туннель должны иметь ttl=64 (а это было б странно, если надо иметь не фиксированные значения, которые можно указать опциями, впрочем, утилита traceroute на этом эффекте и работает), а все остальные адреса находятся прямо в локальной сети за хостом с программой туннелирования (в нашей схеме сети так и есть), и опускать реальные значения ttl, а кодировать автоматически как 63, то мы выиграем ещё байт;
  5. если ограничиться только протоколами TCP/UDP/ICMP через туннель, то можно получить ещё не байт, как можно показаться, а целых 3, так как у этих протоколов есть свой crc. Пусть с сохранностью пакетов у нас будет всё хорошо...
  6. я пошёл ещё дальше и добавил опциями по одному номеру для tcp и udp порта, для которого можно закодировать его номер просто одним байтом 0. Это я продумал не сколько для возможности уменьшить mtu, сколько для уменьшения пакета для обмена хостов через туннель по преимущественно одному/двум протоколам;
  7. вышеизложенные методы не имеют состояний, пакеты с одинаковыми полями будут строго одного размера и кодироваться без каких либо эффектов;
  8. выигрыш от энтропийного сжатия, конечно, может существенно уменьшить размер пакета, но не гарантированно. При желании у меня есть оба варианта таких туннелей, со сжатием и без.

Так по зёрнышку в результате у меня получился выигрыш 14-16 байт для следующих опций: количество хостов не более одна сеть/24 и 1 хост/32 и хост с туннелем, протоколы только TCP/UDP/ICMP, фиксированные ttl. Для справки: размер заголовка IP+UDP — 28 байт, минимальная добавка к заголовку для внутренних целей туннелирования: 1-2 байта. Что в сумме даёт как минимум 28-14+2=12 байт, что заметно лучше, чем туннель с минимальной добавкой без ухищрений: IPIP 20 байт. А вот OpenVPN у меня требует аж 84 дополнительных байт. И в этой задаче потому он бы не подошёл.

Значение MTU вычисляется и устанавливается интерфейсу tun моей программой автоматически, согласно указанным перечислениям адресов/сетей, какие мы хотим пропускать через наш туннель, вплоть до 0/0. Это тоже может дать выигрыш, так как смысла делать туннель с разрешением пропуска всех адресов на обеих сторонах не просматривается для нашей, да и вообще для большинства задач.

И опять о ebtables и маршрутизации

После того, как у меня схема заработала стало понятно, почему такое решение, наверное, и невозможно найти в сети. На первый взгляд всё что требуется для успешной работы схемы уже изложено в алгоритмах решения выше. Но как это и обычно получается, это обманчиво. Вот смотрите. Если у нас нет адреса у машины в главной сети, то какой адрес шлюза указать рабочему хосту? Конечно, когда у нас brouter работает в режиме моста, то это не имеет ровным счётом никакого значения, так как безусловно хост будет отправлять пакеты через маршрутизатор, без учёта, какой адрес мы присвоили управляющему интерфейсу моста/коммутатора. Выключив же мост, на удивление ничего страшного не произойдёт, пакеты отправятся по интерфейсу с нулевым адресом на выходной интерфейс согласно маршрутизации по адресу назначения, ведь в самом деле, внутренние адреса интерфейсов в обычной маршрутизации значения не имеют. Но тут и возникает засада. Перед тем как отправить пакет в интерфейс в свободное плавание, необходимо узнать адрес назначения всех нижележащих протоколов, в которые будет обернута наша полезная информация. То есть ядро рабочего хоста перед отправкой пакета с данными должно узнать MAC для нашего шлюза. А если шлюз неработоспособен, потому мы и хотим перейти на запасной? Маршрутизацию то мы включим на бывшем до этого прозрачном мосте, но пакеты не получим, так как рабочий хост перед отправкой пакетов будет долго и мучительно ждать работоспособности своего шлюза по умолчанию, опрашивая его MAC. Вот мы и подошли к изюминке этой статьи. Ну а как же? Всё дело как всегда в деталях. После продолжительных размышлений, а ведь это действительно не сразу доходит, почему вот только что всё работало, а после паузы — перестало, я решил, что неверное ничего лучше, чем жестко прописать в скриптах MAC адрес нашего корпоративного шлюза, а потом применить грязноватый хак, но весьма действенный: пусть будет постоянный ответ от моста на запрос MAC шлюза. То есть работает он или нет, мост будет отвечать хостам жестко прописанным этим MAC.

ebtables -t nat -A PREROUTING -p ARP -i eth2 --arp-op Request \
	--arp-ip-dst `resolv corp-gw` -j arpreply --arpreply-mac ${a}6

где ${a} — MAC у интерфейса corp-gw

Не всё так же просто и с маршрутизацией. Во-первых, ядро само прописывает маршруты для эзернет интерфейсов с IP адресом. У нас же придётся прописывать маршруты для хоста Br самостоятельно. Но и это не поможет для работы маршрутизации без указания MAC для тех шлюзов, где будет маршрутизация с несконфигуренного адреса. Интересный эффект, впрочем вполне объяснимым, также можно наблюдать, если вы укажете маршруты до удалённых сетей без указания шлюза, в надежде, что шлюз мы уже указывали ранее. Ядро это позволит сделать, но работать такой маршрут начнёт только после указания MAC для этого удаленного хоста равному MAC шлюза. Объяснение этому очень простое, так как абсолютно все IPv4 адреса попадают под "сеть" на интерфейсе у неуказанного адреса 0.0.0.0/0.0.0.0, то ядро считает, что все адреса находятся вот сразу на этом супер-глобальном эзернете и значит надо искать MAC перед тем как начать обмен с любым адресом.

Новая тестовая лаборатория

Как и в прошлой статье, я и в этот раз подготовил готовый образ виртуальной машины, один для всех хостов на основе qemu и ядром Linux и моей версией busybox. Прошёл год с момента написания прошлой статьи, за это время я таки решился поменять в моей сборке (они же и в мейнстримной сборке) минимальные варианты утилит ping и ip на полноценные. Уж очень хочется провести полные тесты, воспользоваться опциями обычного варианта утилиты ping для получения пакетов разной длинны и детектирования ответных ICMP пакетов.

Как ни покажется странно, но на написание своего "OpenVPN" у меня ушло не много времени. Но секрет прост: я уже делал этот компрессор IPv4 заголовков для туннеля vtund, много лет его использовал для VPN дом-работа, но сейчас полностью переписал. Это дело обычное, когда умеешь и делаешь для себя — берем готовое, правим и понимаем, что проще написать с нуля...

На полноценную утилиту ip я возлагал надежды на проведения испытаний разных типов встроенных туннелей, но встроенные не подошли из-за проблем, описанных в главе о экономии IP адресов. Так как пакет включал в себя утилиту tc, то я очень надеюсь, что следующая статья будет о tc — управлении трафиком. Многочисленные тесты выявили ошибку портирования утилиты ebtables, присутствовавшую в предыдущей версии моего busbox, где она только появилась.

Как и в прошлый раз, создание и описание лаборатории надо начинать с упрощении схемы сети и назначении адресов, а ещё лучше и их имён на интерфейсах.

Для сетевого взаимодействия виртуальных машин пронумеруем tap интерфейсы и мосты.

Соответственно, файлы в виртуальных машинах будут для /etc/hosts:

Показать файл
# For loopbacking.
127.0.0.1       localhost

10.68.0.1       cr-gw tap1
#0.0.0.0        ZERO TAP2
192.168.0.1     users tap3
192.168.0.254   inet tap4
#0.0.0.0        zero tap5
10.68.0.254     corp-gw tap6
1.1.0.3         corp-igw tap7
1.1.0.2         br tap8
1.1.0.254       igw tap9
1.1.0.100       i4g tap10
1.1.1.254       uplink tap11
#0.0.0.0        zero tap12
10.68.1.1       cr-gw1 tap13
10.68.1.2       br1 tap14
#0.0.0.0        zero tap15
10.68.1.254     corp-gw1 tap16
10.68.254.1     ext1 tap17
10.68.254.254   ext tap18
192.168.1.3     corp-igw1 tap19
192.168.1.1     inet1 tap20
192.168.1.100   r1 tap21
#0.0.0.0        zero tap22
1.1.1.1         igw1 tap23
# for fw
255.255.255.255 broadcast

где сети /etc/networks:

Показать файл
#
loopback        127.0.0.0
cr-net          10.68.0.0
public-net      1.1.0.0
inet-net        192.168.0.0
cloud-net       10.68.254.0
cr1-net         10.68.1.0
inet1-net       192.168.1.0
inet-cloud      1.1.1.0
tunnel-net      10.0.0.0
# for fw
link-local      169.254.0.0
Test-net        192.0.2.0
Unallocated     248.0.0.0
Private-172     172.16.0.0
Multicast-224   224.0.0.0
Reserved-240    240.0.0.0
# for micro tst lab
tst-net         10.68.45.0

Теперь у нас виртуальных машин больше 9, потому пришлось изменить скрипт по их запуску, воспользовавшись уже полным последним байтом MAC, пусть и не прибегая к трансляции десятеричной в шестнадцатеричную систему:

Показать скрипт
#!/usr/bin/env bash
#
# Usage
#
# lab_start.sh [all] [[-]host ...]
#  start hosts (all) -do not start this hosts

HOSTS="cr-gw:1 br:8:4:2:5 corp-gw:6:18:7 corp-gw1:16:17:19 br1:14:21:15:12:22 igw:9:11 cr-gw1:13 igw1:23:20:10 users:3"
. ./lab0.sh

minus2underline_set() {
	local __h=${1//-/_}
	eval $__h=$2
}

minus2underline_tst_non_zero() {
	local h=${1//-/_}
	h=${!h}
	[[ -n $h && 0 -ne $h ]]
}

cut_host() {
	local __out __out2 __IFS=$IFS
	__out=$1=\$$3
	__out2=__out2=\$$3
	IFS=:
	set -- $2
	IFS=$__IFS
	eval $__out
	shift
	eval $__out2
	[ -z "$__out2" ]
}

declare -a nic_args

start() {
	local i t r=1
	echo -n start $1
	nic_args=()
	for((i=2;r!=0;i++)); do
		cut_host t $2 $i
		r=$?
		nic_args+=("-net")
		nic_args+=("nic,vlan=$t,model=virtio,macaddr=52-54-00-12-34-$t")
		nic_args+=("-net")
		nic_args+=("tap,vlan=$t,script=no,downscript=no,ifname=tap$t")
		echo -n " tap$t"
	done
	echo

	qemu-system-x86_64 -name $1 \
	 -boot c -m 64M -enable-kvm \
	 -drive file=$IMG,if=virtio,format=raw \
	 "${nic_args[@]}" \
	 -display sdl -vga vmware \
	 -parallel none -serial none -balloon none -localtime -daemonize
}

s=x$1
if [[ x = $s ]]; then
	s=xall
fi
while [[ x != $s ]]; do
  found=0
  for ht in $HOSTS; do
	cut_host h $ht 1
	if [[ xall = $s || $s = x$h ]]; then
		minus2underline_set "$h" 1
		found=1
	elif [[ $s = x-$h ]]; then
		minus2underline_set "$h" 0
		found=1
	fi
  done
  if [ $found -eq 0 ]; then
	if [[ ${s:0:2} != x- ]]; then
		minus=
	else
		minus="-"
	fi
	echo "$0: "$minus"host '$1' unknown" 2>&1
	exit 1
  fi
  shift
  s=x$1
done

for ht in $HOSTS; do
	cut_host h $ht 1
	r=$?
	if minus2underline_tst_non_zero "$h"; then
		if [ $r -eq 0 ]; then
			echo "Do not found tap's number for host $ht" 2>&1
			exit 1
		fi
		start $h "$ht"
	fi
done

Скрипт настройки мостов на хостовой системы логику не поменял, но, безусловно, требует новую конфигурацию, потому я его облагородил для работы со строкой конфигурации:

Показать скрипт
#!/usr/bin/env bash

BR=br
nr_br=1
. ./lab0.sh
BRIDGES="1-2 3-4 5-6 7-10 11-12 13-14 15-16 17-18 19-21 22-23"

add_int() {
	ifconfig $1 down && ifconfig $1 0.0.0.0 promisc up
	brctl addif $2 $1
}

br_close() {
	ifconfig $1 down && brctl delbr $1
}

br_start() {
	br_close $1 && brctl addbr $1
	for ((I=$1; I<=$2; I++)); do
		ifconfig tap$I > /dev/null 2>&1 || tunctl -t tap$I -u $USER
		add_int tap$I $1
	done
	brctl stp $1 off && ifconfig $1 up
}

if [[ xstart == "x$1" || xclose == "x$1" ]]; then
	for b in $BRIDGES; do br_$1 "$BR$((nr_br++))" ${b%-*} ${b#*-}; done
else
	echo usage: $0 'start|close' >&2 && exit 1
fi

Для виртуальных машин специальных хостов Br и Br1 дополнительно вызывается скрипт под созданию мостов, теперь с помощью утилиты ip

Показать скрипт
#!/bin/sh

echo Configure br$1

ip link add br$1 type bridge
ip link set dev $2 master br$1
ip link set $2 up
ip link set dev $3 master br$1
ip link set $3 up
ip link set br$1 up

Общий стартовый скрипт почти не поменялся, для сокращения его объёма при имеющихся количестве хостов я решил сделать отдельный каталог с файлами-скриптами по именам хостов для их персональных настроек. Такой скрипт для хоста Br /etc/hostnames/br вызывается после общего скрипа конфигурирования мостов:

Показать скрипт
#!/bin/sh

echo Set route for test
route add -net cr-net/24 dev br0
# set tap6
arp -s corp-gw ${a}6
route add -host corp-gw1 gw corp-gw dev br0
ebtables -t nat -A PREROUTING -p ARP -i eth2 --arp-op Request --arp-ip-dst `resolv corp-gw` -j arpreply --arpreply-mac ${a}6

echo Set SNAT/DNAT
iptables -t nat -A POSTROUTING -s $HOSTNAME -d corp-gw1 -j SNAT --to-source `resolv cr-gw`
iptables -t nat -A POSTROUTING -s users -o eth0 -j SNAT --to-source `resolv $HOSTNAME`
iptables -t nat -A PREROUTING -d br -p tcp --dport 8080 -j DNAT --to-destination `resolv users`:80

/etc/tun.sh server inet/24 br -A cr-gw -B inet-net -r /etc/tun.sh

nohup /etc/sw.sh < /dev/null > /dev/null 2> /dev/null &

Разберем его построчно. Первая строка route вручную добавляет маршрут к зеленой сети, по которой осуществляется обмен по мосту этого хоста. Как уже говорилось, у этого моста и его интерфейсов адресов нет, ядро не будет добавлять этот маршрут автоматически. Далее мы уже можем указывать маршруты для удаленных сетей через шлюз находящейся в только что добавленной сети. Но без указания MAC обмен всё равно не пойдёт, так что добавим следующей строкой статическую запись результата протокола ARP. Строку ebtables мы уже отдельно разобрали в главе "И опять о ebtables...". Далее устанавливаем трансляции сетевых адресов:

Показать скрипт
#!/bin/sh

PORT=5500
KEY_FILE="/etc/key_tun.txt"
cmd=$1
if [ x == "x$cmd" ]; then
	if [ -n "$TUN_DEV" ]; then
		logger "VU4TUN: route add -net cr1-net/24 dev $TUN_DEV"
		route add -net cr1-net/24 dev "$TUN_DEV"
		exit 0
	fi
	echo "Usage $0 server/client or set $TUN_DEV" >&2
	exit 1
fi
shift
LOCAL=$1
shift
R_B=$1
shift

if [ "x$cmd" == xclient ]; then
	echo Configure VU4TUN as client
	vu4tun -p -l "$LOCAL" "$@" -k "$KEY_FILE" "$R_B" $PORT
elif [ "x$cmd" == xserver ]; then
	echo Configure VU4TUN as server
	vu4tun -p -l "$LOCAL" "$@" -k "$KEY_FILE" -b "$R_B" $PORT
fi

Свою программу по установлению туннеля я обозвал как vu4tun. Она поддерживает аутентификацию обеих сторон одним паролем по принципу chap-md5. Если первый аргумент не server и не client, то проверяем переменную $TUN_DEV и добавляем маршрут до удаленной стороны через туннель. В нашей лаборатории это делает только сторона Br, где все маршруты сразу присутсвуют и работает по умолчанию мост, поэтому маршрут у нас захаркожен для сети cr1-net. В зависимости от режима ($1) мы указываем: для клиента $2 — адрес сервера, а для сервера $2 — адрес, на который сервер должен сделать bind (опция -b), остальные аргументы (сети для передачи по туннелю) передаются программе напрямую ("$@")

Скрипт-переключатель основной-запасной имеет 4 состояния: начальное (state=0), когда все настройки проделаны внешними скриптами по установке маршрутов и броутинга для работы по основному каналу. В цикле проверяем доступность удаленной стороны по основному каналу. Если state пока 0 и появились проблемы прохождения ping с желаемым MTU и запрещенной фрагментацией (-M do — включаем режим определения mtu путем установки DF), то однократно для изменения state 0 в 1 сообщаем в лог о проблеме. Только после того, как мы убедились, что основной канал не работает, производится проверка запасного канала. Если он так же не работает, то меняем state на 2 и сообщаем о проблемах в лог и об этом. Если же запасной канал работоспособен, то меняем статус на 3 если он еще пока не равен 3 и только тогда производим переключение на запасной канал и производим информирование в лог об этом. Если основной канал заработал, то есть в новой итерации цикла проверка основного канала стала удачной и статус был равен 3, то переключаем на основной канал и меняем статус на начальный — 0.

Показать скрипт
#!/bin/sh

MAIN=corp-gw1
REZ=r1

state=0

logger "switcher started"

while true; do
	if ! ping -c1 -M do -s 1456 $MAIN > /dev/null 2>&1 ; then
		if [ $state -eq 0 ]; then
			logger "Can not ping MAIN ($MAIN)"
			state=1
		fi
		if ! ping -c1 -M do -s 1456 $REZ > /dev/null 2>&1 ; then
			if [ $state -eq 1 ]; then
				logger "Can not ping REZERV ($REZ)"
				state=2
			fi
		else
			if [ $state -ne 3 ]; then
				logger "Change the corporative channel to REZERV"
				/etc/brouting.sh eth2
				state=3
			fi
		fi
	elif [ $state -eq 3 ]; then
		logger "Change the corporative channel to MAIN"
		/etc/brouting.sh
		state=0
	fi
	sleep 1
done

Тесты, тесты

Как и в прошлой статье, все настройки в прилагаемом едином образе виртуальных машин уже проделаны для успешного прохождения тестов. Небольшое исключение есть в том, что "облако 4G" не сконфигурено и это надо делать самостоятельно. В ядре добавлена поддержка трансляции ARP, моя версия busybox уже включает все необходимые утилиты, в том числе полноценные утилиты ping, ip, iptables, ebtables и мою программу установки туннеля. Логгинг в моих настройках лаборатории выводит свои сообщения на 6-й терминал. Сильно уменьшая mtu, а потом восстанавливая на интерфейсах ext-ext1 можно увидеть как появляются сообщения о переключении каналов. Особенность конфигурации в том, что маршрутизация будет асимметричной при изменении mtu не на ext/ext1, а только на одной стороне copr-gw или copr-gw1. Как и в прошлой статье проверять имя исходящего и получившего соединение хоста можно тривиальным тестовым CGI-скриптом, который запрашивает мой http-сервер из busybox с URL http://remote/cgi-bin/test.cgi показывает свой HOSTNAME так и REMOTE_ADDRESS. Скрипт: /bin/www.sh хост [порт]

Так для пользователей Интернета центрального офиса все обращения к "Интернету", а именно к интерфейсам с белыми (жёлтыми) адресами этот скрипт в качестве REMOTE_ADDRESS должен показать "br" — адрес подмены, для филиала — с хоста corp-gw1, который у нас выполняет также роль пользователя Интернет — igw1.

Проверка работоспособности доступа в Интернет от хоста corp-gw1 интересна ещё и тем, что мы доказываем, что воровство IP для установки туннеля не помешала маршрутизатору использовать "белый" IP. Важно помнить, что в этой статье межсетевой экран не рассматривается и не конфигурируется, все хосты доступны друг другу, если сконфигурены шлюзы, поэтому стоит обращать внимание не только на возможность соединения, но и на оба адреса, локальный и удаленный.

Если вы хотите убедиться, что без трансляции ARP основной канал не работает, необходимо не только отключить эту трансляцию, поменяв в вызове ebtables опцию -A на опцию -D, но и очистить ядерные таблицы ARP, записи в которые были помещены как нами вызовом arp -s, так и самим ядром, вызвав arp -d хост.

Переконфигурация виртуального образа после изменения изнутри путем перезагрузки можно проводить командой /etc/mkres -r Стоит помнить, что если вы начали после этого править другую запущенную машину, то конфиг на ней пока будет без изменения, и если вы хотите получить содержимое уже после изменения на первой машине, то стоит перезапустить и эту машину, но уже командой reboot и только после перезагрузки первой машины, чтобы была гарантия, что образ с первыми изменениями уже полностью перезаписан.