Pull to refresh

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

Reading time 10 min
Views 58K
Хочу поделиться своим скриптом для перехода на резервный интернет, когда основной пропадает, и возврату на основной, как только он вновь заработает. Сразу скажу, каналы доступны по-одному, никакого 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 );
Tags:
Hubs:
+20
Comments 6
Comments Comments 6

Articles