Около 2,5 лет назад писал статью на тему автоматического переключения канала Интернет на резервный. Скрипт, конечно, и по сей день работает «на отлично», но его внешний вид и некоторые нюансы…
Итак, встала задача улучшить скрипт, максимально устранив побочные эффекты. Что ж, приступим.
В нашем распоряжении Mikrotik RB850Gx2, для которого мы будем писать скрипт (его работоспособность также проверена на моделях RB450G и RB951G-2HnD).
Что же будем в нем использовать?
Итак, для начала определим необходимые переменные:
где:
$firstInterface — имя нашего PPPoE-соединения основной линии связи.
$secondInterface — имя нашего Ethernet-соединения резервной линии.
$pingTo1 и $pingTo2 — IP-адреса ресурсов, которые будем «пинать».
$pingCount — количество пингов на каждый IP-адрес
$stableConnectFrom — процентное соотношение для определения «стабильности» Интернет-канала. Например, в нашем случае при потере свыше 30% пакетов пинга (`$pingStatus < $stableConnectFrom`, об этом ниже) будет произведено переключение канала на резервный.
$prefix — будем использовать для вывода логов, чтобы нам было виден нужный текст.
$firstInterfaceName — переменная, в которую «запомним» имя интерфейса, дабы исключить конструкцию вида `[get $firstInterface gateway]`, которая была в прошлом примере
При изменении дистанции роута без очистки ARP таблицы будут возникать ошибки и так как в нашем коде есть несколько мест, откуда необходимо вызвать очистку данной таблицы, оформим код в функцию:
Далее, для уменьшения строк кода, мы используем конструкцию `/ip route {}`, внутри которой нельзя вызывать другие корневые команды, например, `/interface pppoe-client...`. Поэтому мы создадим еще одну функцию, отвечающую за переподключение PPPoE-соединения:
В нашем случае в качестве основной линии используется «Ростелеком», который имеет «фишку» в периодическом отключении Интернета при том, что внутренняя их линия работает без сбоев. Это достигли опытным путем, используя SIP-сервер от Ростелеком, который так ни разу и не упал.
В общем, создали функцию переподключения соединения. Идем дальше.
Перед основной частью добавим «защиту от дураков». Вдруг у нас интерфейс выключен, или дистанция не правильная…
Итак, проверяем активны ли интерфейсы и если нет — активируем их:
Но это еще не все! Дальше мы проверим выставленные дистанции роутов:
Так как основная часть работает в `/ip route`, мы добавили «защиту от дураков» в ее начало.
В нашем случае используем 2 роута с `dst. address` равным `0.0.0.0/0`, шлюзом по имени интерфейса и «какой-то» дистанцией.
Что делает скрипт:
Вначале он присваивает переменным объекты для каждого из роутов, чтобы меньше кода писать.
Затем, проверяем дистанцию основного роута (Ростелеком). Мы используем значение «2».
После этого проверяем дистанцию запасного роута — он может принимать значение дистанций либо «1», либо «3». Это упрощает задачу с изменением дистанций, так как нет необходимости изменять дистанцию основного канала.
Проверили дистанции и выставили нужные. Что дальше?
Так как интерфейс может быть неактивен, то в первоначальном условии используем конструкцию не только проверки пинга, но и статуса интерфейса:
Так как решено ввести процент для выявления «стабильности» канала, была добавлена локальная переменная `$pingStatus`, в которую производится сохранение процентного соотношения успешных пакетов к общему их числу (спасибо esudnik за примечание).
То есть, если интерфейс выключен или процент успешных пакетов ниже указанного нами в переменной `$stableConnectFrom`, считаем, что Интернет «отсох».
Обратите внимание на строку `[/ping $pingTo1 interface=$firstInterfaceName count=$pingCount]`, где `interface=...` — это жестко указанный интерфейс, через который будем проверять пинг. Так как «падает» основной, то и укажем его имя. Этот метод исключает необходимость добавления правил блокировки трафика определенного канала с определенным IP-адресом на вкладке файервола.
Комментаторы статьи ( icCE и mafet) обратили внимание на то, что часто пинги на один ресурс могут пропадать, поэтому мы ввели 2 ресурса для «пинков».
Вначале выводим лог, что Инета нет, после чего изменяем дистанцию резервного канала и чистим таблицу ARP, вызвав функцию.
После этого вызываем функцию переподключения PPPoE-соединения. Так как имя соединения мы указываем в одном месте (чтобы не плодить переменные), функция написана с учетом принятия переменной, содержащей имя интерфейса. Таким образом, при вызове функции в параметр `nameInterface` мы передаем имя нужного нам PPPoE-интерфейса.
На этом-то этапе мы и заюзаем функцию `if else`:
Как и в первой части функции, выводим сообщение о доступности сети, после чего изменяем дистанцию резервного канала и чистим таблицу ARP.
В поле `Name` введите имя записи, чтобы не запутаться.
Поле `Start Time` я выставил `00:00:00` для запуска ровно в полночь.
Интервал — 30 секунд
В поле `On Event` вписываем имя нашего скрипта — `script-check-inet`
И жмем «ОК».
Вот, собственно, и все!
Ниже под спойлером приведен полный код скрипта.
Изначально скрипт был написан и проверен на устройствах Mikrotik RB450G и RB951G-2HnD (ядро `mipsbe`), но при запуске на модели RB850Gx2 (ядро `powerpc`) возникала ошибка в строке: `[/ping $pingTo interface=[get $firstInterface gateway] count=5]`, а именно в отказе обработки `[get $firstInterface gateway]` внутри `[/ping… ]`.
Так как мы ввели дополнительную переменную `$firstInterfaceName`, то удалось изменить эту конструкцию и добились работоспособности скрипта на всех перечисленных устройствах.
Код скрипта в очередной раз был обновлен. esudnik обратил внимание на проблему большого количества потерянных пакетов при активном подключении к сети. Таким образом, мы ввели дополнительную переменную (`stableConnectFrom`), используемую для определения процентного соотношения «качества» линии.
В примере указано значение переменной равное «70». Это значит, если процент успешно отправленных пакетов будет ниже 70%, скрипт активирует резервный канал.
Как заметил icCE, при использовании скрипта на прошивке 6.36rc10, выходит ошибка:
Прошивке не нравится "* 100". Решение проблемы было простым — обрамить вычисляемые значения в дополнительные скобки, получив
Текст скрипта в статье обновил.
Итак, встала задача улучшить скрипт, максимально устранив побочные эффекты. Что ж, приступим.
В нашем распоряжении Mikrotik RB850Gx2, для которого мы будем писать скрипт (его работоспособность также проверена на моделях RB450G и RB951G-2HnD).
Для начала, добавим новый скрипт
Присвойте скрипту имя, например, script-check-inet
Присвойте скрипту имя, например, script-check-inet
Что же будем в нем использовать?
Итак, для начала определим необходимые переменные:
:local firstInterface "pppoe-rostelecom";
:local secondInterface "eth2-MTS";
:local pingTo1 "8.8.8.8";
:local pingTo2 "217.112.35.75";
:local pingCount 5;
:local stableConnectFrom 70;
:local prefix ">>> ";
:local firstInterfaceName $firstInterface;
:local secondInterfaceName $secondInterface;
где:
$firstInterface — имя нашего PPPoE-соединения основной линии связи.
$secondInterface — имя нашего Ethernet-соединения резервной линии.
$pingTo1 и $pingTo2 — IP-адреса ресурсов, которые будем «пинать».
$pingCount — количество пингов на каждый IP-адрес
$stableConnectFrom — процентное соотношение для определения «стабильности» Интернет-канала. Например, в нашем случае при потере свыше 30% пакетов пинга (`$pingStatus < $stableConnectFrom`, об этом ниже) будет произведено переключение канала на резервный.
$prefix — будем использовать для вывода логов, чтобы нам было виден нужный текст.
$firstInterfaceName — переменная, в которую «запомним» имя интерфейса, дабы исключить конструкцию вида `[get $firstInterface gateway]`, которая была в прошлом примере
При изменении дистанции роута без очистки ARP таблицы будут возникать ошибки и так как в нашем коде есть несколько мест, откуда необходимо вызвать очистку данной таблицы, оформим код в функцию:
:local clearArp do={
:local dumplist [/ip arp find]
:foreach i in=$dumplist do={
/ip arp remove $i
}
:log info ($prefix . "ARP cleaned");
}
Далее, для уменьшения строк кода, мы используем конструкцию `/ip route {}`, внутри которой нельзя вызывать другие корневые команды, например, `/interface pppoe-client...`. Поэтому мы создадим еще одну функцию, отвечающую за переподключение PPPoE-соединения:
# Function to reconnect PPPoE connection
:local reconnectPPPoE do={
/interface pppoe-client set $nameInterface disable=yes;
:delay 1s;
/interface pppoe-client set $nameInterface disable=no;
}
В нашем случае в качестве основной линии используется «Ростелеком», который имеет «фишку» в периодическом отключении Интернета при том, что внутренняя их линия работает без сбоев. Это достигли опытным путем, используя SIP-сервер от Ростелеком, который так ни разу и не упал.
В общем, создали функцию переподключения соединения. Идем дальше.
Перед основной частью добавим «защиту от дураков». Вдруг у нас интерфейс выключен, или дистанция не правильная…
Итак, проверяем активны ли интерфейсы и если нет — активируем их:
# Check FIRST interface
/interface pppoe-client {
:if ( [get $firstInterface disable] = true) do={
set $firstInterface disable=no;
:delay 5s;
}
}
# Check SECOND interface
/interface ethernet {
:if ( [get $secondInterface disable] = true) do={
set $secondInterface disable=no;
}
}
Но это еще не все! Дальше мы проверим выставленные дистанции роутов:
/ip route {
# Set objects to variables
:set firstInterface [find dst-address="0.0.0.0/0" gateway=$firstInterfaceName];
:set secondInterface [find dst-address="0.0.0.0/0" gateway=$secondInterfaceName];
# Check routes
:if ( [get $firstInterface distance] != 2 ) do={
set $firstInterface distance=2;
:log info ($prefix . "Distance for " . $firstInterfaceName . " corrected");
}
:if ( [get $secondInterface distance] != 1 && [get $secondInterface distance] != 3) do={
set $secondInterface distance=3;
:log info ($prefix . "Distance for " . $secondInterfaceName . " corrected");
}
# ... body ...
}
Так как основная часть работает в `/ip route`, мы добавили «защиту от дураков» в ее начало.
В нашем случае используем 2 роута с `dst. address` равным `0.0.0.0/0`, шлюзом по имени интерфейса и «какой-то» дистанцией.
Что делает скрипт:
Вначале он присваивает переменным объекты для каждого из роутов, чтобы меньше кода писать.
Затем, проверяем дистанцию основного роута (Ростелеком). Мы используем значение «2».
После этого проверяем дистанцию запасного роута — он может принимать значение дистанций либо «1», либо «3». Это упрощает задачу с изменением дистанций, так как нет необходимости изменять дистанцию основного канала.
Проверили дистанции и выставили нужные. Что дальше?
Проверка пингом
Так как интерфейс может быть неактивен, то в первоначальном условии используем конструкцию не только проверки пинга, но и статуса интерфейса:
/ip route {
# ...
:local pingStatus \
((( [/ping $pingTo1 interface=$firstInterfaceName count=$pingCount] + \
[/ping $pingTo2 interface=$firstInterfaceName count=$pingCount] ) / ($pingCount * 2)) * 100);
:if ( [get $firstInterface active] = false or $pingStatus < $stableConnectFrom) do={
# ...
}
# ...
}
Так как решено ввести процент для выявления «стабильности» канала, была добавлена локальная переменная `$pingStatus`, в которую производится сохранение процентного соотношения успешных пакетов к общему их числу (спасибо esudnik за примечание).
То есть, если интерфейс выключен или процент успешных пакетов ниже указанного нами в переменной `$stableConnectFrom`, считаем, что Интернет «отсох».
Обратите внимание на строку `[/ping $pingTo1 interface=$firstInterfaceName count=$pingCount]`, где `interface=...` — это жестко указанный интерфейс, через который будем проверять пинг. Так как «падает» основной, то и укажем его имя. Этот метод исключает необходимость добавления правил блокировки трафика определенного канала с определенным IP-адресом на вкладке файервола.
Комментаторы статьи ( icCE и mafet) обратили внимание на то, что часто пинги на один ресурс могут пропадать, поэтому мы ввели 2 ресурса для «пинков».
:if ( [get $firstInterface active] = false or $pingStatus < $stableConnectFrom) do={
:log info ($prefix . "FIRST NO INTERNET!!!");
# Change distance
:if ( [get $secondInterface distance] != 1 ) do={
set $secondInterface distance=1;
:log info ($prefix . "Distance for " . $secondInterfaceName . " changed");
$clearArp;
}
$reconnectPPPoE nameInterface=$firstInterfaceName;
}
Вначале выводим лог, что Инета нет, после чего изменяем дистанцию резервного канала и чистим таблицу ARP, вызвав функцию.
После этого вызываем функцию переподключения PPPoE-соединения. Так как имя соединения мы указываем в одном месте (чтобы не плодить переменные), функция написана с учетом принятия переменной, содержащей имя интерфейса. Таким образом, при вызове функции в параметр `nameInterface` мы передаем имя нужного нам PPPoE-интерфейса.
Интернет «появился», или как все вернуть назад
На этом-то этапе мы и заюзаем функцию `if else`:
/ip route {
# ...
:if ( [get $firstInterface active] = false or $pingStatus < $stableConnectFrom) do={
# ...
} else={
:log info ($prefix . "FIRST INTERNET CONNECTED");
# Change distance
:if ( [get $secondInterface distance] != 3 ) do={
set $secondInterface distance=3;
:log info ($prefix . "Distance for " . $secondInterfaceName . " changed");
$clearArp;
}
}
# ...
}
Как и в первой части функции, выводим сообщение о доступности сети, после чего изменяем дистанцию резервного канала и чистим таблицу ARP.
Как запускать
Создадим запись в шедулере
В поле `Name` введите имя записи, чтобы не запутаться.
Поле `Start Time` я выставил `00:00:00` для запуска ровно в полночь.
Интервал — 30 секунд
В поле `On Event` вписываем имя нашего скрипта — `script-check-inet`
И жмем «ОК».
Вот, собственно, и все!
Ниже под спойлером приведен полный код скрипта.
Полный код скрипта
# Set local variables
:local firstInterface "pppoe-rostelecom";
:local secondInterface "eth2-MTS";
:local pingTo1 "8.8.8.8";
:local pingTo2 "217.112.35.75";
:local pingCount 5;
:local stableConnectFrom 70;
:local prefix ">>> ";
# Local variables
:local firstInterfaceName $firstInterface;
:local secondInterfaceName $secondInterface;
# Function to cleaning ARP table
:local clearArp do={
:local dumplist [/ip arp find]
:foreach i in=$dumplist do={
/ip arp remove $i
}
:log info ($prefix . "ARP cleaned");
}
# Function to reconnect PPPoE connection
:local reconnectPPPoE do={
/interface pppoe-client set $nameInterface disable=yes;
:delay 1s;
/interface pppoe-client set $nameInterface disable=no;
}
:log info ($prefix . "START PING to $pingTo1 & $pingTo2");
# Check FIRST interface
/interface pppoe-client {
:if ( [get $firstInterface disable] = true) do={
set $firstInterface disable=no;
:delay 5s;
}
}
# Check SECOND interface
/interface ethernet {
:if ( [get $secondInterface disable] = true) do={
set $secondInterface disable=no;
}
}
/ip route {
# Set objects to variables
:set firstInterface [find dst-address="0.0.0.0/0" gateway=$firstInterfaceName];
:set secondInterface [find dst-address="0.0.0.0/0" gateway=$secondInterfaceName];
# Check routes
:if ( [get $firstInterface distance] != 2 ) do={
set $firstInterface distance=2;
:log info ($prefix . "Distance for " . $firstInterfaceName . " corrected");
}
:if ( [get $secondInterface distance] != 1 && [get $secondInterface distance] != 3) do={
set $secondInterface distance=3;
:log info ($prefix . "Distance for " . $secondInterfaceName . " corrected");
}
# Get ping successfully packets. In percent
:local pingStatus \
((( [/ping $pingTo1 interface=$firstInterfaceName count=$pingCount] + \
[/ping $pingTo2 interface=$firstInterfaceName count=$pingCount] ) / ($pingCount * 2)) * 100);
# Check Internet
:if ( [get $firstInterface active] = false or $pingStatus < $stableConnectFrom) do={
:log info ($prefix . "FIRST NO INTERNET!!!");
# Change distance
:if ( [get $secondInterface distance] != 1 ) do={
set $secondInterface distance=1;
:log info ($prefix . "Distance for " . $secondInterfaceName . " changed");
$clearArp;
}
$reconnectPPPoE nameInterface=$firstInterfaceName;
} else={
:log info ($prefix . "FIRST INTERNET CONNECTED");
# Change distance
:if ( [get $secondInterface distance] != 3 ) do={
set $secondInterface distance=3;
:log info ($prefix . "Distance for " . $secondInterfaceName . " changed");
$clearArp;
}
}
}
:log info ($prefix . "END PING");
UPD
Изначально скрипт был написан и проверен на устройствах Mikrotik RB450G и RB951G-2HnD (ядро `mipsbe`), но при запуске на модели RB850Gx2 (ядро `powerpc`) возникала ошибка в строке: `[/ping $pingTo interface=[get $firstInterface gateway] count=5]`, а именно в отказе обработки `[get $firstInterface gateway]` внутри `[/ping… ]`.
Так как мы ввели дополнительную переменную `$firstInterfaceName`, то удалось изменить эту конструкцию и добились работоспособности скрипта на всех перечисленных устройствах.
UPD 2
Код скрипта в очередной раз был обновлен. esudnik обратил внимание на проблему большого количества потерянных пакетов при активном подключении к сети. Таким образом, мы ввели дополнительную переменную (`stableConnectFrom`), используемую для определения процентного соотношения «качества» линии.
В примере указано значение переменной равное «70». Это значит, если процент успешно отправленных пакетов будет ниже 70%, скрипт активирует резервный канал.
UPD 3
Как заметил icCE, при использовании скрипта на прошивке 6.36rc10, выходит ошибка:
:local pingStatus \
(( [/ping $pingTo1 interface=$firstInterfaceName count=$pingCount] + \
[/ping $pingTo2 interface=$firstInterfaceName count=$pingCount] ) / ($pingCount * 2)) * 100;
Прошивке не нравится "* 100". Решение проблемы было простым — обрамить вычисляемые значения в дополнительные скобки, получив
:local pingStatus \
((( [/ping $pingTo1 interface=$firstInterfaceName count=$pingCount] + \
[/ping $pingTo2 interface=$firstInterfaceName count=$pingCount] ) / ($pingCount * 2)) * 100);
Текст скрипта в статье обновил.