autorBartłomiej Dabiński

Konfiguracja

Spróbujemy teraz ustawić MikroTika do pracy z dwoma łączami w wariancie failover. Zbudujemy do tego celu topologię sieciową jak na rysunku:

https://sekurak.pl/wp-content/uploads/2015/07/mikrotik3.png

 

Dysponujemy dwoma łączami: do ISP1 (dostęp główny) i do ISP2 (dostęp zapasowy). Łącze główne działa na adresie IP otrzymanym z serwera DHCP, łącze zapasowe zrealizowane jest w technice PPPoE. Są to dwie najczęściej stosowane przez operatorów techniki. Nasz przykład jest adekwatny także do dość typowej sytuacji, gdy jako łącza zapasowego używamy połączenia PPP zestawionego za pośrednictwem modemu 3G/4G podłączonego bezpośrednio do płyty RouterBoard. Tutaj są dostępne listy wspieranych modemów 3G i 4G.

Do routera podłączony jest komputer użytkownika przy użyciu adresacji statycznej. Adres IP użytkownika to 10.0.0.2/24, jego brama to 10.0.0.1 (MikroTik).

Zacznijmy od skonfigurowania klientów DHCP oraz PPPoE oraz zaadresowania interfejsu dla sieci lokalnej.

Dodanie klienta DHCP:

[admin@MikroTik] > ip dhcp-client add interface=ether1 disabled=no

Zweryfikujemy jego działanie:

[admin@MikroTik] > ip dhcp-client print
Flags: X - disabled, I - invalid
#   INTERFACE    USE-PEER-DNS ADD-DEFAULT-ROUTE STATUS       ADDRESS
0   ether1       yes         yes               bound         192.168.39.102/24

 

Jak widać, klient DHCP działa poprawnie i otrzymaliśmy adres 192.168.39.102/24.

Dodanie klienta PPPoE (login: user, hasło: password):

[admin@MikroTik] > interface pppoe-client add interface=ether2 user=user password=password disabled=no

Weryfikacja

[admin@MikroTik] > interface pppoe-client print
Flags: X - disabled, R - running
0 R name="pppoe-out1" max-mtu=1480 max-mru=1480 mrru=disabled interface=ether2 user="user" password="password" profile=default keepalive-timeout=60 service-name="" ac-name="" add-default-route=no dial-on-demand=no use-peer-dns=no allow=pap,chap,mschap1,mschap2[admin@MikroTik] > interface pppoe-client monitor 0
status: connected
uptime: 44s
active-links: 1
encoding:
service-name: service1
ac-name: host
ac-mac: 02:DF:C4:6B:9E:94
mtu: 1480
mru: 1480
local-address: 172.16.0.2
remote-address: 172.16.0.1

Klient PPPoE również działa poprawnie, otrzymaliśmy adres 172.16.0.2.

Ustawienie adresu statycznego dla ether3:

[admin@MikroTik] > ip address add address=10.0.0.1/24 interface=ether3

Weryfikacja wpisów IP

[admin@MikroTik] > ip address print
Flags: X - disabled, I - invalid, D - dynamic
#   ADDRESS           NETWORK         INTERFACE
0 D 192.168.39.102/24 192.168.39.0   ether1
1 D 172.16.0.2/32     172.16.0.1     pppoe-out1
2   10.0.0.1/24       10.0.0.0       ether3

Aby w sieci lokalnej był dostęp do Internetu, potrzebujemy jeszcze skonfigurować NAT dla obu łącz:

[admin@MikroTik] > ip firewall nat add chain=srcnat out-interface=ether1 action=masquerade
[admin@MikroTik] > ip firewall nat add chain=srcnat out-interface=pppoe-out1 action=masquerade

Na komputerze ustawiamy statycznie adresację 10.0.0.2/24 i bramę 10.0.0.1. Sprawdźmy połączenie z Internetem, pingując do publicznego serwera DNS Googla:

1. Sposób prosty

W tej chwili mamy dostęp do Internetu przez łącze ISP1, ponieważ MikroTik domyślnie dodaje domyślną trasę routingu dla klienta DHCP, a nie dodaje dla klienta PPPoE (zachowanie to dla obu klientów można zmienić poprzez atrybut add-default-route).

Potrzebujemy włączyć sprawdzanie dostępności bramy domyślnej od ISP1. Mamy tu dwie opcje do wyboru: ping lub ARP. Jeśli brama operatora odpowiada na zapytania ICMP, to zalecana jest opcja ping, ponieważ zapytania ping są przesyłane unicastowo. Przy opcji ARP MikroTik będzie wysyłał pod adres broadcastowy pakiet ARP request.

Ponieważ domyślnie dla dodanej trasy nie ma zaznaczonej opcji check-gateway, a tras dodanych dynamicznie nie możemy edytować bezpośrednio, to skorzystamy z funkcji filtrowania tras routingu:

[admin@MikroTik] > routing filter add action=passthrough chain=dynamic-in disabled=no set-check-gateway=ping

Reguła ta będzie ustawiać opcję check-gateway także dla innych dynamicznych tras w tablicy routingu (z wyjątkiem tras bezpośrednio podłączonych i dodanych przez protokoły routingu). W niektórych sytuacjach takie rozwiązanie może nam nie odpowiadać, więc ograniczmy zakres reguły filtrującej trasy routingu:

[admin@MikroTik] > ip dhcp-client set 0 default-route-distance=254

[admin@MikroTik] > routing filter set 0 distance=254 set-distance=1

Zmieniamy dystans administracyjny (na 254) dla trasy tworzonej przez klienta DHCP, aby użyć tego parametru jako wyróżnika. Dla wcześniej utworzonej reguły filtrowania trasy routingu dodajemy warunek, aby przechwytywał tylko trasy z dystansem 254 i zmieniał go na pierwotną wartość, czyli 1.

Jeżeli główne łącze jest zestawione przy użyciu techniki PPPoE lub adresacji statycznej, to powyższe kombinacje nie są potrzebne. Wystarczy wtedy dodać statyczną trasę domyślną, ustawiając jej opcję check-gateway w analogiczny sposób, w jaki teraz dodamy trasę zapasową przez łącze PPPoE:

[admin@MikroTik] > ip route add gateway=pppoe-out1 distance=2

Dystans administracyjny (distance) dla trasy zapasowej ustawiliśmy na 2, czyli wyższy niż dla trasy głównej (1). Jeśli router ma w tablicy routingu dwie trasy do tej samej sieci docelowej (np. dwie trasy domyślne), to aktywna będzie trasa z niższym dystansem administracyjnym. Trasa zapasowa (z wyższym dystansem) stanie się aktywna dopiero wtedy, gdy trasa główna stanie się niedostępna.

W tej chwili mamy już działający, prosty mechanizm typu failover. Jego główna wada polega na tym, że odpytywana jest tylko adres IP bramy, czego konsekwencje zostały wcześniej opisane.

Kolejna wada, to że nie mamy wpływu na czułość/szybkość wykrycia usterki. Przy opcji check-gateway MikroTik wysyła zapytanie co 10 sekund, a po dwóch kolejnych brakach odpowiedzi określi trasę jako nieosiągalną. Do przywrócenia aktywnego stanu trasy wystarczy jedna odpowiedź, zatem mechanizm będzie podatny na flapowanie łącza.

Dalej, nie mamy możliwości ustawić maksymalnego dopuszczalnego opóźnienia.

Pewną wadą jest też to, że MT zmienia łącza w sposób cichy – bez żadnego powiadomienia ani śladu w historii. Możemy włączyć debugowanie:

[admin@MikroTik] > system logging add topics=route,debug,!calc

Niestety, zapisywane logi są mało precyzyjne i mało czytelne.

2. Sposób zaawansowany

Wykorzystując możliwości, jakie daje użycie skryptów na MikroTiku, możemy zbudować znacznie lepszy mechanizm, który skutecznie adresuje wyżej wspomniane braki. Poniższe podejście ma ograniczenie dotyczące sytuacji, gdy jedno z łącz (tak jak w naszym przykładzie) opiera się na DHCP. Potrzebujemy dodać trasy domyślne dla łącz do kilku tablic routingu, a MT nie ma funkcji, która by to pozwoliła zrobić w przypadku klienta DHCP (i nie pomogą też reguły filtrowania tras routingu). Przyjmiemy w tym momencie założenie, że brama domyślna, jaką otrzymujemy od operatora przez DHCP, jest zawsze taka sama (często tak jest) i możemy ją ustawić wpisem statycznym. (Jeśli takiego założenia nie możemy poczynić, to problem da się rozwiązać prostym skryptem, który będzie aktualizował trasy statyczne, jeśli nowa trasa otrzymana z DHCP będzie inna.)

Zasada mechanizmu jest taka, że monitorowana będzie grupa adresów IP należących do hostów publicznie dostępnych w Internecie (w konfiguracji skryptu uzupełniamy listę adresów). Aby łącze zostało zakwalifikowane jako uszkodzone, to dla każdego z adresów z listy muszą wystąpić minimalne określone straty pakietów. Domyślnie dla każdego adresu musi zginąć min. 2/10 pakietów. Dzięki takiemu zachowaniu mechanizm jest odporny na sytuację, gdy jeden z monitorowanych hostów przestanie udzielać (z jakiejkolwiek przyczyny) odpowiedzi na zapytania ICMP. Dopóki przynajmniej jeden host z listy będzie działał prawidłowo, nasz mechanizm nie będzie generował fałszywych przełączeń na łącze zapasowe.

Przejście na łącze główne nastąpi dopiero wtedy, jeśli otrzymamy określoną liczbę odpowiedzi (domyślnie 10/10) od przynajmniej jednego z hostów. Jest to warunek zapobiegający częstym przełączeniom w przypadku dużych strat pakietów na łączu. Tutaj także wystarczy nam jeden działający monitorowany host.

MikroTik nie pozwala na bezpośrednie określenie czasu timeout dla polecenia ping, natomiast maksymalne opóźnienie ustawiamy, wykorzystując parametr interwału pingowania. Domyślnie w skrypcie jest on ustawiony na 300 ms. Jeśli MikroTik w momencie wysyłania kolejnego zapytania ICMP nie ma odpowiedzi na poprzednie zapytanie, to jest to traktowane jako strata.

Mechanizm jest uogólniony na dowolną liczbę łącz. Każde z łącz jest niezależnie testowane i wyłączane w przypadku wykrycia awarii, co jest dobrym punktem wyjścia do wdrożenia load balancingu. W naszym przypadku (czysty failover) monitorowanie łącza zapasowego może wydawać się zbędne, jednak warto je prowadzić, aby uniknąć sytuacji, gdy o tym, że łącza zapasowe (od dawna) nie działa, dowiadujemy się dopiero w momencie awarii głównego.

Parametry mechanizmu są konfigurowalne, więc możemy go dostosować do naszych wymagań, ustalając właściwy kompromis np. pomiędzy szybkością wykrycia usterki a ryzykiem narażenia się na problemy w przypadku flapowania głównego łącza.

W przypadku przełączeń, skrypt zapisuje odpowiednią informację w logach, co pozwala na szybkie wykrycie usterki przez administratora, a także prześledzenie historii.

Pomimo że poniższy skrypt nie wykorzystuje żadnych zaawansowanych funkcji, zalecane jest staranne przetestowanie jego działania w przypadku innej wersji systemu RouterOS niż v6.27 oraz każdorazowo po aktualizacji systemu.

MikroTik w swej historii wielokrotnie dokonywał zmian w składni (czasem bardzo drobnych), zwykle nie wspominając o tym w changelogu. W takiej okoliczności skrypt się nie wykona (ze względu na błąd składni). W razie wystąpienia błędu, skrypt powinien zapisać w logach „wan monitoring: blad wykonania skryptu”

Wróćmy teraz do stanu podstawowej konfiguracji routera, tzn. sprzed części „Sposób prosty”. Ponieważ wszystkie trasy będziemy dodawać statycznie, to wyłączymy dodawanie trasy domyślnej przez klienta DHCP. Zanim jednak to zrobimy, sprawdźmy, jaka jest ta brama, aby wiedzieć, co potem dodać:

[admin@MikroTik] > ip route print
Flags: X - disabled, A - active, D - dynamic,
C - connect, S - static, r - rip, b - bgp, o - ospf, m - mme,
B - blackhole, U - unreachable, P - prohibit
#     DST-ADDRESS       PREF-SRC       GATEWAY           DISTANCE
0 ADS 0.0.0.0/0                          192.168.39.1             1
1 ADC 10.0.0.0/24       10.0.0.1       ether3                   0
2 ADC 172.16.0.1/32     172.16.0.2     pppoe-out1               0
3 ADC 192.168.39.0/24   192.168.39.102 ether1                  0

Jak widzimy, brama domyślna na łączu głównym to 192.168.39.1. Wyłączmy teraz jej automatyczne dodawanie przez klienta DHCP:

[admin@MikroTik] > ip dhcp-client set 0 add-default-route=no

Dodajemy teraz trasy statyczne. Oprócz domyślnej tablicy routingu, używamy osobnych tablic dla każdego z łącz, które będą służyć do testowania danego łącza. W tablicy głównej do tras dodajemy komentarze. Jest ważne, aby komentarze były dokładnie w formacie „wan monitoring: laczeX” (gdzie X to nr łącza odpowiadający numerowi na liście lacza_interfejsy w skrypcie), ponieważ komentarz ten jest wykorzystywany przez skrypt do wykonywania operacji na trasie.

[admin@MikroTik] > ip route add gateway=192.168.39.1 comment="wan_monitoring: lacze1"
[admin@MikroTik] > ip route add gateway=pppoe-out1 distance=2 comment="wan_monitoring: lacze2"
[admin@MikroTik] > ip route add gateway=192.168.39.1 routing-mark=lacze1
[admin@MikroTik] > ip route add type=blackhole routing-mark=lacze1 distance=2
[admin@MikroTik] > ip route add gateway= pppoe-out1 routing-mark=lacze2
[admin@MikroTik] > ip route add type=blackhole routing-mark=lacze2 distance=2

W tablicach do testowania poszczególnych łącz dodane są też wpisy typu blackhole ze słabszym dystansem. Dzięki tym trasom, w razie niedostępności bramy domyślnej dla wybranego łącza, ruch nie będzie przetwarzany przez główną tablicę routingu (i nie zostanie przekierowany na inne łącze).

Skrypt należy dodać do harmonogramu (System > Scheduler), zalecany interwał powinien być większy o min. 10 s.[2] od iloczynu maksymalnego tolerowanego opóźnienia (ping_interval), liczby łącz (lacza_interfejsy), liczby pingów do sprawdzania danego hosta (ile_pingow) i liczby hostów do monitorowania (adresy_monit). W naszym przykładzie iloczyn będzie miał wartość 300ms*2*10*4 = 24s, zatem zalecana wartość interwału dla harmonogramu to ok. 35 sekund.

:do {
   # ---------------------------- tu ustaw parametry ----------------------------
   # interfejsy na ktorych dzialaja lacza: {"lacze1";"lacze2"; ...}
   :local "lacza_interfejsy" {"ether1";"pppoe-out1"}
   # lista adresow ktore beda pingowane do weryfikacji kazdego z lacz
   :local "adresy_monit" {"176.31.167.200";"213.180.141.140";"194.204.159.1";"85.232.232.65"}
   # maksymalne tolerowane opoznienie
   :local "ping_interval" "300ms"
   # liczba wysylanych pingow w celu sprawdzenia danego adresu
   :local "ile_pingow" 10
   # co najmniej ile odpowiedzi musi wrocic aby uznac ze adres nadal dziala
   # jesli wroci mniej to nastepuje zmiana stanu up > down
   # wartosc nie moze byc wieksza niz ile_pingow
   :local "ile_pingow_up" 7
   # co najmniej ile pingow musi wrocic aby uznac ze adres zmienil stan down > up
   # wartosc powinna byc wieksza niz ile_pingow_updown
   # wartosc nie moze byc wieksza niz ile_pingow
   :local "ile_pingow_downup" 10
   # --------------------------------------------------------------------------
   :global "wan_status"
   :global aktywne
   :local "ile_lacz" [:len $"lacza_interfejsy"]
   :local "ile_adresow" [:len $"adresy_monit"]
   :local zmiany
   :local "log_tresc" ""
   :if ([:typeof $"wan_status"] = "nothing") do={
       :log debug "inicjalizacja"
       :set aktywne 1
       :for i from=1 to=$"ile_lacz" do={
           :set "wan_status" ($"wan_status","up")
           /ip route disable [find comment=("wan_monitoring: lacze" . $i)]
           :if ([/ip route get [find comment=("wan_monitoring: lacze" . $i)] distance] < [/ip route get [find comment=("wan_monitoring: lacze" . $aktywne)] distance]) do={
               :set aktywne $i
           }
       }
       /ip route enable [find comment=("wan_monitoring: lacze" . $aktywne)]
   }
   :for i from=1 to=$"ile_lacz" do={
       :set zmiany ($zmiany,"bez_zmian")
       :local "tab_rut_test" ("lacze" . $i)
       :local "ile_dziala" 0
       :if (($"wan_status" -> ($i - 1)) = "down") do={
           :for j from=1 to=$"ile_adresow" do={
               :if ([/ping ($"adresy_monit" -> ($j - 1)) interval=$"ping_interval" routing-table=$"tab_rut_test" count=$"ile_pingow"] >= $"ile_pingow_downup") do={
                   :set "ile_dziala" ($"ile_dziala" + 1)
               }
           }
           :if ($"ile_dziala" > 0) do={
               :set ($zmiany -> ($i - 1)) "downup"
           }
       } else={
           :for j from=1 to=$"ile_adresow" do={
               :if ([/ping ($"adresy_monit" -> ($j - 1)) interval=$"ping_interval" routing-table=$"tab_rut_test" count=$"ile_pingow"] >= $"ile_pingow_up") do={
                   :set "ile_dziala" ($"ile_dziala" + 1)
               }
           }
           :if ($"ile_dziala" = 0) do={
               :set ($zmiany -> ($i - 1)) "updown"
           }
       }
   }
   :for i from=1 to=$"ile_lacz" do={
       :if (($zmiany -> ($i - 1)) = "updown") do={
           :set ($"wan_status" -> ($i - 1)) "down"
           :if ($"log_tresc" != "") do={
               :set $"log_tresc" ($"log_tresc" . ", ")
           }
           :set $"log_tresc" ($"log_tresc" . "lacze" . $i . " down")
           :if ($aktywne = $i) do={
               /ip route disable [find comment=("wan_monitoring: lacze" . $i)]
               # czyszczenie wpisow connection tracking
               :if ([/interface get ($"lacza_interfejsy" -> ($i - 1)) disabled] = no) do={
                   /interface disable ($"lacza_interfejsy" -> ($i - 1))
                   /interface enable ($"lacza_interfejsy" -> ($i - 1))
               }
               # czyszczenie wpisow connection tracking - koniec
               :local distance 255
               :for j from=1 to=$"ile_lacz" do={
                   :if (($"wan_status" -> ($j - 1)) = "up" && [/ip route get [find comment=("wan_monitoring: lacze" . $j)] distance] < distance) do={
                       :set aktywne $j
                       :set distance [/ip route get [find comment=("wan_monitoring: lacze" . $j)] distance]
                   }
               }
              :if ($distance < 255) do={
                   /ip route enable [find comment=("wan_monitoring: lacze" . $aktywne)]
               } else={
                       :set aktywne 0
               }
           }
       }
       :if (($zmiany -> ($i - 1)) = "downup") do={
           :set ($"wan_status" -> ($i - 1)) "up"
           :if ($"log_tresc" != "") do={
               :set $"log_tresc" ($"log_tresc" . ", ")
           }
           :set $"log_tresc" ($"log_tresc" . "lacze" . $i . " up")
         :if ($aktywne = 0) do={
               :set aktywne $i
               /ip route enable [find comment=("wan_monitoring: lacze" . $i)]
           } else={
               :if ([/ip route get [find comment=("wan_monitoring: lacze" . $i)] distance] < [/ip route get [find comment=("wan_monitoring: lacze" . $aktywne)] distance]) do={
                   /ip route disable [find comment=("wan_monitoring: lacze" . $aktywne)]
                   # czyszczenie wpisow connection tracking
                   :if ([/interface get ($"lacza_interfejsy" -> ($aktywne - 1)) disabled] = no) do={
                       /interface disable ($"lacza_interfejsy" -> ($aktywne - 1))
                       /interface enable ($"lacza_interfejsy" -> ($aktywne - 1))
                   }
                   # czyszczenie wpisow connection tracking - koniec
                   :set aktywne $i
                   /ip route enable [find comment=("wan_monitoring: lacze" . $i)]
               }
           }
       }
   }
   if ($"log_tresc" != "") do={
       :log error ("wan_monitoring: " . $"log_tresc")
   }
} on-error={
   :log error "wan_monitoring: blad wykonania skryptu"
}

3. Problem z tablicą Connection Tracking

Na koniec zwróćmy uwagę na fragmenty skryptu następujący po komentarzu „czyszczenie wpisow connection tracking”. O co tutaj chodzi? Historia zaczyna się od tego, że aby udostępnić Internet w sieci lokalnej (z adresacją prywatną), to musimy wykonać NAT wielu adresów lokalnych na jeden adres zewnętrzny przydzielony przez operatora. Ponieważ taki NAT nadpisuje adres źródłowy pakietu, to pakiet nie zawiera dalej już informacji jednoznacznie identyfikującej rzeczywistego nadawcy w naszej sieci. Zatem, aby wiedzieć do kogo skierować dany pakiet przychodzący z Internetu, router z NAT-em utrzymuje tablicę połączeń. W momencie gdy z sieci lokalnej nawiązywane jest nowe połączenie[3], to jest ono rejestrowane w tablicy razem z parametrami opisującymi adres źródłowy (oryginalny i po translacji) i docelowy, protokół i numery portów. Dzięki tym informacjom router przekazuje pakiety przychodzące z Internetu do właściwych hostów, wykonując odwrotną translację.

Sedno problemu tkwi w tym, że podczas zmiany domyślnych tras routingu wpisy w tabeli śledzenia połączeń pozostają bez zmian. Gdy klient wysyła kolejne pakiety będące częścią raz zarejestrowanego połączenia, to router na potrzeby NAT-u korzysta z wcześniej dodanego wpisu tablicy połączeń, nie zwracając uwagi, że przypisany do połączenia adres zewnętrzny uległ zmianie. Zatem router, pomimo że, wysyłając pakiety już na nowe (zapasowe) łącze, będzie wykonywał translację na adres zewnętrzny przydzielony do łącza głównego. Taka komunikacja oczywiście się nie powiedzie.

Dla większości komunikacji problem będzie znikomy, bo nawiązanie nowego połączenia TCP spowoduje dodanie nowego wpisu (z właściwym adresem zewnętrznym) do tablicy. Problem będzie istotny dla protokołów opartych np. o UDP (np. SIP), bo każdy nowy pakiet wysłany przez klienta będzie odświeżał błędny wpis w tablicy śledzenia połączeń. Wtedy czas bezczynności nigdy nie wygaśnie i usługa będzie niedostępna dopóki klient nie wstrzyma się na dłuższy czas od transmisji lub do momentu, gdy zdesperowany administrator zrestartuje router, czyszcząc przy tym tablicę śledzenia połączeń. Problem ten dotyczy także „sposobu prostego”, opisanego wyżej.

Optymalnym rozwiązaniem byłoby przefiltrowanie tablicy połączeń w momencie zmiany łącza i skasowanie nieaktualnych wpisów. Niestety – pomimo że system RouterOS teoretycznie daje taką możliwość, to operacja często kończy się błędem action failed (6). Zgłoszenia błędu na oficjalnym forum nie spotkały ze się specjalnym zainteresowaniem producenta i problem pozostaje nierozwiązany.

W tej sytuacji, skutecznym i prostym (aczkolwiek niezbyt eleganckim) obejściem tego problemu jest wymuszone wyczyszczenie części wpisów z tablicy połączeń poprzez zrestartowanie interfejsu, którym ruch wychodził przed zmianą łącza.

— Bartłomiej Dabiński

 

 

 

Oryginał TU

WordPress Themes