Отказоустойчивый кластер из MikroTik’ов

Как быть когда твой провайдер мудаг?

Предистория

Сегодня я расскажу историю о том, как быть когда нужен стабильный интернет в режиме 24/7, руки и оборудование позволяют, а твой провайдер и его ближайшие соседи — имбецилы.
Дело было в далёком 2017 году в замечательный городе «С», который отличается несколькими достопримечательностями:
— Свежий воздух;
— Мягкий климат;
— Бесхребетный people (без обобщений).

oleni

C первыми двумя пунктами всё замечтательно, но вот последний, без преувеличения и обобщения являет собой корень зла сей географической точки. Это тебе и пообещал — проставил, и сервис ниже плинтуса, и нахамить клиенту — ачивмент. Одним словом — печаль! Естественно, не обошло стороной сие горе и сферу телекоммуникаций. Здесь тебе и дропы в полный рост, и купленный абонетом «белый» IP пилят в тихую на 100-500 хомячков, и медные воздушки между домами #over100метров, короче всё как полагается!

Спустя пару лет мытарств с оператора на оператора, я пришел к неутешительному выводу, что судьбинушка помяла их всех без разбора. Выход был один — прекратить попытки сбора сливок на говне и заняться делом. К тому времени, в нагрузку к бэдам с провайдерами на меня свалился шквал лагов со стороны моего домашнего серванта на какинтоше. Так как на нём помимо файлопомоек, плюха (Plex) и контроллера домена крутился ещё и DHCP с DNS, любой отвал серванта приводил к понятным последствиям. Дело пахло керосином и пора уже было что то решать, ибо роль сапожника без сапог уже изрядно утомила. Благо первый опыт написания скриптов для MikroTik не прошёл бесследно, да и к этому времени уже много чего было написано для данной платформы. На тот момент я оброс вторым таким же маршрутизатором и дело оставалось за малым — начать действовать.

Задача

Стала необходимость обеспечения отказоустойчивости домосетки как извне так и изнутри. Что предстояло сделать:

  1. Организовать VRRP кластер;
  2. Обеспечить переезд сервисов маршрутизатора при сбое (PPTP/L2TP);
  3. Подстраховаться на случай падения внутреннего сервера (переезд DNS+DHCP);
  4. Получать извещения на почту когда кто-то облажался.

Условия

  • Два нерадивых провайдера;
  • Два 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
VRRP интерфейсы на маршрутизаторе №1
VRRP
VRRP интерфейсы на маршрутизаторе №2

Это собственно и всё что касается конфигурации VRRP. Теперь зверушки начали общаться друг с другом, что говорит об успехе мероприятия.

vrrp announce
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 .

Вывод глобальных переменных в environment
Вывод глобальных переменных в environment

— Объявляем функции.
*И вот именно в этот момент я полез править работающий скрипт. Как результат — 2 функции вместо 6 и минус 2 часа жизни :р. Потом мне не понравились 4 функции звуковых сигналов… Спустя 2 часа осталась одна, продолжаю.
grabli
В процессе эксплуатации были обнаружены эпические «грабли» с мостами, для решения которых пришлось как обычно «прикручивать костыли». Подробнее об этих и прочих «граблях», а так же о их решениях можно почитать здесь, а в данном случае, плюс ещё одна функция и трафик между 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-server
DHCP сервера на MikroTik’е

Таким образом, мне не нужно задавать отдельно имена 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 интерфейса, то почта будет отправлена через активное соединение на соседнем маршрутизаторе.

default routes
Статические маршруты для отправки почты

Дополнительно хочу добавить, что для отправки почты в том виде, в котором это происходит в данном скрипте, необходимо внести соответствующие настройки в 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
366
367
368
 
 
 
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’ов: 6 комментариев

  1. Внимательно почитал Ваше решение — хорошая работа! Прекрасно все задокументировано.
    Возникло пару вопросов:
    1. Я правильно понимаю, что скрипт только на двух роутерах крутится?
    2. Почему отключаете VRRP, а не переводите в backup путем понижения стоимости? Вижу потенциальные грабли — если на обоих роутерах не будет unlink, то на них выключился VRRP и у клиентских устройств не будет активного шлюза ни в одной из подсетей.

    1. Здравствуйте. Спасибо за отзыв.
      1) Верно, в моём случае на двух, однако ничего не мешает добавить в кластер болшее количество участников.
      2) Да, действительно, если оба аплинка дохлые, тогда VRRP интерфейсов нет, однако обмен траффиком в пределах одного широковещательного домена остаётся. По сути можно дополнить скрипт переменной стоимости, но при двух дохлых аплинках интернет от этого не появится )). В любом случае подумаю на эту тему. Спасибо!

  2. У меня на каждом роутере может быть несколько uplink интерфейсов.
    Я думаю реализовать так:
    На каждом роутере задается несколько uplink интерфейсов, для каждого — свой priority VRRP.
    В зависимости от приоритета, один или другой роутер становятся мастером.

    1. У меня на роутерах маршрутизируются несколько локальных подсетей, поэтому шлюз по-умолчанию нужен живой. )))

      1. Здравствуйте Алексей!
        Да, когда стоит задача маршрутизации нескольких сетей, естественно нужен шлюз. )))
        Дальнейшее развитие скрипта смотрите здесь. https://blog.set-pro.net/отказоустойчивый-кластер-из-mikrotikов-с-эд/
        Если у Вас будут еще интересные идеи и предложения, пишите!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.