Универсальный скрипт переключения 2-х каналов интернета Mikrotik

  • Tutorial
Около 2,5 лет назад писал статью на тему автоматического переключения канала Интернет на резервный. Скрипт, конечно, и по сей день работает «на отлично», но его внешний вид и некоторые нюансы…

Итак, встала задача улучшить скрипт, максимально устранив побочные эффекты. Что ж, приступим.

image

В нашем распоряжении Mikrotik RB850Gx2, для которого мы будем писать скрипт (его работоспособность также проверена на моделях RB450G и RB951G-2HnD).

Для начала, добавим новый скрипт
image

Присвойте скрипту имя, например, 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.

Как запускать



Создадим запись в шедулере
image

image


В поле `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);


Текст скрипта в статье обновил.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 32

    +1
    Отлично!
    Но на заметку, лучше пропинговать два ресурса. Иногда у ростелекома бывает проблема с доступом у google dns.
      +2
      А не пробовали еще оптимизировать скрипт стандартным микротиковским netwatch? :)
        0
        Пробовал примерно 2,5 года назад. В начале статьи линки как раз на ту тему, где этот метод описывал.
        С netwatch проблема в том, что нельзя принудительно выбрать интерфейс, через который пинговать ресурс. Поэтому нужно добавлять дополнительные правила в файервол, на блокировку пинга через резервный канал.
        Это неудобно тем, что если мы захотим, например, изменить IP-адрес пинаемого ресурса, нужно не только в скриптах править, но и на вкладке файервола. А если забудем?..

        Вдобавок, такой метод раньше был. Я его описывал в другой своей статье.
        0
        Отлично, вот все никак руки не доходили донастроить свой, что бы работал с dhcp. Хотелось сделать универсальный роутер все же, а не заточенный под конкретных провайдеров.
          0
          Чуток не понял связь переключения каналов Инета с DHCP...
            0
            Просто обычно все настройки завязаны на то, что ISP статический, и ip шлюза заранее известен. Это делает железку совершенно не переносимой.
              0
              Не всегда. На крайнем месте моей работы, для которой я этот скрипт писал, есть 2 канала: Ростелеком и МТС. Первый использует PPPoE, второй MAC-адрес устройства в качестве "логина" и "пароля" (при первом входе переадресовывает на страницу МТС для ввода логина/пароля, после чего делает привязку по маку).
              Так вот, у Ростелекома белый статический IP-адрес, а у МТС получаю через DHCP Client в Микротике.
              При всем этом, ни в каких правилах у меня нет жесткой привязки к конкретному IP-адресу. Есть привязка лишь к именам интерфейсов, например, "pppoe-rostelecom", "eth2-MTS"...

              Другими словами, совсем не обязательно жестко указывать IP-адреса, когда можно воспользоваться их текстовым эквивалентом (названия интерфейсов).
                0
                У нас уже давненько все отказались от ppoe/l2tp. Сейчас у всех привязка по маку. Суть в том что большинство конфигов в инете обходятся без скриптов, просто проверкой доступности шлюза провайдера. Это конечно проверка так себе, но лучше чем ничего. И тут получается мы завязаны на инфраструктуру провайдеров. А должно все работать из коробки, настроил железку, отправил ее в другой город, там ее воткнули в сеть и она работает. И тут уже без скриптов не обойтись :)
                  +1
                  pppoe еще много где используется как и l2tp.
                  Пинговать шлюз не очень хорошая мысль, так как шлюз может спокойно себе работать. Опять же очень часто у ростелекома такая ситуация.
                    0
                    У нас в городе Ростелеком активно юзает PPPoE. Фишка в том, что при глюке сети их шлюз доступен, как и их внутренняя сеть (например, их сервер SIP-телефонии), НО внешний Инет недоступен пока не переподключишь соединение.
                      0
                      Угу, я так и написал, что это не самое лучшее решение. Но зато самое простое.
            +1
            А почему только 8.8.8.8. Были глюки в интернете, и 8.8.8.8 был не очень доступен. Как эта ситуация отработает?
              0
              8.8.8.8 использовал в качестве примера. Согласитесь, если в примере указать, скажем, какой-нить 217.112.35.75, народ начнет спрашивать "что это за ресурс и почему пинаем именно его"? Хотя это обычный www.ru
                +2
                Вопрос не почему 8.8.8.8 а

                "А почему только 8.8.8.8"

                Как я и писал выше< надо пинговать как минимум два ресурса.
                  +1
                  Исправил код скрипта, несколько его улучшив. Как раз добавил пинки для второго ресурса.
                0
                GGC разве не предусматривает недоступность ресурса?
                0
                Хорошая статья. Но я в подобном случае поступил по другому, без сложных скриптов. Также ситуация, один провайдер основной, представим что это шлюз 172.16.1.1 и провайдер который считает трафик помегабайтно или низкая скорость, шлюз будет 192.168.1.1. Создаем 2 скрипта, один назовем change-to-main с следующим содержимым:
                /ip route set gateway=172.16.1.1 [find dst-address=0.0.0.0/0];
                /tool e-mail send server="Адрес почтового сервера" port=25 user="Учетка почты" password="Пароль" to="кому отправить" from="От кого отправлено письмо" \
                subject="MikroTik: $[/system clock get date], $[/system clock get time]" \
                body="Переключение на основной канал\nДата: $[/system clock get date]\nВремя: $[/system clock get time]\nМеня зовут: $[/system identity get name]:";

                Как видно скрипт будет уведомлять администратора о переключении на резервный канал. И второй скрипт подобный только с другим именем и другим шлюзом, дав ему имя change-to-reserv и поменять "Переключение на резервный канал". Можно написать по английски, кому как нравится.
                далее идем в Tool> netwatch
                добавляем следующее:
                add down-script=change-to-reserv host=8.8.8.8 up-script=change-to-main
                add disabled=yes down-script=change-to-reserv host=172.16.1.1 \
                up-script=change-to-main

                Самое важное здесь, создать правило в файрволе, чтобы резервный провайдер не смог пинговать адрес который мы укажем в netwatch (гугловский днс я бы не рекомендовал ставить в данном случае, для этого есть много других ресурсов в пиринге города), если правило не укажем, то netwatch правильно работать не будет. Например, если резервный провайдер будет на ether2, то правило будет выглядеть вот так.
                /ip firewall filter
                add action=drop chain=output dst-address=8.8.8.8 out-interface=\
                ether2
                  +1
                  Судя по названиям скриптов у Вас настроено согласно моей прошлой статье)
                  Да, метод работает, НО это неудобно тем, что если мы захотим, например, изменить IP-адрес пинаемого ресурса, нужно не только в скриптах править, но и на вкладке файервола. А если забудем?..
                    +1
                    Точно, давно настраивал и поэтому написал. Извините за плагиат :).
                      0
                      Все норм) Прост увидел знакомые названия скриптов.

                      В одном из предыдущих мест работы этот скрипт все-то работает. Кстати, именно для той работы я его и писал, и с него же выложил статью на Хабре)

                      А не меняю его по одной причине — я там не работаю)))
                  +1
                  Спасибо за скрипт. Но на практике могут быть проблемы с проверкой только пинга. Например, может быть так что пинг будет работать, но интернет соединение в целом будет работать медленно или нестабильно. В таком случае имеет смысл тоже переключит на резервный канал.

                  Как дальнейшее улучшение можно сделать более сложный скрипт проверки на доступность интернета. Где не только в ответе будет да или нет, а будет определятся фактор качества интернет соединения. Например, 0 = интернет не работает, 100 = интернет работает очень хорошо, 70 = интернет работает нормально, но могло бы быть лучше. Соответственно в алгоритме проверять не только пинг но и различные другие параметры. Потом переключать на резервный интернет если оба значения сильно отличаются.
                    0
                    Согласен, задумка отличная. В моем случае разница такова, что скрипт настраивал для двух контор: в одной 42 ПК, в другой 96. Вдобавок, по практике, Инет в них либо есть, либо его нет совсем. У обоих Ростелеком по оптике в качестве основной линии.
                    Поэтому и не приходилось задумываться над доработкой скрипта в плане оценки качества линии.
                      0
                      Не мог устоять идее и реализовал ее в скрипте. Текст статьи обновил)
                      +1
                      Настроил так больше года назад: https://habrahabr.ru/post/244385/
                      по разделу "Обеспечение failover с более глубоким анализом канала"

                      Исправно работает, вообще без скриптов.
                        0
                        Нечто подобное пытался реализовать на базе найденных в сети примеров.
                        До конца не удалось довести по причине отсутствия опыта.
                        Работать, конечно, работало, но...

                        … бухгалтеры ругались, что тот же "Сбербанк Бизнес Онлайн" каждый раз требует пароль — оказалось, что пакеты принимались с одного канала, а уходили на другой.
                        Значит, криво настроил. Ну да ладно)

                        Статью прочел, расписано хорошо и понятно. Принял к сведению) Спасибо!
                        0
                        Возник тут еще один вопрос. Можно как то сделать учет трафика?
                        Например, второй канал может быть лимитирован.
                        Вроде я видел у routeros подобие БД, может кто уже успел реализовать ?
                          +1
                            +2
                            шлюз может спокойно работать, но интернета не будет.
                              +1
                              Там тот же способ проверки что и здесь — пингуют указанный хост.
                            0
                            У меня вот другая интересная "фича" была. Провайдер ТТК, при отключении не поднимал связь, пока не передернут кабель или не ребутнут роутер. Пришлось дописывать в существующий скрипт рестарт интерфейса...
                              0
                              Аналогичная ситуация и у Ростелекома — их внутренняя сеть доступна, а внешняя нет.
                              Поэтому и выкрутился таким кодом:

                              # Function to reconnect PPPoE connection
                              :local reconnectPPPoE do={
                                  /interface pppoe-client set $nameInterface disable=yes;
                                  :delay 1s;
                                  /interface pppoe-client set $nameInterface disable=no;
                              }
                              +1
                              Недавно смотрел готовые решения по переключению каналов микротика и наткнулся на этот скрипт. Спасибо, особенно за «оценку качества» с пингом на два адреса.

                              Хотелось бы отметить следующее: при каждом запуске скрипта мы переключаемся на шлюз основного канала. Т.е. если у нас через основной канал теряются пакеты (т.е. он работает, но кое-как), то при тестировании мы всех переключаем на этот глючный основной канал. При этом сеть начинает «лагать». Особенно радуются sip-пользователи.

                              Лучший и правильный вариант в этом случае — пинговать через конкретную таблицу маршрутизации с минимальным размером пакета, например:

                              /ping $pingTo1 interface=$firstInterfaceName size=28 count=$pingCount routing-table=$firstInterfaceName


                              Кроме того, лучше переделать инициализацию интерфейсов из-за вышеперечисленного и того, что не всегда можно указать интерфейс как шлюз (т.е. если на стороне провайдера не включен proxy-arp):

                              :set firstInterface [find dst-address="0.0.0.0/0" gateway=$firstInterfaceName];
                              :set secondInterface [find dst-address="0.0.0.0/0" gateway=$secondInterfaceName];

                              :set firstInterface [find dst-address="0.0.0.0/0" comment=$firstInterfaceName !routing-mark];
                              :set secondInterface [find dst-address="0.0.0.0/0" comment=$secondInterfaceName];


                              Соответственно, в таблице маршрутизации мы должны добавить маршрут до тестируемого адреса (или до 0.0.0.0/0) с именем таблицы равной названию основного интерфейса.

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

                              ЗЫЖ данная ситуация не будет наблюдаться когда основной канал в дауне, т.к. основной шлюз не будет активным и дистанция до резервного канала не имеет значения и описываемые «залипания» не видны

                              Only users with full accounts can post comments. Log in, please.