Mikrotik: скрипт переключения на резервный канал интернета

Хочу поделиться своим скриптом для перехода на резервный интернет, когда основной пропадает, и возврату на основной, как только он вновь заработает. Сразу скажу, каналы доступны по-одному, никакого load-balance тут не будет. Оба канала — PPP соединения (в моем случае один проводной, второй — 3G свисток). Скрипт сделан специально как наиболее гибкое средство мониторинга, так как другие варианты, в частности check-gateway, не совсем корректны для меня.

Основной принцип прост: поднятый VPN канал не означает, что интернет через него работает. Я проверяю, пингуя несколько внешних адресов. Можно придумать, когда и пинги не являются показателем работы, но эти случаи я опускаю, в скрипте можно указать любой другой способ проверки, под ситуацию. Другие особенности: резервный канал — мобильная сеть, и он подключается только при отсутствии основного канала, в остальное время интерфейс выключен. При возврате обратно на основной канал корректно проверяется его работоспособность. Методика, отличная от пинга с указанием интерфейса. Ну и route-distance у интерфейсов меняются динамически и всегда не равны, что дает возможность одновременной работы каналов, но трафик направляется только на один из них.

При понимании можно легко переделать скрипт, если провайдеры или один из них, дает статику.

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

Допустим, есть 2 PPP соединения ISP1 — основной, и ISP2 — резервный, оба настроены и работают по-отдельности. Выставляем на них dial-on-demand=no и add-default-route=yes, затем устанавливаем у ISP2 параметр default-route-distance на единицу больше, чем у ISP1. Настраиваем стандартные вещи, как NAT, маркировка пакетов и соединений для ответов по тому же интерфейсу, откуда пришел запрос, маршрутов для помеченных пакетов:

Подготовка
/ip firewall mangle
add action=mark-connection chain=forward connection-mark=no-mark \
    in-interface=ISP1 new-connection-mark=ISP1 passthrough=no
add action=mark-routing chain=prerouting connection-mark=ISP1 in-interface=\
    bridge-local new-routing-mark=to_ISP1 passthrough=no

add action=mark-connection chain=forward connection-mark=no-mark \
    in-interface=ISP2 new-connection-mark=ISP2 passthrough=no
add action=mark-routing chain=prerouting connection-mark=ISP2 in-interface=\
    bridge-local new-routing-mark=to_ISP2 passthrough=no

/ip firewall nat
add action=masquerade chain=srcnat out-interface=ISP1
add action=masquerade chain=srcnat out-interface=ISP2

/ip route
add distance=1 gateway=ISP1 routing-mark=to_ISP1
add distance=1 gateway=ISP2 routing-mark=to_ISP2

Также предположим, что локальный адрес роутера 192.168.xx.yy, а подсеть 192.168.xx.0/24. Эти данные, как и имена интерфейсов, нужно изменить на свои. Это не вся настройка, но обо всем по-порядку.

Переменные
global FailoverTimes;
global FailoverLastTime;
global FailoverLastBackTime;

local ifMain "ISP1";
local ifRes "ISP2";

local scriptName "Failover";
local state 0;
local pingNum 0;
local pingRes;
local routeDist;
local routeDist2;
local tmp;

local ip { x.x.x.x; y.y.y.y; z.z.z.z }; 
local pingSrcAddr 192.168.xx.yy;

Определяем переменные: пишем названия интерфейсов в ifMain и ifRes, локальный адрес роутера в pingSrcAddr (далее будет понятно, зачем он нужен), и 3 внешних адреса, которые будут пинговаться для проверки канала в массив ip.

Single instance
if ( [len [/system script job find where script=$scriptName]] > 1) do= { error "single instance" };
delay 15;

Разрешим запускаться только одной копии скрипта. Delay на случай запуска при старте RouterOS, даем время подняться соединениям.

Пропустим немного, и перейдем к основной части. Скрипт работает бесконечно, вернее, пока его не остановят или не случится ошибка. В бесконечном цикле он анализирует текущее состояние по переменной state и выполняет необходимые действия. Рассмотрим их.

State 0
  if ($state = 0) do= {
    do {
      if ($pingNum >= 3) do= { set $pingNum 0; }
      if ([ping ($ip->$pingNum) count=1] = 0) do= {
        set $pingRes [ping ($ip->0) count=2];
        set $pingRes ($pingRes+[ping ($ip->1) count=2]);
        set $pingRes ($pingRes+[ping ($ip->2) count=2]);
        if ($pingRes = 0) do= {
          set $FailoverLastTime "$[/system clock get date] $[/system clock get time]";
          set $FailoverTimes ([tonum $FailoverTimes] + 1)
          set $state 1;
          log info "$scriptName: state changed 0->1";
        }
      }
      set $pingNum ($pingNum + 1);
      if ($state = 0) do= { delay 15 };
    } while ($state = 0);
  }

Состояние 0 — когда работает основной канал. Раз в 15 секунд проверяем последовательно один из трех указанных адресов, если ответа нет — проверяем все 3 адреса. Глухо — инициируем переход на резервный канал. Тут жестко указано, что адресов в массиве — 3. Если это не так, придется подправить.

State 1
  if ($state = 1) do= {
      if ( [/interface l2tp-client get $ifMain default-route-distance] > 10) do= {
        /interface ppp-client set $ifRes default-route-distance=1;
      }
      /interface enable $ifRes;
      beep frequency=2000 length=250ms;
      delay 500ms;
      beep frequency=2000 length=250ms;
      delay 500ms;
      delay 6;
      /interface disable $ifMain;
      set $routeDist ([/interface ppp-client get $ifRes default-route-distance] + 1);
      /interface l2tp-client set $ifMain default-route-distance=$routeDist;
      /interface enable $ifMain;
      set $state 2;
      log info "$scriptName: state changed 1->2";
  }

Состояние 1 — переключение каналов. Здесь важно, какие именно PPP соединения используются. В примере — ISP1 это l2tp-client, а ISP2 — ppp-client. Если другие, нужно их подправить в строках с default-route-distance.

После включения резервного канала ждем 7 секунд. Это достаточное время для меня, за которое 3G соединение поднимается. За это время текущие соединения и новые висят в таймаутах, пока основной VPN еще не разорвался, и минимизируются ответы роутера типа dest unreachable.

Звуковая индикация на любителя, может и ночью сработать. Если не нужно — убираем.

Далее основной канал отключается, его default-route-distance устанавливается на 1 больше, чем у резервного, и он включается обратно. За счет этого имеем возможность ждать возврата основного канала без помех для работы интернета через резерв.

Забегая вперед, при переходе обратно на основной канал и отключении резерва его default-route-distance снова увеличится на 1. С каждым переключением route distance у PPP соединений последовательно увеличивается. Для того, чтобы они не уходили слишком далеко, здесь проверяется текущее значение и происходит сброс на 1 при превышении 10 (цифра не имеет значения, взято для примера, теоретически максимум около 250).

State 2
  if ($state = 2) do= {
    do {
      if ( [len [interface find where name=$ifMain and running] ] = 1) do= {
        set $pingRes [ping ($ip->0) src-address=$pingSrcAddr count=2];
        set $pingRes ($pingRes+[ping ($ip->1) src-address=$pingSrcAddr count=2]);
        set $pingRes ($pingRes+[ping ($ip->2) src-address=$pingSrcAddr count=2]);
        if ($pingRes > 0) do= {
          set $state 3;
          log info "$scriptName: state changed 2->3";
        }
      }
      if ($state = 2) do= { delay 15 };
    } while ($state = 2);
  }

Состояние 2 — ожидание восстановления основного канала. Стоит отметить, что состояние резерва не интересно. Если он не подключился, ничего не поделать, все условия для него созданы, и фактически нас интересует только основной канал.

Здесь ожидается поднятие VPN основного канала, и после этого через него при активном резерве происходят попытки пинга внешних адресов. Сделано это сложновато, но правильно. Если писать ping xx.xx.xx.xx interface=$ifMain, то по словам разработчиков, это может как работать, так и нет. Тут используется пинг с локального адреса роутера. Допускается, что он всегда есть, иначе зачем роутер нужен. Я не использовал внешний адрес основного канала, потому что провайдер его дает динамическим. Разбираемся, как же сказать роутеру посылать такие пинги через основной канал, даже когда его маршрут неактивен (route distance больше, чем у резервного):

Донастройка
/ip firewall mangle
add action=mark-routing chain=output comment=Failover_script_rule \
    dst-address=!192.168.xx.0/24 new-routing-mark=to_ISP1 passthrough=no \
    protocol=icmp src-address=192.168.xx.yy

/ip route rule
add action=lookup-only-in-table routing-mark=to_ISP1 src-address=\
    192.168.xx.yy/32 table=to_ISP1

Трафик пингов, используемый тут, является нестандартным. Это output трафик, исходящий от самого роутера на внешний адрес. Обычно, в таких случаях за src-address роутер берет адрес интерфейса, по которому уйдет пакет. Указывая локальный адрес роутера как src-address, мы как бы выносим его за тот же NAT, за которым сидит локалка. Далее, такой трафик метится с routing-mark основного канала, и пакеты идут по основному каналу за счет маршрута с меткой.

Второе правило также необходимо. Без него, если вдруг основной канал снова упадет, то пинги, даже помеченные to_ISP1, пойдут по маршруту без метки резервного канала, что приведет к некорректному возврату на основной канал. Так работает RouterOS, если канал не подключен, то маршруты, даже помеченные, отключаются. Чтобы было немного яснее, представим, что state=2, основной канал поднят, но трафик через него не идет. На пинги в таком случае уйдет 6 секунд. Так вот если в это время основной канал отключится, то пинги начнут проходить по резерву. Второе правило это исключает.

Отмечаем, что пинги в локалку с роутера не метятся и работают как обычно.

State 3
  if ($state = 3) do= {
      /interface disable $ifRes;
      set $routeDist ([/interface l2tp-client get $ifMain default-route-distance] + 1);
      /interface ppp-client set $ifRes default-route-distance=$routeDist;
      set $state 0;
      set $FailoverLastBackTime "$[/system clock get date] $[/system clock get time]";
      log info "$scriptName: state changed 3->0";
      beep frequency=500 length=500ms;
  }

Состояние 3 — переход на основной канал. После того, как пинги по основному каналу начали проходить, достаточно выключить резервный VPN, и будет использоваться основной. Далее, меняем default-route-distance у резервного на 1 больше, чем у основного, и подаем звуковой сигнал. Обращаем внимание на тип PPP соединений, и меняем по-необходимости.

На этом цикл замыкается и происходит возврат в состояние 0.

Теперь о том, как при запуске скрипта он узнает текущее состояние:

Initial state
set $routeDist [/interface l2tp-client get $ifMain default-route-distance];
set $routeDist2 [/interface ppp-client get $ifRes default-route-distance];
if ($routeDist < $routeDist2) do= {
  if ( [/interface get $ifMain running] = true) do= { set $state 0; } else= { set $state 1; }
} else= {
  if ( [/interface get $ifMain disabled] = true) do= { /interface enable $ifMain; }
  if ($routeDist > $routeDist2 and [/interface get $ifRes disabled] = false) do= {
    set $state 2;
  } else= { set $state 3; }
}

log info "$scriptName: initial state $state";

Здесь логика также сложновата на первый взгляд. Анализируются 3 параметра: запущен ли ISP1, запущен ли ISP2, и соотношение default route distance у них. Начальные состояния 1 и 3 являются нестандартными, и говорят о неправильной настройке, но скрипт в таком случае сам все восстанавливает, пусть иногда и путем ненужных переключений.

Исключенное состояние
У меня есть еще одно состояние, которое я исключил, т.к. скорее всего оно вряд ли нужно большинству. Мой ISP1 подключает VPN по имени, не по IP, для разрешения этого имени нужно использовать DNS этого же провайдера, т.к. оно разрешает в локальный адрес. И если не помочь скрипту с разрешением, указывая конкретный DNS, то даже после доступности сети ISP1 он никогда не подключится, т.к. не разрешит доменное имя, а будет продолжать использовать DNS резерва. Вот это доп. состояние:

  if ($state = 2) do= {
    do {
      if (([ping DNSip1 count=1] > 0) or ([ping DNSip2 count=1] > 0)) do= {
        set $tmp 0;
        do { resolve VPNaddress server=DNSip1; } on-error= { };
        do { resolve VPNaddress server=DNSip2; } on-error= { };
        do { resolve VPNaddress } on-error= { set $tmp 1; };
        if ($tmp = 0) do= { 
          set $state 3;
          log info "$scriptName: state changed 2->3";
          delay 5;
        }
      }
      if ($state = 2) do= { delay 15 };
    } while ($state = 2);
  }

Вместо DNSip1, DNSip2 и VPNaddress подставляем нужные данные. Все состояния ниже соответственно смещаются на +1.


Вот в принципе и все, разработано и отлажено на 6.26 и RB951G-2HnD. На других версиях — не обещаю, и простите за отсутствие ':' перед командами.

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

Скрипт-монитор
global FailoverDisabled;

if ( [len [/system script job find where script="Failover"]] = 0 and $FailoverDisabled != 1) do= {
  do { execute script="Failover"; } on-error= { log info "$scriptName: Failed to execute Failover" };
}

Глобальной переменной можно отключить запуск скрипта Failover. Также, за счет расписания, при непредвиденных перезагрузках роутера скрипт будет автоматически запущен снова.

Скрипт Failover целиком
global FailoverTimes;
global FailoverLastTime;
global FailoverLastBackTime;

local ifMain "ISP1";
local ifRes "ISP2";

local scriptName "Failover";
local state 0;
local pingNum 0;
local pingRes;
local routeDist;
local routeDist2;
local tmp;

local ip { x.x.x.x; y.y.y.y; z.z.z.z }; 
local pingSrcAddr 192.168.xx.yy;

if ( [len [/system script job find where script=$scriptName]] > 1) do= { error "single instance" };
delay 15;

set $routeDist [/interface l2tp-client get $ifMain default-route-distance];
set $routeDist2 [/interface ppp-client get $ifRes default-route-distance];
if ($routeDist < $routeDist2) do= {
  if ( [/interface get $ifMain running] = true) do= { set $state 0; } else= { set $state 1; }
} else= {
  if ( [/interface get $ifMain disabled] = true) do= { /interface enable $ifMain; }
  if ($routeDist > $routeDist2 and [/interface get $ifRes disabled] = false) do= {
    set $state 2;
  } else= { set $state 3; }
}

log info "$scriptName: initial state $state";

do {
  if ($state = 0) do= {
    do {
      if ($pingNum >= 3) do= { set $pingNum 0; }
      if ([ping ($ip->$pingNum) count=1] = 0) do= {
        set $pingRes [ping ($ip->0) count=2];
        set $pingRes ($pingRes+[ping ($ip->1) count=2]);
        set $pingRes ($pingRes+[ping ($ip->2) count=2]);
        if ($pingRes = 0) do= {
          set $FailoverLastTime "$[/system clock get date] $[/system clock get time]";
          set $FailoverTimes ([tonum $FailoverTimes] + 1)
          set $state 1;
          log info "$scriptName: state changed 0->1";
        }
      }
      set $pingNum ($pingNum + 1);
      if ($state = 0) do= { delay 15 };
    } while ($state = 0);
  }
# endof if state = 0

  if ($state = 1) do= {
      if ( [/interface l2tp-client get $ifMain default-route-distance] > 10) do= {
        /interface ppp-client set $ifRes default-route-distance=1;
      }
      /interface enable $ifRes;
      beep frequency=2000 length=250ms;
      delay 500ms;
      beep frequency=2000 length=250ms;
      delay 500ms;
      delay 6;
      /interface disable $ifMain;
      set $routeDist ([/interface ppp-client get $ifRes default-route-distance] + 1);
      /interface l2tp-client set $ifMain default-route-distance=$routeDist;
      /interface enable $ifMain;
      set $state 2;
      log info "$scriptName: state changed 1->2";
  }

  if ($state = 2) do= {
    do {
      if ( [len [interface find where name=$ifMain and running] ] = 1) do= {
        set $pingRes [ping ($ip->0) src-address=$pingSrcAddr count=2];
        set $pingRes ($pingRes+[ping ($ip->1) src-address=$pingSrcAddr count=2]);
        set $pingRes ($pingRes+[ping ($ip->2) src-address=$pingSrcAddr count=2]);
        if ($pingRes > 0) do= {
          set $state 3;
          log info "$scriptName: state changed 2->3";
        }
      }
      if ($state = 2) do= { delay 15 };
    } while ($state = 2);
  }
# endof if state = 2

  if ($state = 3) do= {
      /interface disable $ifRes;
      set $routeDist ([/interface l2tp-client get $ifMain default-route-distance] + 1);
      /interface ppp-client set $ifRes default-route-distance=$routeDist;
      set $state 0;
      set $FailoverLastBackTime "$[/system clock get date] $[/system clock get time]";
      log info "$scriptName: state changed 3->0";
      beep frequency=500 length=500ms;
  }
# bad programming protection
  delay 1;
} while= ( true );
  • +20
  • 48,3k
  • 6
Поделиться публикацией

Комментарии 6

    0
    А почему не использовали net watch, как здесь?
      0
      Действительно? почему не использовать Netwatch?
      У меня, например используется алгоритм такой: определяем маршруты до пингуемых мест, пингуем 1-ip через isp_1, второй через isp_2
      при потери пакетов до адресата 1-ip, выполняем скрипт (одна строка) "/ip route enable [find dst-address=0.0.0.0/0 and gateway=ххх.ххх.ххх.ххх and distance >= 3;" — т.е. меняем весомость роута первого провайдера и отправляем траффик через резервного провайдера, при появлении пингов возвращаем. тоже самое и со вторым провайдером. за пару лет проблем не наблюдалось, работает отлично без нареканий.
      все гениально просто!
        0
        Там много моментов, почему не подходит. Главный — что роуты на статике всегда активны, а если указывать в качестве шлюза интерфейс (что придется делать для PPP), то он не будет активным, когда канал упадет, и приехали. Во-вторых, пинговать один хост — не лучшая идея, с любым хостом может что-то случиться… И в-третьих, очень не хочется держать постоянно активным 3G соединение.
          0
          Немного поразмыслив, понял, что неактивные роуты — не проблема, и в принципе через netwatch можно все сделать и для PPP соединений, если игнорировать два других моих аргумента. Но возникает вопрос, делаете ли Вы при включении/отключении роутов ресет текущим соединениям?
            0
            Можете поделиться как решается проблема неактивных маршрутов? Кажется роуты на статике тоже могут переставать работать. Например, упало соединение на физическом интерфейсе (кабель порвался). Тогда маршрут для проверки основного канала станет неактивным и Netwatch успешно сделает ping, так и не поняв, что канал упал. Если же резервный канал поднимается только после регистрации падения основного, то Netwatch выполнит переход на резервный канал, а затем снова успешно сделает ping и выполнит скрипт для возврата на мертвый основной канал. Так и будет по кругу…

            А вот в собственном скрипте мы можем явно указать интерфейс в команде ping, избежав проблем.
              0
              Я поначалу тоже так думал. Возможно я неправ, но если посмотреть, как сделано по ссылке в первом комментарии, то в той конфигурации проверочный пинг на 8.8.8.8 пойдет либо через шлюз первого провайдера, либо не пойдет вообще, т.к. маршрута через другие шлюзы на 0.0.0.0 без routing mark нет. Скорее всего роутер скажет dest unreachable для пингов. Когда канал заработает, маршрут до 8.8.8.8 поднимется и пинги пойдут. А весь forward трафик идет по маршрутам с метками, на которых и висит логика.

              Интерфейс в команде ping указывать некорректно, в описании к скрипту об этом написано, еще вот ссылочка. Хотя у меня тоже вроде работает, но делать так не стал.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое