Как стать автором
Обновить

Asterisk: Приоритезация VoIP трафика и резервирование доступа в Интернет двух провайдеров на MikroTik

Время на прочтение11 мин
Количество просмотров48K
Казалось бы вещи, вынесенные в заголовок, достаточно тривиальны и описаны во множестве мест глобальной сети, но это только на первый взгляд. Опробовав наиболее часто встречающиеся советы я обнаружил несколько «подводных камней», глыб и даже скальных образований.

Но это все слова, ближе к делу.
Достаточно распространенная ситуация — Asterisk внутри ЛКС, за маршрутизатором MikroTik.
Дабы выделить трафик сервера, где установлена PBX, администратор отрезает часть канала провайдера выделяя его исключительно для конкретного IP.
Или другая реализация, когда нужный трафик определяется не только по IP-адресу PBX, но и по размеру пакетов и протоколу.
Попробовали — работает. Можно забыть? А вот и нет.

Что если администратору захочется слить что-то из Интернет находясь в консольке сервера, или наоборот отправить куда-либо в Интернет большое количество траффика? Правильно — он приоритезируется на MikroTik так же как и полезный трафик от PBX, что в итоге приведет к проблемам с IP-телефонией.

Решение здесь старо как сам IPv4 — метить трафик на сервере с Asterisk генерируемый только ею, и так, чтобы MikroTik это мог «увидеть», отматчить(простите за столь грубый англицизм) и приоритезировать только его.

Следующим пунктом у нас идет резервирование каналов от двух интернет-провайдеров.
Думаю что каждому системному администратору, использующему в своем хозяйстве маршрутизаторы MikroTik, знаком скрипт из wiki — wiki.mikrotik.com/wiki/Failover_Scripting
Он всем хорош, но как и в предыдущей ситуации есть ряд «но».
Наиболее весомому из них имя «Connection tracking» и заключается оно вот в чем:
когда наш основной ISP изволит отдохнуть от трудов праведных, траффик переключается на резервного.

Все вроде бы довольны, ютуб работает, яп тоже, но сколько бы мы не кричали экспекто потронум
sip reload

и в отчаянии не пытались применить магию высших порядков
core restart now

SIP-регистрации не поднимаются.

А дело в том, что в механизме «Connection tracking» остались висеть записи от «старого»(основного) интернет-канала и их нужно удалить, после чего регистрации успешно поднимутся и звонки начнут проходить.

Если вам интересно как доказать MikroTik'у кто все-таки верблюд, а так же как автоматизировать в скрипте сброс «старых» соединений, то вам прямо под кат.


Часть 1. Правильная приоритезация VoIP трафика


От весьма словоохотливого вступления перейдем к практической части.
Как же метить трафик, чтобы эту метку распознал MikroTik?
Ответ прост — DSCP!
ОК, а что на счет трафика конкретного демона, в данном случает Asterisk?
И здесь без проблем, сэр(мадам) — iptables --uid-owner!

На сервере с PBX узнаем какой user id у нашей Asterisk(звездочка — она)
grep -i asterisk /etc/passwd|cut -d: -f3
1001


И добавим правило для указания нужного нам значения DSCP(для трафика VoIP принято указывать CS5, или, в числовом формате — 40)
комментарий от ksg222
Хотел бы обратить внимание, что обычно голосовой трафик «раскрашивают» двумя значениями DSCP: CS5 (CS3 в случае Cisco) для сигнализации и EF для RTP-трафика. Безусловно в вашем примере это не критично, так как у вас в конфигурации основной момент — это выделить голосовой трафик среди остальных. Всё равно дальше в сети интернет приоритезации по полю DSCP не будет.

tools.ietf.org/html/rfc4594#section-2.3

www.cisco.com/c/en/us/td/docs/solutions/Enterprise/WAN_and_MAN/QoS_SRND/QoS-SRND-Book/QoSIntro.html#pgfId-46256


iptables -I OUTPUT 1 -t mangle -m owner --uid-owner 1001 -j DSCP --set-dscp 40

*для любознательных — в конце статьи буду ссылки, где можно узнать с чего я это взял


как подсказал varnav в комментариях, выставлять нужные значения можно сразу в sip.conf
А зачем ставить DSCP метку через Iptables, если это можно делать через sip.conf:

tos_sip=cs3; Sets TOS for SIP packets.
tos_audio=ef; Sets TOS for RTP audio packets.
tos_video=af41; Sets TOS for RTP video packets.

За сим на сервере с IP АТС мы закончим и перейдем к настройке маршрутизатора.
За основу я взял вот этот материал — voxlink.ru/kb/voip-devices-configuration/mikrotik-voip-traffic-configuration
Репостить не стану, там все доступно расписано, покажу только листинги.
Таблица MANGLE фаерволла:
/ip f m exp
# nov/26/2015 17:09:20 by RouterOS 6.21.1
# software id = 5QIF-MH9A
#
/ip firewall mangle
add action=mark-packet chain=forward new-packet-mark=def_out src-address=192.168.5.0/24
add action=mark-packet chain=forward new-packet-mark=def_out src-address=192.168.6.0/24
add action=mark-packet chain=forward dst-address=192.168.5.0/24 new-packet-mark=def_in
add action=mark-packet chain=forward dst-address=192.168.6.0/24 new-packet-mark=def_in
add action=mark-packet chain=forward dscp=40 new-packet-mark=voip_out src-address=192.168.7.10
add action=mark-packet chain=forward dscp=40 dst-address=192.168.7.10 new-packet-mark=voip_in

Как и описано во вступлении — матчим нужный трафик по IP и по полю DSCP.

И очереди:
/queue tree export 
# nov/26/2015 17:09:27 by RouterOS 6.21.1
# software id = 5QIF-MH9A
#
/queue tree
add max-limit=30M name=in parent=global
add max-limit=28M name=def-in packet-mark=def_in parent=in
add limit-at=1M max-limit=2M name=voip-in packet-mark=voip_in parent=in priority=1
add max-limit=30M name=out parent=global
add max-limit=28M name=def-out packet-mark=def_out parent=out
add limit-at=1M max-limit=2M name=voip-out packet-mark=voip_out parent=out priority=1

К слову, маршрутизатор и так должен приоритезировать траффик с бОльшим значением DSCP, но я все таки отделил для него полосу в 2Мб/с.

Часть 2. Скрипт failover'а


Начнем с самого начала, расскажу как у меня настроены линки провайдеров и маршрутизация.
Есть два ISP, один подключается по ethernet, второй по PPPoE.
Для наглядности приведу листинги. Ethernet подключение:
[f@777777] > ip addr pr where interface=ether7
Flags: X - disabled, I - invalid, D - dynamic 
 #   ADDRESS            NETWORK         INTERFACE                                                                                         
 0   46.11.6.78/30    46.11.6.76    ether7       

PPPoE:
[f@777777] > int pppoe-cl pr 
Flags: X - disabled, R - running 
 0  R name="rtk" max-mtu=auto max-mru=auto mrru=disabled interface=ether8 user="f" password="w" profile=default 
      keepalive-timeout=60 service-name="" ac-name="" add-default-route=no dial-on-demand=no use-peer-dns=yes 
      allow=pap,chap,mschap1,mschap2 

заметьте — я не добавляю маршрут по умолчанию при поднятии интерфейса, он будет добавлен вручную, статически

следующим этапом идет настройка маршрутизации
[f@777777] > ip r p d t
 0 A S  comment=RTK_TABLE dst-address=0.0.0.0/0 gateway=rtk gateway-status=rtk reachable distance=1 scope=30 target-scope=10 routing-mark=RTK_mark 
 1 A S  comment=GoodLine_TABLE dst-address=0.0.0.0/0 gateway=46.18.6.77 gateway-status=46.18.6.77 reachable via  ether7 distance=1 scope=30 target-scope=10 routing-mark=GoodLine_mark 
 2 A S  comment=MAIN_TABLE dst-address=0.0.0.0/0 gateway=46.18.6.77 gateway-status=46.18.6.77 reachable via  ether7 distance=1 scope=30 target-scope=10 
 3   S  comment=MAIN_TABLE dst-address=0.0.0.0/0 gateway=rtk gateway-status=rtk reachable distance=2 scope=30 target-scope=10 
 4 ADC  dst-address=10.155.177.1/32 pref-src=10.155.177.13 gateway=pptp-out1 gateway-status=pptp-out1 reachable distance=0 scope=10 
 5   S  dst-address=10.155.177.1/32 gateway=pptp-out1 gateway-status=pptp-out1 reachable distance=1 scope=30 target-scope=10 
 6 ADC  dst-address=46.18.6.76/30 pref-src=46.18.6.78 gateway=ether7 gateway-status=ether7 reachable distance=0 scope=10 
 7 ADC  dst-address=172.16.79.1/32 pref-src=172.16.79.251 gateway=l2tp-out1 gateway-status=l2tp-out1 reachable distance=0 scope=10 
 8 ADC  dst-address=192.168.77.0/24 pref-src=192.168.77.1 gateway=bridge-local gateway-status=bridge-local reachable distance=0 scope=10 
 9 A S  comment=thecall dst-address=192.168.254.0/24 gateway=172.16.79.1 gateway-status=172.16.79.1 reachable via  l2tp-out1 distance=1 scope=30 target-scope=10 
10 ADC  dst-address=213.22.11.99/32 pref-src=217.11.15.125 gateway=rtk gateway-status=rtk reachable distance=0 scope=10 

тут много лишнего, нас интересует только 0,1,2,3
2 и 3 — это маршруты по умолчанию, 2 с метрикой(distance) «1» и является активным на данный момент и 3 с метрикой «2», на данный момент не используется

0 и 1 нужны для того, чтобы марштутизатор мог ответить с того же интерфейса, на который пришел запрос, т.е. не зависимо от того, какой сейчас маршрут по умолчанию активен
этой же логике служат и правила 0,1 из следующего листинга правил маршрутизации:
[f@777777] > ip r ru pr  
Flags: X - disabled, I - inactive 
 0   src-address=217.11.15.125/32 action=lookup table=RTK_mark 

 1   src-address=46.18.6.78/30 action=lookup table=GoodLine_mark 

 2   dst-address=192.168.77.0/24 action=lookup table=main

правило 2 нужно для устройств локальной сети за MikroTik.

Все это называется емкими словами «Policy based routing» и в завершении опуса я дам несколько полезных ссылок для понимания работы этих механизмов-правил.
*да, у меня реализован самый минимум, но это то что нужно мне

С аперетивом мы расправились, займемся основным блюдом.
Листинг скрипта упакован под спойлером, т.к. слишком велик(все UPD уже включены).
UPD1 — скрипт исправлен 28.11.2015,
изменил условие(справедливо и для ISP2):
- :if ($PingFailCountISP1 < ($FailTreshold))
+ :if ($PingFailCountISP1 < ($FailTreshold+1))

так же дописал добавление маршрута
:if ([/interface get value-name=running $InterfaceISP1])  do={
/ip route add dst-address=$PingTarget gateway=$GatewayISP1
:set PingResult [ping $PingTarget count=3]

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

в конце проверки каждого ISP маршрут удаляется
/ip route rem [find dst-address="$PingTarget" . "/32"]



UPD2 — скрипт успешно работает на RouterOS версии 6.33.1

UPD3
переменную PBXIP нужно обозначать в кавычках
:local PBXIP "192.168.77.10"

а каждая строчка удаления соединений «Connection tracking» должна быть без ковычек
/ip firewall connection { remove [find src-address~$PBXIP] }



Скрытый текст
# ------------------- header -------------------
# Script by Tomas Kirnak, version 1.0.7
# If you use this script, or edit and
# re-use it, please keep the header intact.
#
# For more information and details about
# this script please visit the wiki page at
# http://wiki.mikrotik.com/wiki/Failover_Scripting
# ------------------- header -------------------

#------------------- header_1 ------------------
# FessAectan has made some changes: 
# - add ISPs link state checking -
# - add clearing connection tracking states for selected IP - 
# ------------------ header_1 ------------------

# ------------- start editing here -------------
# Edit the variables below to suit your needs

# Please fill the WAN interface names
:local InterfaceISP1 ether7
:local InterfaceISP2 rtk

# Please fill the gateway  IPs (or interface names in case of PPP)
:local GatewayISP1 46.18.6.77
:local GatewayISP2 rtk

# Please fill the ping check host - currently: resolver1.opendns.com
:local PingTarget 21.7.2.77

# Please fill how many ping failures are allowed before fail-over happends
:local FailTreshold 1

# Define the distance increase of a route when it fails
:local DistanceIncrease 2

# Editing the script after this point may break it
# -------------- stop editing here --------------

# Declare the global variables
:global PingFailCountISP1
:global PingFailCountISP2
:global InterfaceFailISP1
:global InterfaceFailISP2

# This inicializes the PingFailCount variables, in case this is the 1st time the script has ran
:if ([:typeof $PingFailCountISP1] = "nothing") do={:set PingFailCountISP1 0}
:if ([:typeof $PingFailCountISP2] = "nothing") do={:set PingFailCountISP2 0}

# IntercaceFail variables. First time initialization.
:if ([:typeof $InterfaceFailISP1] = "nothing") do={:set InterfaceFailISP1 0}
:if ([:typeof $InterfaceFailISP2] = "nothing") do={:set InterfaceFailISP2 0}

# This variable will be used to keep results of individual ping attempts
:local PingResult

# Your PBX IP
:local PBXIP "192.168.77.10"

# Check ISP1

:if ([/interface get value-name=running $InterfaceISP1])  do={
/ip route add dst-address=$PingTarget gateway=$GatewayISP1
:set PingResult [ping $PingTarget count=3 interface=$InterfaceISP1]

:if ($PingResult = 0) do={
    :if ($PingFailCountISP1 < ($FailTreshold+1)) do={
        :set PingFailCountISP1 ($PingFailCountISP1 + 1)}
            
    :if ($PingFailCountISP1 = $FailTreshold) do={
        :log warning "ISP1 has a problem en route to $PingTarget - increasing distance of routes."
        :foreach i in=[/ip route find gateway=$GatewayISP1 && static && comment=MAIN_TABLE] do=\
            {/ip route set $i distance=([/ip route get $i distance] + $DistanceIncrease)}
        :log warning "Route distance increase finished."
        /ip firewall connection { remove [find src-address~$PBXIP] }
    }
}
} else={
        :if ($InterfaceFailISP1 = 0) do={
        :set InterfaceFailISP1 1 
        :log warning "ISP1 intarface link is down - clear all connections from $PBXIP"
        /ip firewall connection { remove [find src-address~$PBXIP] }
}
}

:if ([/interface get value-name=running $InterfaceISP1])  do={
        :if ($InterfaceFailISP1 = 1) do={
        :set InterfaceFailISP1 0
        :log warning "ISP1 intarface link is up - clear all connections from $PBXIP"
        /ip firewall connection { remove [find src-address~$PBXIP] }
}
}

:if ($PingResult > 0) do={
    :if ($PingFailCountISP1 > 0) do={
        :set PingFailCountISP1 ($PingFailCountISP1 - 1)
            
    :if ($PingFailCountISP1 = ($FailTreshold -1)) do={
        :log warning "ISP1 can reach $PingTarget again - bringing back original distance of routes."
        :foreach i in=[/ip route find gateway=$GatewayISP1 && static  && comment=MAIN_TABLE] do=\
            {/ip route set $i distance=([/ip route get $i distance] - $DistanceIncrease)}
        :log warning "Route distance decrease finished."
        /ip firewall connection { remove [find src-address~$PBXIP] }
    }
    }
}
/ip route rem [find dst-address="$PingTarget" . "/32"]

# Check ISP2

:if ([/interface  get value-name=running $InterfaceISP2]) do={
/ip route add dst-address=$PingTarget gateway=$GatewayISP2
:set PingResult [ping $PingTarget count=3 interface=$InterfaceISP2]

:if ($PingResult = 0) do={
    :if ($PingFailCountISP2 < ($FailTreshold+1)) do={
        :set PingFailCountISP2 ($PingFailCountISP2 + 1)}
            
    :if ($PingFailCountISP2 = $FailTreshold) do={
        :log warning "ISP2 has a problem en route to $PingTarget - increasing distance of routes."
        :foreach i in=[/ip route find gateway=$GatewayISP2 && static  && comment=MAIN_TABLE] do=\
            {/ip route set $i distance=([/ip route get $i distance] + $DistanceIncrease)}
        :log warning "Route distance increase finished."
        /ip firewall connection { remove [find src-address~$PBXIP] }
    }
}
} else={
       :if ($InterfaceFailISP2 = 0) do={
       :set InterfaceFailISP2 1
       :log warning "ISP2 interface link is down - clear all connections from $PBXIP"
        /ip firewall connection { remove [find src-address~$PBXIP] }
}
}

:if ([/interface get value-name=running $InterfaceISP2]) do={
        :if ($InterfaceFailISP2 = 1) do={
        :set InterfaceFailISP2 0
        :log warning "ISP2 intarface link is up - clear all connections from $PBXIP"
        /ip firewall connection { remove [find src-address~$PBXIP] }
}
}

:if ($PingResult > 0) do={
    :if ($PingFailCountISP2 > 0) do={
        :set PingFailCountISP2 ($PingFailCountISP2 - 1)
            
    :if ($PingFailCountISP2 = ($FailTreshold -1)) do={
        :log warning "ISP2 can reach $PingTarget again - bringing back original distance of routes."
        :foreach i in=[/ip route find gateway=$GatewayISP2 && static  && comment=MAIN_TABLE] do=\
            {/ip route set $i distance=([/ip route get $i distance] - $DistanceIncrease)}
        :log warning "Route distance decrease finished."
        /ip firewall connection { remove [find src-address~$PBXIP] }
    }
    }
}
/ip route rem [find dst-address="$PingTarget" . "/32"]



С описанием переменных все ясно, далее кратко сама логика:
1. Проверяем есть ли линк на интерфейсе(существует ли PPPoE подключение)
2. Есть — проверяем доступность $PingTarget
2а. Нет — удаляем все соединения с src-address нашей PBX. Активным становится маршрут с метрикой «2», чтобы поднялись SIP-регистрации старые соединения нужно убрать.
3. $PingTarget доступна — идем проверять ISP2
3a. $PingTarget не доступна — уменьшаем метрику маршрута через ISP1 на 2 и удаляем старые соединение из «Connection tracking»

Зачем я добавил проверку состояния интерфейса?
Если на порту нет линка, то нет смысла слать пинг.
Или, в случае с PPP интерфейсами(как у меня), пинг вообще не пройдет и нарушится логика скрипта.
[f@777777] <SAFE> interface pppoe-cl di 0
[f@777777] <SAFE> 
[f@777777] <SAFE> 
[f@777777] <SAFE> 
[f@777777] <SAFE> 
[f@777777] <SAFE> :if ([/ping 8.8.8.8 interface=rtk count=3]>0) do={:put "Yes sir"}
  SEQ HOST                                     SIZE TTL TIME  STATUS                                                                                                                                             
<!-- тут команда тупо виснет и ничего не происходит - интерфейса то нет -->

[f@777777] <SAFE> interface pppoe-cl en 0                                            
[f@777777] <SAFE> :if ([/ping 8.8.8.8 interface=rtk count=3]>0) do={:put "Yes sir"}
  SEQ HOST                                     SIZE TTL TIME  STATUS                                                                                                                                             
    0 8.8.8.8                                                 timeout                                                                                                                                            
    1 8.8.8.8                                                 timeout                                                                                                                                            
    2 8.8.8.8                                    56 254 52ms 
    sent=3 received=1 packet-loss=66% min-rtt=52ms avg-rtt=52ms max-rtt=52ms 

Yes sir



Дело за малым, добавить задание в шедулер роутера.
[f@777777] > sys sch exp
# nov/26/2015 17:37:29 by RouterOS 6.30
# software id = LY7Z-747B
#
/system scheduler
add interval=30s name=check_ISPs on-event=check_gateways policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive start-date=nov/20/2015 start-time=05:37:37

И здесь закончили, перейдем к десерту — используемым ресурсам.

Часть 3. Заключение, используемые ресурсы


Если хочется почитать о DSCP, советую воспользоваться вот этими ссылками:
1. cisco
2. ru.wikipedia
3. msdn.microsoft
4. microsin

О том как ходят пакеты в MikroTik, как настраивается Policy routing:
1. wiki Manual:Packet_Flow
2. wiki Policy_Base_Routing
3. nixman Policy_Base_Routing
4. blog.butchevans Policy_Base_Routing

Парочка полезных, хотя думаю всем известных, мануалов по Iptables:
1. ru.wikibooks.org/wiki/Iptables
2. opennet iptables guid

За сим откланиваюсь, спасибо тем, кто дочитал.
Буду безмерно рад, если окажусь полезен, ведь в этом и суть.
Всем удачи, все!
Теги:
Хабы:
Всего голосов 17: ↑17 и ↓0+17
Комментарии16

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань