Как быть когда твой провайдер мудаг?
Предистория
Сегодня я расскажу историю о том, как быть когда нужен стабильный интернет в режиме 24/7, руки и оборудование позволяют, а твой провайдер и его ближайшие соседи — имбецилы.
Дело было в далёком 2017 году в замечательный городе «С», который отличается несколькими достопримечательностями:
— Свежий воздух;
— Мягкий климат;
— Бесхребетный people (без обобщений).
Русскоязычный онлайн-курс по MikroTik от нашего коллеги Дмитрия Скромнова. Здесь можно изучить MikroTik и RouterOS самостоятельно по курсу «Настройка оборудования MikroTik». Курс основан на официальной программе MTCNA, но содержит больше информации. Это 162 видеоурока и большая практическая задача, разбитая на 45 лабораторных работ. Время на изучение неограниченно – все материалы передаются бессрочно и их можно пересматривать сколько нужно. Первые 25 уроков можно посмотреть бесплатно, оставив заявку на странице курса.
C первыми двумя пунктами всё замечтательно, но вот последний, без преувеличения и обобщения являет собой корень зла сей географической точки. Это тебе и пообещал — проставил, и сервис ниже плинтуса, и нахамить клиенту — ачивмент. Одним словом — печаль! Естественно, не обошло стороной сие горе и сферу телекоммуникаций. Здесь тебе и дропы в полный рост, и купленный абонетом «белый» IP пилят в тихую на 100-500 хомячков, и медные воздушки между домами #over100метров, короче всё как полагается!
Спустя пару лет мытарств с оператора на оператора, я пришел к неутешительному выводу, что судьбинушка помяла их всех без разбора. Выход был один — прекратить попытки сбора сливок на говне и заняться делом. К тому времени, в нагрузку к бэдам с провайдерами на меня свалился шквал лагов со стороны моего домашнего серванта на какинтоше. Так как на нём помимо файлопомоек, плюха (Plex) и контроллера домена крутился ещё и DHCP с DNS, любой отвал серванта приводил к понятным последствиям. Дело пахло керосином и пора уже было что то решать, ибо роль сапожника без сапог уже изрядно утомила. Благо первый опыт написания скриптов для MikroTik не прошёл бесследно, да и к этому времени уже много чего было написано для данной платформы. На тот момент я оброс вторым таким же маршрутизатором и дело оставалось за малым — начать действовать.
Задача
Стала необходимость обеспечения отказоустойчивости домосетки как извне так и изнутри. Что предстояло сделать:
- Организовать VRRP кластер;
- Обеспечить переезд сервисов маршрутизатора при сбое (PPTP/L2TP);
- Подстраховаться на случай падения внутреннего сервера (переезд DNS+DHCP);
- Получать извещения на почту когда кто-то облажался.
Условия
- Два нерадивых провайдера;
- Два MikroTik’а с 5 сетями (4хVLAN + широковещательный);
- Один домашний сервант и точка доступа.
Теперь по порядку.
1. VRRP кластер
Суть работы VRRP кластера достаточно проста. На двух или более маршрутизаторах создаются одинаковые VRRP интерфейсы, имеющие одинаковые MAC и IP адреса. Отличаются эти интерфейсы только лишь приоритетом, у кого выше — тот главный. В момент когда основной интерфейс активен, второстепенные не активны. К примеру маршрутизатор №1 имеет IP 10.0.0.2, а маршрутизатор №2 имеет IP 10.0.0.3, при этом IP VRRP интерфейса на обоих маршрутизаторах 10.0.0.1. Таким образом, для клиентского устройства маршрутизатором по факту является то усройство, на котором активен VRRP интерфейс. В своём случае я создал 5 VRRP интерфейсов, для каждой из сетей.
Это собственно и всё что касается конфигурации VRRP. Теперь зверушки начали общаться друг с другом, что говорит об успехе мероприятия.
Вот так выглядит пакет анонса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | Frame 2023: 46 bytes on wire (368 bits), 46 bytes captured (368 bits) on interface 0 Interface id: 0 (en0) Encapsulation type: Ethernet (1) Arrival Time: Feb 26, 2017 10:09:37.770546000 MSK [Time shift for this packet: 0.000000000 seconds] Epoch Time: 1488092977.770546000 seconds [Time delta from previous captured frame: 0.003152000 seconds] [Time delta from previous displayed frame: 0.000000000 seconds] [Time since reference or first frame: 1.833961000 seconds] Frame Number: 2023 Frame Length: 46 bytes (368 bits) Capture Length: 46 bytes (368 bits) [Frame is marked: False] [Frame is ignored: False] [Protocols in frame: eth:ethertype:ip:vrrp] [Coloring Rule Name: Routing] [Coloring Rule String: hsrp || eigrp || ospf || bgp || cdp || vrrp || carp || gvrp || igmp || ismp] Ethernet II, Src: IETF-VRRP-VRID_64 (00:00:5e:00:01:64), Dst: IPv4mcast_12 (01:00:5e:00:00:12) Destination: IPv4mcast_12 (01:00:5e:00:00:12) Source: IETF-VRRP-VRID_64 (00:00:5e:00:01:64) Type: IPv4 (0x0800) Internet Protocol Version 4, Src: 10.0.0.2, Dst: 224.0.0.18 0100 .... = Version: 4 .... 0101 = Header Length: 20 bytes (5) Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT) Total Length: 32 Identification: 0x1234 (4660) Flags: 0x00 Fragment offset: 0 Time to live: 255 Protocol: VRRP (112) Header checksum: 0xbf25 [validation disabled] [Header checksum status: Unverified] Source: 10.0.0.2 Destination: 224.0.0.18 [Source GeoIP: Unknown] [Destination GeoIP: Unknown] Virtual Router Redundancy Protocol Version 3, Packet type 1 (Advertisement) Virtual Rtr ID: 100 Priority: 101 (Non-default backup priority) Addr Count: 1 0000 .... = Reserved: 0 .... 0000 1100 1000 = Adver Int: 200 Checksum: 0x7440 [correct] [Checksum Status: Good] IP Address: 10.0.0.1 |
Теперь нужно обеспечить «переезд» VRRP интерфейса (и не только) в случае отключения интернет соединения, чем как раз и занимается мой скрипт.
2. Обеспечение переезда сервисов маршрутизатора (PPTP/L2TP) при сбое
Помимо очевидного, на маршрутизаторе дополнительно подняты PPTP/L2TP соединения. Соответственно их тоже нужно мигрировать, ибо держать поднятыми все PPTP/L2TP на двух маршрутизаторах одновременно на мой взгляд не спортивно да и не всегда получается. Теперь разберём сам скрипт по частям:
— Для начала объявляем переменные.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | beep frequency=1760 length=10ms; :local scriptName "Failover"; # Задаем имя скрипта :if ([len [/system script job find where script=$scriptName]]>1) do={ # Проверяем на наличее уже запущенного # скрипта. Если скрипт уже запущен, выходим, если нет, работаем. log error "$scriptName: Single instance"; beep frequency=440 length=1000ms; } else={ delay 60s; # При старте вместе с системой лучше подождать, иначе возможны грабли, проверено. :local globalIPArray {"8.8.8.8";"77.88.8.8";"193.58.251.251";"114.114.115.115"}; # Определяем адреса # которые скрипт пингует для проверки интернет соединения. Кол-во адресов может быть любое. В примере указано 4 # публичных DNS сервера (Google, Yandex, SkyDNS, 114dns). :local localIPArray {"10.0.0.4";"10.0.1.4";"10.0.2.4";"172.16.0.4";"192.168.0.4"}; # Определяем адреса # которые скрипт пингует для проверки локального соединения. Кол-во адресов может быть любое. :local restoreDelays {600;7200;21600;43200;86400}; # Определяем тайм-ауты восстановления аплинка в # секундах. Кол-во и значения могут быть любые. :local filePath "disk1/var/log/full_log.0.txt"; # Указываем файл высылаемый на почту при сбое. :local email "mail@example.com"; # Куда спамить письмами. :local displayOnError "bond1.0"; # Что отображаем на дисплее при сбое. :local displayOnRestore "uplink0"; # Что отображаем на дисплее в нормальном режиме. :local uplink "uplink0"; # Указываем интерфейс аплинка. :local pingCount 5; # Кол-во пингов на один IP. :local conA 0; :local conB 0; :local conD 0; :local conC 0; :local once 0; :local pingResA 0; :local pingResB 0; :local mailSubject ([/system identity get name]."::".$scriptName); # Тема отправляемого E-Mail. # Вводим глобальные переменные для отображения текущего статуса в environment. :global FailoverState 0; # Отображает текущий статус скрипта, всего их 5, от 0 до 4. :global UplinkIPCurrentScan "waiting..."; # Отображает IP в интернет который скрипт пингует в # данный момент. :global LocalIPCurrentScan "waiting..."; # Отображает локальный IP который скрипт пингует в # данный момент. :global RestoreDelay 0; # Номер следующего тайм-аута восстановления аплинка. :global RestoreDelayTime "0 s"; # Длительность следующего тайм-аута. :global RestoreDelayTimeRemaining "0 s"; # Отображает оставшеся время до восстановления аплинка # без учёта длительности основного цикла. :global UplinkDrops 0; # Количество потерянных пакетов на аплинке. :global UplinkDisabled "false"; # Статус аплинк интерфейса. :global UplinkFailoverTimes 0; # Кол-во падений аплинка. :global UplinkFailoverLastTime "-"; # Дата и время последнего падения аплинка. :global UplinkRestoreTimes 0; # Кол-во восстановлений аплинка. :global UplinkRestoreLastTime "-"; # Дата и время последнего восстановления аплинка. :global LocalDrops 0; # Количество потерянных пакетов в локалке. :global LocalDisabled "false"; # Статус локального интерфейса. :global LocalFailoverTimes 0; # Кол-во падений локалки. :global LocalFailoverLastTime "-"; # Дата и время последнего падения локалки. :global LocalRestoreTimes 0; # Кол-во восстановлений локалки. :global LocalRestoreLastTime "-"; # Дата и время последнего восстановления локалки. |
Вот так выглядит вывод глобальных переменных в environment. В консоль можно вывести всё это добро командой /environment print
.
— Объявляем функции.
*И вот именно в этот момент я полез править работающий скрипт. Как результат — 2 функции вместо 6 и минус 2 часа жизни :р. Потом мне не понравились 4 функции звуковых сигналов… Спустя 2 часа осталась одна, продолжаю.
В процессе эксплуатации были обнаружены эпические «грабли» с мостами, для решения которых пришлось как обычно «прикручивать костыли». Подробнее об этих и прочих «граблях», а так же о их решениях можно почитать здесь, а в данном случае, плюс ещё одна функция и трафик между VRRP интерфейсами и мостами начинает ходить. Для того чтобы «разбудить» мосты, при изменении состояния VRRP интерфейсов, меняется параметр Edge с «auto» на «yes» и наоборот.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | # Функция отвечает за вывод даты и времени в письмах и глобальных переменных. :local scdGt do={:return "$[/system clock get date] $[/system clock get time]";} # Функция отвечает за озвучку действий скрипта. На входе воспринимает три массива - частота звука, # длительность звука и длительность задержки между звуками. Данные указываются в герцах и миллисекундах. # Примеры: # [$beepM fArr=({"1568.0"; "1318.5"; "1046.5"}) dArr=({"100"}) lArr=({"100"})]; # [$beepM fArr=({"1568.0"; "1318.5"; "1046.5"}) dArr=({"100"; "200"; "300"}) lArr=({"100"; "200"; "300"})]; :local beepM do={ set $cnt ([len $fArr]); do { if (([len $lArr])>1) do={set $lnt [($lArr->$cnt)];} else={set $lnt [($lArr->0)];} if (([len $dArr])>1) do={set $dnt [($dArr->$cnt)];} else={set $dnt [($dArr->0)];} execute "beep frequency=$[($fArr->$cnt)] length=$($lnt)ms "; delay "$($dnt+1) ms"; set $cnt ($cnt-1); set $lnt ($lnt-1); } while ($cnt>-1); delay 500ms; } # Функция конвертирует дату и время формата Feb/25/2017 14:18:12 в два варианта, первый - это количество # секунд от рождества Христова (это нам нужно для вычислений тайм-аутов) и второй - вот такого вида: # 25-02-2017-18-43 для формирования удобоваримых имён файлов. # Примеры: # ([$datr fdte=[$scdGt]]->0) - Возвращает текущую дату в секундах; # ([$datr fdte=[$scdGt]]->1) - Возвращает текущую дату в формате 25-02-2017-18-43. :local datr do={ :local dfy 0; :local yar [pick $fdte 7 11]; :local mth [pick $fdte 0 3]; :local day [pick $fdte 4 6]; :local hur [pick $fdte 12 14]; :local min [pick $fdte 15 17]; :local sec [pick $fdte 18 20]; :local mts ("jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"); :local mtd {0;31;28;31;30;31;30;31;31;30;31;30;31}; :local mtn ([find $mts $mth -1]+1); :local yrd (($yar*365)+($yar / 4)); :local cmd ($mtd->$mtn); :local conA $mtn; do { set $conA ($conA-1); set $dfy ($dfy+($mtd->$conA)); if ($conA=0) do={set $dfy ($dfy+$day);} } while ($conA>0); if ($mtn<=9) do={set mtn ("0".$mtn);} return {((($yrd+$dfy)*86400)+($hur*3600)+($min*60)+$sec);($day."-".$mtn."-".$yar."-".$hur."-".$min)}; } # Функция включает отключенные и выключает включенные VRRP интерфейсы. # Примеры: # [$vrrpIf scriptName=$scriptName vrrpDo="enable"]; - Включаем VRRP интерфейсы; # [$vrrpIf scriptName=$scriptName vrrpDo="disable"]; - Отключаем VRRP интерфейсы. :local vrrpIf do={ if ($vrrpDo="enable") do={set $vrrpSt false;} else={set $vrrpSt true;} :local vrrpIfCount [/interface vrrp print count-only;]; # Получаем кол-во VRRP интерфейсов. do { set $vrrpIfCount ($vrrpIfCount-1); # Если режим "disable" или статус интерфейса НЕ бекап. if ($vrrpSt=true || [/interface vrrp get $vrrpIfCount backup]!=true) do={ # Определяем статус активности интерфейса. if ([/interface vrrp get $vrrpIfCount running]=$vrrpSt) do={ beep frequency=1760 length=10ms; log info "$scriptName: $vrrpDo VRRP interface $[/interface vrrp get $vrrpIfCount name]"; execute "/interface vrrp $vrrpDo $vrrpIfCount"; # Выполняем заданное действие. # Ждём изменений в статусе активности интерфейса перед переходом к следующему. do {delay 10ms;} while ([/interface vrrp get $vrrpIfCount running]=$vrrpSt); } } } while ($vrrpIfCount>0); } # Функция возвращает статус VRRP интерфейсов. Если все активны - true, если хотя бы один не активен - false. # Примеры: # if ([$vrrpSt]=true) do={log info "All VRRP IFs is active";} - Если все VRRP интерфейсы активны, пишем лог; :local vrrpSt do={ :local vrrpIfCount [/interface vrrp print count-only;]; do { set $vrrpIfCount ($vrrpIfCount-1); if ([/interface vrrp get $vrrpIfCount running]=true) do={ set $out true; } else={ set $vrrpIfCount 0; set $out false; } } while ($vrrpIfCount>0); return $out; } # Те самые костыли. Функция изменяет параметр edge для всех портов мостов. # Примеры: # [$brdgPt brdgEd="auto"]; - Изменяет значение параметра edge на заданный (в примере на auto); :local brdgPt do={ :local brdgPtCount [/interface bridge port print count-only;]; do { set $brdgPtCount ($brdgPtCount-1); if ([/interface bridge port get $brdgPtCount edge]!=$brdgDo) do={ execute "/interface bridge port set edge=$brdgEd numbers=$brdgPtCount"; } } while ($brdgPtCount>0); } # Функция включает отключенные и выключает включенные PPTP/L2TP интерфейсы. # Примеры: # [$tunlIf scriptName=$scriptName tunlDo="enable"]; - Включаем PPTP/L2TP интерфейсы; # [$tunlIf scriptName=$scriptName tunlDo="disable"]; - Отключаем PPTP/L2TP интерфейсы. :local tunlIf do={ if ($tunlDo="enable") do={set $tunlSt true;} else={set $tunlSt false;} :local pptIfCount [/interface pptp-client print count-only;]; # Получаем кол-во PPTP интерфейсов. :local l2tpIfCount [/interface l2tp-client print count-only;]; # Получаем кол-во L2TP интерфейсов. do { set $pptIfCount ($pptIfCount-1); if ([/interface pptp-client get $pptIfCount disabled]=$tunlSt) do={ # Узнаём статус интерфейса. beep frequency=1760 length=10ms; log info "$scriptName: $tunlDo PPTP interface $[/interface pptp-client get $pptIfCount name]"; execute "/interface pptp-client $tunlDo $pptIfCount"; # Выполняем заданное действие. # Ждём изменения состояния интерфейса перед переходом к следующему. do {delay 100ms;} while ([/interface pptp-client get $pptIfCount disabled]=$tunlSt); } } while ($pptIfCount>0); do { set $l2tpIfCount ($l2tpIfCount-1); if ([/interface l2tp-client get $l2tpIfCount disabled]=$tunlSt) do={ # Узнаём статус интерфейса. beep frequency=1760 length=10ms; log info "$scriptName: $tunlDo L2TP interface $[/interface l2tp-client get $pptIfCount name]"; execute "/interface l2tp-client $tunlDo $l2tpIfCount"; # Выполняем заданное действие. # Ждём изменения состояния интерфейса перед переходом к следующему. do {delay 100ms;} while ([/interface l2tp-client get $l2tpIfCount disabled]=$tunlSt); } } while ($l2tpIfCount>0); } |
— Старт.
1 2 3 4 5 6 7 8 9 10 11 12 | lcd backlight state=on; # Включаем дисплей. [$brdgPt brdgEd="yes"]; # "Подготавливаем" мосты. [/interface enable $uplink;] # Включаем аплинк. lcd interface display $displayOnRestore; # Отображаем заданный интерфейс. [$beepM fArr=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) dArr=({"100"}) lArr=({"100"})]; # Звучим о старте. # Шлём на почту файл (текущий лог) и сообщение о старте. /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Started.\n" file=$filePath; if ($FailoverState=0) do={ # Проверяем текущий статус, если "0", то стартует основной цикл. Именно он держит скрипт # в режиме постоянно активного процесса. [$beepM fArr=({"1975"; "1975"}) dArr=({"1000"; "150"}) lArr=({"100"})]; # Звучим о старте процесса. |
— Следующий цикл проверяет физическое подключение аплинк-нтерфейса. Задача цикла очень проста. Вытащили кабель из порта — получили статус «4», воткнули — получили статус «0». Таким образом мы активируем процесс проверки соединения при подключенном кабеле и останавливаем при отключенном.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | do { if ($conC<1) do={ # Если тайм-аут отсутствует (меньше 1). if ([/interface get $uplink running]=false) do={ # Проверяем состояние интерфейса. if ($once=0) do={ # Если выполняется впервые - действуем. set $once 1; # Отмечаем как выполненное. log info "$scriptName: Uplink IF DOWN"; set $UplinkFailoverLastTime "$[$scdGt]"; set $UplinkFailoverTimes ($UplinkFailoverTimes+1); log info "$scriptName: state changed $FailoverState->1"; set $FailoverState 1; # Устанавливаем статус "1". [$beepM fArr=({"1046.5"; "1318.5"; "1568.0"; "1760.0"; "1975.0"}) dArr=({"150"}) lArr=({"150"})]; [$tunlIf scriptName=$scriptName tunlDo="disable"]; # Отключаем PPTP/L2TP интерфейсы. [$vrrpIf scriptName=$scriptName vrrpDo="disable"]; # Отключаем VRRP интерфейсы. log info "$scriptName: state changed $FailoverState->4"; set $FailoverState 4; # Устанавливаем статус "4". lcd backlight state=off; # Отключаем дисплей для наглядности. # Шлём на почту файл (текущий лог) и сообщение о падении физики. /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Uplink IF DOWN.\n" file=$filePath; } } else={ if ($FailoverState=4) do={ # Проверяем статус "4". if ($once=1) do={ # Если выполнено предидущее действие - стартуем. set $once 0; # Сбрасываем счётчик выполнения. log info "$scriptName: Uplink IF UP"; set $UplinkRestoreLastTime "$[$scdGt]"; set $UplinkRestoreTimes ($UplinkRestoreTimes+1); log info "$scriptName: state changed $FailoverState->3"; set $FailoverState 3; # Устанавливаем статус "3". [$beepM fArr=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) dArr=({"150"}) lArr=({"150"})]; [$vrrpIf scriptName=$scriptName vrrpDo="enable"]; # Включаем VRRP интерфейсы. if ([$vrrpSt]=true) do={ # Если все VRRP интерфейсы активны. [$brdgPt brdgEd="auto"]; # "Будим" мосты. [$tunlIf scriptName=$scriptName tunlDo="enable"]; # Включаем PPTP/L2TP интерфейсы. } log info "$scriptName: state changed $FailoverState->0"; set $FailoverState 0; # Устанавливаем статус "0". lcd backlight state=on; # Включаем дисплей для наглядности. # Шлём на почту файл (текущий лог) и сообщение о восттановлении физики. /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Uplink IF UP.\n" file=$filePath; } } } |
— Пингуем «Интернет» и фиксируем результат.
1 2 3 4 5 6 7 8 9 10 11 12 13 | if ($FailoverState<4) do={ # Если статус меньше "4" то работаем. set $conA 0; set $pingResA 0; set $conB ([len $globalIPArray]-1); # Определяем длинну массива с IP адресами. do { set $conA ($conA+$pingCount); # Считаем количество пингов. set $UplinkIPCurrentScan [($globalIPArray->$conB)]; # Берём из массива IP адрес. set $pingResA ($pingResA+[ping $UplinkIPCurrentScan count=$pingCount]); # Считаем кол-во ответов. set $conB ($conB-1); # Уменьшакем счётчик цикла. delay "$(([len $globalIPArray]*$pingCount)*100) ms"; # Тайм-аут. Счиается из расчёта 100 мс на один пинг. } while ($conB>-1); set $UplinkDrops ($UplinkDrops+($conA-$pingResA)); # Считаем количество потерянных пакетов. } |
— На основании полученного результата, включаем/отключаем интерфейсы. Концепция следующая. Если из всех попыток пинга ни одна не привела к успеху, то отключаем все VRRP (+ PPTP/L2TP) интерфейсы и переходим в статус «2», если попытка пинга удалась, то включаем все VRRP (+ PPTP/L2TP) интерфейсы и переходим в статус «0». Дополнительно предусмотрена защита от «забора». Часто бывает так, что соединение с интернет может пропасть и снова появиться несколько раз с высокой периодичностью. Чтобы избежать прыжков с маршрутизатора на маршрутизатор, устанавливается тайм-аут. Если падение произошло более чем 1 раз, активируется алгоритм, который выдерживает заданные в массиве тайм-ауты последовательно, плюс время выполнения основного цикла. В моём случае один цикл равен примерно одной минуте. Это зависит от количества пингуемых IP адресов и выделенного для каждого пинга промежутка времени. Исчерпав все возможные тайм-ауты, отключаем аплинк интерфейс и шлём письмо о том, что «Бобик сдох».
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | if ($pingResA=0) do={ # Если результат пинга равен нулю. if ($FailoverState=0) do={ # Если статус "0". log info "$scriptName: connection to the Internet lost"; set $UplinkFailoverLastTime "$[$scdGt]"; set $UplinkFailoverTimes ($UplinkFailoverTimes+1); log info "$scriptName: state changed $FailoverState->1"; set $FailoverState 1; # Устанавливаем статус "1". # Звучим о потере соединения. [$beepM fArr=({"1046.5"; "1318.5"; "1568.0"; "1760.0"; "1975.0"}) dArr=({"150"}) lArr=({"150"})]; [$tunlIf scriptName=$scriptName tunlDo="disable"]; # Отключаем PPTP/L2TP интерфейсы. [$vrrpIf scriptName=$scriptName vrrpDo="disable"]; # Отключаем VRRP интерфейсы. log info "$scriptName: state changed $FailoverState->2"; set $FailoverState 2; # Устанавливаем статус "2". lcd backlight state=off; # Отключаем дисплей для наглядности. # Шлём на почту файл (текущий лог) и сообщение об отсутствии подключения. /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Connection to the Internet lost."; } } else={ # Если результат пинга не равен нулю. if ($FailoverState=2) do={ # Если статус "2". log info "$scriptName: connection to the Internet established"; set $UplinkRestoreLastTime "$[$scdGt]"; set $UplinkRestoreTimes ($UplinkRestoreTimes+1); log info "$scriptName: state changed $FailoverState->3"; set $FailoverState 3; # Устанавливаем статус "3". # Звучим о восттановлении соединения. [$beepM fArr=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) dArr=({"150"}) lArr=({"150"})]; [$vrrpIf scriptName=$scriptName vrrpDo="enable"]; # Включаем VRRP интерфейсы. if ([$vrrpSt]=true) do={ # Если все VRRP интерфейсы активны. [$brdgPt brdgEd="auto"]; # "Будим" мосты. [$tunlIf scriptName=$scriptName tunlDo="enable"]; # Включаем PPTP/L2TP интерфейсы. } log info "$scriptName: state changed $FailoverState->0"; set $FailoverState 0; # Устанавливаем статус "0". lcd backlight state=on; # Включаем дисплей для наглядности. # Шлём на почту файл (текущий лог) и сообщение о восстановлении подключения. /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Connection to the Internet established."; } } # Принцип работы тайм-аутов следующий. Если "падений" более одного, то устанавливаем первый тайм-аут из заданных в массиве # и устанавливаем счётчик тайм-аутов равным единице. По истечению тайм-аута, пробуем использовать аплинк. Если попытка удалась, # то ждём истечения времени заданного в последней позиции массива для сброса счётчика тайм-аутов, если нет, то устанавливаем # следующий тайм-аут из массива и увеличиваем значение счётчика на единицу. Если все тайм-ауты израсходованы, то отключаем # аплинк-интерфейс и отсылаем письмо об ошибке. if ($FailoverState=2) do={ # Если статус "2". if ($UplinkFailoverTimes>1) do={ # Если "падений" более одного. set $conC 1; # Устанавливаем наличие тайм-аута. set $rdLe [len $restoreDelays]; # Определяем длинну массива с тайм-аутами. set $RestoreDelay ($RestoreDelay+1); # Устанавливаем номер следующего тайм-аута. # Если номер следующего тайм-аута больше длинны массива с тайм-аутами. if ($RestoreDelay>$rdLe) do={ set $FailoverState 4; # Устанавливаем статус "4". # Устанавливаем время следующего восстановления "бесконечность". set $RestoreDelayTime "infinit"; set $UplinkDisabled "true"; # Устанавливаем статус аплинка "отключен". [/interface disable $uplink;] # Отключаем аплинк интерфейс. # Отправляем письмо о том, что аплинк-интерфейс отключен. /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Error occurred! Uplink interface $uplink disabled.\n" file=$filePath; } } } if ($FailoverState=0) do={ # Если статус "0". if ($RestoreDelay>0) do={ # Если номер следующего тайм-аута больше нуля. # Если ( "текущая дата и время" минус "дата и время последнего восстановления аплинка" ) больше чем # "последний тайм-аут в массиве тайма-утов". if ((([$datr fdte=[$scdGt]]->0)-([$datr fdte=$UplinkRestoreLastTime]->0))>[($restoreDelays->($rdLe-1))]) do={ set $RestoreDelay 0; # Устанавливаем номер следующего тайм-аута "0". set $RestoreDelayTime "0 s"; # Устанавливаем время следующего восстановления "0 секунд". } } } # Если "падений" более нуля, устанавливаем время следующего восстановления из массива тайм-аутов под номером # следующего тайм-аута. if ($UplinkFailoverTimes>0) do={set $RestoreDelayTime "$[($restoreDelays->$RestoreDelay)] s";} } else={ # Если тайм-аут присутствует (больше 0). # Если номер следующего тайм-аута больше нуля. if ($RestoreDelay>0) do={ # Если ( "текущая дата и время" минус "дата и время последнего "падения" аплинка" ) больше чем # "текущее значение тайм-аута" в массиве тайма-утов", устанавливаем что тайм-аут отсутствует (значение "0"). if ((([$datr fdte=[$scdGt]]->0)-([$datr fdte=$UplinkFailoverLastTime]->0))>[($restoreDelays->($RestoreDelay-1))]) do={set $conC 0;} # Если номер следующего тайм-аута равен нулю или меньше, устанавливаем что тайм-аут отсутствует (значение "0"). } else={set $conC 0;} } # Вычисляем оставшееся время до окончания тайм-аута. set $rdtrs ((([$datr fdte=$UplinkFailoverLastTime]->0)+[($restoreDelays->($RestoreDelay-1))])-([$datr fdte=[$scdGt]]->0)); # Если оставшееся время до окончания тайм-аута больше нуля, то устанавливаем значение оставшегося времени равным оставшемуся времени, # если нет, то устанавливаем значение оставшегося времени равным нулю (выполняется один раз за цикл). if ($rdtrs>0) do={set $RestoreDelayTimeRemaining "$rdtrs s";} else={set $RestoreDelayTimeRemaining "0 s";} |
— Управляем PPTP/L2TP интерфейсами и «будим» мосты по состоянию VRRP интерфейсов.
1 2 3 4 5 6 7 8 9 | set $vicS [$vrrpSt]; # "Запоминаем" статус VRRP интерфейсов. if ([$vrrpSt]=true) do={ # Если все VRRP интерфейсы активны. [$brdgPt brdgEd="auto"]; # "Будим" мосты. [$tunlIf scriptName=$scriptName tunlDo="enable"] # Включаем PPTP/L2TP интерфейсы. } else={ # Если не все VRRP интерфейсы активны. [$brdgPt brdgEd="yes"]; # "Будим" мосты. [$tunlIf scriptName=$scriptName tunlDo="disable"]; # Отключаем PPTP/L2TP интерфейсы. } |
— Управление локальными сервисами. В самом начале, в массиве я задал IP адреса своего домашнего сервера в разных сетях «10.0.0.4», «10.0.1.4» и тд. Идея следующая. Если с сервером беда — то начинаем выдавать IP с DHCP сервера MikroTik’а где в качестве DNS указан IP VRRP интерфейса этого MikroTik’а. Все DHCP-сервера на MikroTik’е я назвал именами IP адресов своего сервера (в соответствии с адресацией для каждой сети) и назначил на соответствующие VRRP интерфейсы.
Таким образом, мне не нужно задавать отдельно имена DHCP-серверов MikroTik’а, а отключать их по номеру подобно VRRP интерфейсам мне показалось не совсем правильным, так как возможно что помимо искомых DHCP серверов могут быть и другие, работающие отдельно от основной инфраструктуры.
Основной цикл этой функции выполняется при условии отсутствия изменений в конфигурации VRRP интерфейсов. Если же таковые обнаружены, цикл прерывается. Это нужно для того, чтобы в момент «переезда» VRRP интерфейсов, не включались локальные DHCP-сервера из-за отсутствия пинга на сервер. Как выяснилось, в момент изменений на VRRP в MikroTik’е практически всё «замирает» на некоторое время (последствия всё той-же проблемы с мостами).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | set $conD ([len $localIPArray]-1); # Определяем длинну массива с IP адресами. do { if ([$vrrpSt]=$vicS) do={ # Если изменений в конфигурации VRRP интерфейсов нет. set $LocalIPCurrentScan [($localIPArray->$conD)]; # Берём из массива IP адрес. set $pingResB [ping $LocalIPCurrentScan count=$pingCount]; # Считаем кол-во ответов. set $LocalDrops ($LocalDrops+($pingCount-$pingResB)); # Считаем количество потерянных пакетов. set $conD ($conD-1); delay "$(([len $localIPArray]*$pingCount)*100) ms"; # Тайм-аут. Счиается из расчёта 100 мс на один пинг. if ($pingResB=0) do={ # Если результат пинга равен нулю. if ([/ip dhcp-server get $LocalIPCurrentScan disabled]=true) do={ # Если текущий DHCP сервер не активен. # Звучим об активации текущего встроенного DHCP сервера. [$beepM fArr=({"1046.5"; "1568.0"; "1046.5"; "1568.5"}) dArr=({"150"}) lArr=({"150"})]; log info "$scriptName: connection to $LocalIPCurrentScan lost"; [/ip dhcp-server enable $LocalIPCurrentScan;] # Активируем текущий DHCP сервер. log info "$scriptName: enable dhcp-server $LocalIPCurrentScan"; set $LocalFailoverLastTime "$[$scdGt]"; set $LocalFailoverTimes ($LocalFailoverTimes+1); lcd interface display $displayOnError; } } else={ # Если результат пинга не равен нулю. if ($LocalFailoverTimes<20) do={ # Если потерь соединений меньше 20. if ([/ip dhcp-server get $LocalIPCurrentScan disabled]=false) do={ # Если текущий DHCP сервер активен. # Звучим об отключении текущего встроенного DHCP сервера. [$beepM fArr=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) dArr=({"100"}) lArr=({"100"})]; log info "$scriptName: connection to $LocalIPCurrentScan established"; [/ip dhcp-server disable $LocalIPCurrentScan;] # Отключаем текущий DHCP сервер. log info "$scriptName: disable dhcp-server $LocalIPCurrentScan"; set $LocalRestoreLastTime "$[$scdGt]"; set $LocalRestoreTimes ($LocalRestoreTimes+1); lcd interface display $displayOnRestore; } } else={ # Если потерь соединений больше 20. set $LocalDisabled "true"; # Отключаем локальные сервисы. # Шлём на почту файл (текущий лог) и сообщение о том что локальные сервисы отключены. /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Multiple errors! Local services disabled!\n" file=$filePath; } } } else={ # Если произошли изменения в конфигурации VRRP интерфейсов. set $conD -1; # Завершаем цикл. } } while ($conD>-1); |
— Завершаем.
1 2 3 4 5 6 | } while (true); } # В случае сбоя в работе основного цикла, шлём письмо о сбое прикрепляя файл (текущий лог). /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Error occurred! Unexpected end of cycle.\n" file=$filePath; }; |
Вот собственно и всё. Такой вот не замысловатый скрипт спас ситуацию с систематическими падениями сети и сэкономил кучу времени и нервных клеток. Теперь, даже в случае стандартного «выходного» в работе той или иной сети на наличии интернет соединения у меня дома это ни как не влияет.
Коротко о статусах
0 — Работа в штатном режиме.
1 — Утеряно соединение с интернет, переход в режим отключения интерфейсов.
2 — Интерфейсы отключены, режим ожидания возобновления соединенеия.
3 — Соединение с интернет восстановлено, переход в режим включения интерфейсов.
4 — В аплинк интерфейс не подключен кабель или проблемы с физическим соединением.
PS. Думаю внимательный читатель смог заметить и озадачится вопросом, как же скрипт отправит почту если соединение с интернет разорвано? Элементарно Ватсон! Напоминаю, что активным шлюзом является VRRP интерфейс, а это означает что он там, где есть интернет. Соответственно, если прописать статический маршрут для IP почтового сервера и DNS на IP VRRP интерфейса, то почта будет отправлена через активное соединение на соседнем маршрутизаторе.
Дополнительно хочу добавить, что для отправки почты в том виде, в котором это происходит в данном скрипте, необходимо внести соответствующие настройки в Tools -> Email.
UPD: Дальнейшее развитие скрипта смотрите здесь.
Лучшие практики отказоустойчивости от Дмитрия Скромнова в русскоязычном онлайн-курсе по MikroTik для самостоятельного изучения. Курс по настройке MikroTik и RouterOS основан на официальной программе MTCNA, однако содержит намного больше полезной информации. 162 видеоурока и большая практическая задача, разбитая на 45 лабораторных работ. Время на изучение неограниченно – все материалы передаются бессрочно и их можно пересматривать сколько нужно. Первые 25 уроков можно посмотреть бесплатно, оставив заявку на странице курса.
Скрипт полностью.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 | beep frequency=1760 length=10ms; :local scriptName "Failover"; :if ([len [/system script job find where script=$scriptName]]>1) do={ log error "$scriptName: Single instance"; beep frequency=440 length=1000ms; } else={ delay 60s; :local globalIPArray {"8.8.8.8";"77.88.8.8";"193.58.251.251";"114.114.115.115"}; :local localIPArray {"10.0.0.4";"10.0.1.4";"10.0.2.4";"172.16.0.4";"192.168.0.4"}; :local restoreDelays {600;7200;21600;43200;86400}; :local filePath "disk1/var/log/full_log.0.txt"; :local email "mail@example.com"; :local displayOnRestore "uplink0"; :local displayOnError "bond1.0"; :local uplink "uplink0"; :local pingCount 5; :local conA 0; :local conB 0; :local conD 0; :local conC 0; :local once 0; :local pingResA 0; :local pingResB 0; :local mailSubject ([/system identity get name]."::".$scriptName); :global FailoverState 0; :global UplinkIPCurrentScan "waiting..."; :global LocalIPCurrentScan "waiting..."; :global RestoreDelay 0; :global RestoreDelayTime "0 s"; :global RestoreDelayTimeRemaining "0 s"; :global UplinkDrops 0; :global UplinkDisabled "false"; :global UplinkFailoverTimes 0; :global UplinkFailoverLastTime "-"; :global UplinkRestoreTimes 0; :global UplinkRestoreLastTime "-"; :global LocalDrops 0; :global LocalDisabled "false"; :global LocalFailoverTimes 0; :global LocalFailoverLastTime "-"; :global LocalRestoreTimes 0; :global LocalRestoreLastTime "-"; :local scdGt do={:return "$[/system clock get date] $[/system clock get time]";} :local beepM do={ set $cnt ([len $fArr]); do { if (([len $lArr])>1) do={set $lnt [($lArr->$cnt)];} else={set $lnt [($lArr->0)];} if (([len $dArr])>1) do={set $dnt [($dArr->$cnt)];} else={set $dnt [($dArr->0)];} execute "beep frequency=$[($fArr->$cnt)] length=$($lnt)ms "; delay "$($dnt+1) ms"; set $cnt ($cnt-1); set $lnt ($lnt-1); } while ($cnt>-1); delay 500ms; } :local datr do={ :local dfy 0; :local yar [pick $fdte 7 11]; :local mth [pick $fdte 0 3]; :local day [pick $fdte 4 6]; :local hur [pick $fdte 12 14]; :local min [pick $fdte 15 17]; :local sec [pick $fdte 18 20]; :local mts ("jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"); :local mtd {0;31;28;31;30;31;30;31;31;30;31;30;31}; :local mtn ([find $mts $mth -1]+1); :local yrd (($yar*365)+($yar / 4)); :local cmd ($mtd->$mtn); :local conA $mtn; do { set $conA ($conA-1); set $dfy ($dfy+($mtd->$conA)); if ($conA=0) do={set $dfy ($dfy+$day);} } while ($conA>0); if ($mtn<=9) do={set mtn ("0".$mtn);} return {((($yrd+$dfy)*86400)+($hur*3600)+($min*60)+$sec);($day."-".$mtn."-".$yar."-".$hur."-".$min)}; } :local vrrpIf do={ if ($vrrpDo="enable") do={set $vrrpSt false;} else={set $vrrpSt true;} :local vrrpIfCount [/interface vrrp print count-only;]; do { set $vrrpIfCount ($vrrpIfCount-1); if ($vrrpSt=true || [/interface vrrp get $vrrpIfCount backup]!=true) do={ if ([/interface vrrp get $vrrpIfCount running]=$vrrpSt) do={ beep frequency=1760 length=10ms; log info "$scriptName: $vrrpDo VRRP interface $[/interface vrrp get $vrrpIfCount name]"; execute "/interface vrrp $vrrpDo $vrrpIfCount"; do {delay 10ms;} while ([/interface vrrp get $vrrpIfCount running]=$vrrpSt); } } } while ($vrrpIfCount>0); } :local vrrpSt do={ :local vrrpIfCount [/interface vrrp print count-only;]; do { set $vrrpIfCount ($vrrpIfCount-1); if ([/interface vrrp get $vrrpIfCount running]=true) do={ set $out true; } else={ set $vrrpIfCount 0; set $out false; } } while ($vrrpIfCount>0); return $out; } :local brdgPt do={ :local brdgPtCount [/interface bridge port print count-only;]; do { set $brdgPtCount ($brdgPtCount-1); if ([/interface bridge port get $brdgPtCount edge]!=$brdgDo) do={ execute "/interface bridge port set edge=$brdgEd numbers=$brdgPtCount"; } } while ($brdgPtCount>0); } :local tunlIf do={ if ($tunlDo="enable") do={set $tunlSt true;} else={set $tunlSt false;} :local pptIfCount [/interface pptp-client print count-only;]; :local l2tpIfCount [/interface l2tp-client print count-only;]; do { set $pptIfCount ($pptIfCount-1); if ([/interface pptp-client get $pptIfCount disabled]=$tunlSt) do={ beep frequency=1760 length=10ms; log info "$scriptName: $tunlDo PPTP interface $[/interface pptp-client get $pptIfCount name]"; execute "/interface pptp-client $tunlDo $pptIfCount"; do {delay 10ms;} while ([/interface pptp-client get $pptIfCount disabled]=$tunlSt); } } while ($pptIfCount>0); do { set $l2tpIfCount ($l2tpIfCount-1); if ([/interface l2tp-client get $l2tpIfCount disabled]=$tunlSt) do={ beep frequency=1760 length=10ms; log info "$scriptName: $tunlDo L2TP interface $[/interface l2tp-client get $pptIfCount name]"; execute "/interface l2tp-client $tunlDo $l2tpIfCount"; do {delay 10ms;} while ([/interface l2tp-client get $l2tpIfCount disabled]=$tunlSt); } } while ($l2tpIfCount>0); } lcd backlight state=on; [$brdgPt brdgEd="yes"]; [/interface enable $uplink;] lcd interface display $displayOnRestore; [$beepM fArr=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) dArr=({"100"}) lArr=({"100"})]; /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Started.\n" file=$filePath; if ($FailoverState=0) do={ [$beepM fArr=({"1975"; "1975"}) dArr=({"1000"; "150"}) lArr=({"100"})]; do { if ($conC<1) do={ if ([/interface get $uplink running]=false) do={ if ($once=0) do={ set $once 1; log info "$scriptName: Uplink IF DOWN"; set $UplinkFailoverLastTime "$[$scdGt]"; set $UplinkFailoverTimes ($UplinkFailoverTimes+1); log info "$scriptName: state changed $FailoverState->1"; set $FailoverState 1; [$beepM fArr=({"1046.5"; "1318.5"; "1568.0"; "1760.0"; "1975.0"}) dArr=({"150"}) lArr=({"150"})]; [$tunlIf scriptName=$scriptName tunlDo="disable"]; [$vrrpIf scriptName=$scriptName vrrpDo="disable"]; log info "$scriptName: state changed $FailoverState->4"; set $FailoverState 4; lcd backlight state=off; /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Uplink IF DOWN.\n" file=$filePath; } } else={ if ($FailoverState=4) do={ if ($once=1) do={ set $once 0; log info "$scriptName: Uplink IF UP"; set $UplinkRestoreLastTime "$[$scdGt]"; set $UplinkRestoreTimes ($UplinkRestoreTimes+1); log info "$scriptName: state changed $FailoverState->3"; set $FailoverState 3; [$beepM fArr=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) dArr=({"150"}) lArr=({"150"})]; [$vrrpIf scriptName=$scriptName vrrpDo="enable"]; if ([$vrrpSt]=true) do={ [$brdgPt brdgEd="auto"]; [$tunlIf scriptName=$scriptName tunlDo="enable"]; } log info "$scriptName: state changed $FailoverState->0"; set $FailoverState 0; lcd backlight state=on; /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Uplink IF UP.\n" file=$filePath; } } } if ($FailoverState<4) do={ set $conA 0; set $pingResA 0; set $conB ([len $globalIPArray]-1); do { set $conA ($conA+$pingCount); set $UplinkIPCurrentScan [($globalIPArray->$conB)]; set $pingResA ($pingResA+[ping $UplinkIPCurrentScan count=$pingCount]); set $conB ($conB-1); delay "$(([len $globalIPArray]*$pingCount)*100) ms"; } while ($conB>-1); set $UplinkDrops ($UplinkDrops+($conA-$pingResA)); } if ($pingResA=0) do={ if ($FailoverState=0) do={ log info "$scriptName: connection to the Internet lost"; set $UplinkFailoverLastTime "$[$scdGt]"; set $UplinkFailoverTimes ($UplinkFailoverTimes+1); log info "$scriptName: state changed $FailoverState->1"; set $FailoverState 1; [$beepM fArr=({"1046.5"; "1318.5"; "1568.0"; "1760.0"; "1975.0"}) dArr=({"150"}) lArr=({"150"})]; [$tunlIf scriptName=$scriptName tunlDo="disable"]; [$vrrpIf scriptName=$scriptName vrrpDo="disable"]; log info "$scriptName: state changed $FailoverState->2"; set $FailoverState 2; lcd backlight state=off; /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Connection to the Internet lost."; } } else={ if ($FailoverState=2) do={ log info "$scriptName: connection to the Internet established"; set $UplinkRestoreLastTime "$[$scdGt]"; set $UplinkRestoreTimes ($UplinkRestoreTimes+1); log info "$scriptName: state changed $FailoverState->3"; set $FailoverState 3; [$beepM fArr=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) dArr=({"150"}) lArr=({"150"})]; [$vrrpIf scriptName=$scriptName vrrpDo="enable"]; if ([$vrrpSt]=true) do={ [$brdgPt brdgEd="auto"]; [$tunlIf scriptName=$scriptName tunlDo="enable"]; } log info "$scriptName: state changed $FailoverState->0"; set $FailoverState 0; lcd backlight state=on; /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Connection to the Internet established."; } } if ($FailoverState=2) do={ if ($UplinkFailoverTimes>1) do={ set $conC 1; set $rdLe [len $restoreDelays]; set $RestoreDelay ($RestoreDelay+1); if ($RestoreDelay>$rdLe) do={ set $FailoverState 4; set $RestoreDelayTime "infinit" set $UplinkDisabled "true"; [/interface disable $uplink;] /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Error occurred! Uplink interface $uplink disabled.\n" file=$filePath; } } } if ($FailoverState=0) do={ if ($RestoreDelay>0) do={ if ((([$datr fdte=[$scdGt]]->0)-([$datr fdte=$UplinkRestoreLastTime]->0))>[($restoreDelays->($rdLe-1))]) do={ set $RestoreDelay 0; set $RestoreDelayTime "0 s"; } } } if ($UplinkFailoverTimes>0) do={set $RestoreDelayTime "$[($restoreDelays->$RestoreDelay)] s";} } else={ if ($RestoreDelay>0) do={ if ((([$datr fdte=[$scdGt]]->0)-([$datr fdte=$UplinkFailoverLastTime]->0))>[($restoreDelays->($RestoreDelay-1))]) do={set $conC 0;} } else={set $conC 0;} } set $rdtrs ((([$datr fdte=$UplinkFailoverLastTime]->0)+[($restoreDelays->($RestoreDelay-1))])-([$datr fdte=[$scdGt]]->0)); if ($rdtrs>0) do={set $RestoreDelayTimeRemaining "$rdtrs s";} else={set $RestoreDelayTimeRemaining "0 s";} set $vicS [$vrrpSt]; if ([$vrrpSt]=true) do={ [$brdgPt brdgEd="auto"]; [$tunlIf scriptName=$scriptName tunlDo="enable"]; } else={ [$brdgPt brdgEd="yes"]; [$tunlIf scriptName=$scriptName tunlDo="disable"]; } set $conD ([len $localIPArray]-1); do { if ([$vrrpSt]=$vicS) do={ set $LocalIPCurrentScan [($localIPArray->$conD)]; set $pingResB [ping $LocalIPCurrentScan count=$pingCount]; set $LocalDrops ($LocalDrops+($pingCount-$pingResB)); set $conD ($conD-1); delay "$(([len $localIPArray]*$pingCount)*100) ms"; if ($pingResB=0) do={ if ([/ip dhcp-server get $LocalIPCurrentScan disabled]=true) do={ [$beepM fArr=({"1046.5"; "1568.0"; "1046.5"; "1568.5"}) dArr=({"150"}) lArr=({"150"})]; log info "$scriptName: connection to $LocalIPCurrentScan lost"; [/ip dhcp-server enable $LocalIPCurrentScan;] log info "$scriptName: enable dhcp-server $LocalIPCurrentScan"; set $LocalFailoverLastTime "$[$scdGt]"; set $LocalFailoverTimes ($LocalFailoverTimes+1); lcd interface display $displayOnError; } } else={ if ($LocalFailoverTimes<20) do={ if ([/ip dhcp-server get $LocalIPCurrentScan disabled]=false) do={ [$beepM fArr=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) dArr=({"100"}) lArr=({"100"})]; log info "$scriptName: connection to $LocalIPCurrentScan established"; [/ip dhcp-server disable $LocalIPCurrentScan;] log info "$scriptName: disable dhcp-server $LocalIPCurrentScan"; set $LocalRestoreLastTime "$[$scdGt]"; set $LocalRestoreTimes ($LocalRestoreTimes+1); lcd interface display $displayOnRestore; } } else={ set $LocalDisabled "true"; /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Multiple errors! Local services disabled!\n" file=$filePath; } } } else={ set $conD -1; } } while ($conD>-1); } while (true); } /tool e-mail send from=" " to=$email subject=$mailSubject body="$mailSubject:: $[$scdGt]:: Error occurred! Unexpected end of cycle.\n" file=$filePath; }; |
Уведомление:Мосты в MikroTik RouterOS (Bridge bugs) — Aleksov's Blog
Внимательно почитал Ваше решение — хорошая работа! Прекрасно все задокументировано.
Возникло пару вопросов:
1. Я правильно понимаю, что скрипт только на двух роутерах крутится?
2. Почему отключаете VRRP, а не переводите в backup путем понижения стоимости? Вижу потенциальные грабли — если на обоих роутерах не будет unlink, то на них выключился VRRP и у клиентских устройств не будет активного шлюза ни в одной из подсетей.
Здравствуйте. Спасибо за отзыв.
1) Верно, в моём случае на двух, однако ничего не мешает добавить в кластер болшее количество участников.
2) Да, действительно, если оба аплинка дохлые, тогда VRRP интерфейсов нет, однако обмен траффиком в пределах одного широковещательного домена остаётся. По сути можно дополнить скрипт переменной стоимости, но при двух дохлых аплинках интернет от этого не появится )). В любом случае подумаю на эту тему. Спасибо!
У меня на каждом роутере может быть несколько uplink интерфейсов.
Я думаю реализовать так:
На каждом роутере задается несколько uplink интерфейсов, для каждого — свой priority VRRP.
В зависимости от приоритета, один или другой роутер становятся мастером.
У меня на роутерах маршрутизируются несколько локальных подсетей, поэтому шлюз по-умолчанию нужен живой. )))
Здравствуйте Алексей!
Да, когда стоит задача маршрутизации нескольких сетей, естественно нужен шлюз. )))
Дальнейшее развитие скрипта смотрите здесь. https://blog.set-pro.net/отказоустойчивый-кластер-из-mikrotikов-с-эд/
Если у Вас будут еще интересные идеи и предложения, пишите!