Одной из проблем современных сетей является их хрупкость. Множество правил фильтраций, политик обмена маршрутной информации, протоколов динамического роутинга делают сети запутанными и подверженными влиянию человеческого фактора. Авария на сети может произойти ненамеренно при внесении изменений в route-map или ACL (один, два). Нам определено не хватает инструмента, позволяющего оценить поведение сети с новой конфигурацией перед внесением изменений в продакшн. Хочется точно знать, будет ли мне доступна сеть A, если я отфильтрую часть BGP-анонсов, полученных от провайдера B? Каким маршрутом пойдут пакеты из сети C к серверу D, если на одном из транзитных линков я увеличу IGP метрику в два раза? Ответить на эти и многие другие вопросы нам поможет Batfish!
Обзор Batfish
Batfish – это инструмент для моделирования сети. Основным его назначением является тестирование конфигурационных изменений перед их внесением в рабочую сеть. Batfish так же можно использовать для анализа и проверки текущего состояния сети. Существующим CI/CD процессам в сетевом мире явно не хватает инструмента для тестирования новых конфигураций. Batfish позволяет решить эту проблему.
Batfish не требует непосредственного прямого доступа к действующему сетевому оборудованию, Batfish моделирует поведение сети на основе данных, содержащихся в конфигурационных файлах устройств.
Batfish может:
- определить статус соседства протоколов динамической маршрутизации в сети (BGP, IS-IS, OSPF)
- просчитать RIB каждого сетевого элемента
- проверить настройки NTP, AAA, MTU
- позволить определить, блокирует ли ACL прохождение сетевого трафика (аналог packet-tracer на Cisco ASA)
- проверить наличие end-to-end связности между хостами внутри сети
- показать путь прохождения трафика через сеть (виртуальная трассировка)
Поддерживаемые платформы:
- Arista
- Aruba
- AWS (VPCs, Network ACLs, VPN GW, NAT GW, Internet GW, Security Groups)
- Cisco (NX-OS, IOS, IOS-XE, IOS-XR и ASA)
- Dell Force10
- Foundry
- iptables
- Juniper (MX, EX, QFX, SRX, T-series, PTX)
- MRV
- Palo Alto Networks
- Quagga / FRR
- Quanta
- VyOS
Batfish – это Java приложение. Для удобной работы с ним был написан Pybatfish — python SDK.
Перейдем к практике. Я продемонстрирую Вам возможности Batfish на примере.
Пример
Под нашим управлением находится две автономные системы: AS 41214 и AS 10631. В качестве IGP в AS 41214 используется IS-IS, в AS 10631 – OSPF. Внутри каждой AS используется IBGP-fullmesh. LDN-CORE-01 анонсирует своим соседям по BGP префикс 135.65.0.0/19, MSK-CORE-01 – 140.0.0.0/24. Обмен маршрутной информацией между автономными системами происходит на стыке HKI-CORE-01 — SPB-CORE-01.
HKI-CORE-01, STH-CORE-01 — Junos routers
LDN-CORE-01, AMS-CORE-01, SPB-CORE-01, MSK-CORE-01 — Cisco IOS routers
Установим контейнер с Batfish и python SDK:
docker pull batfish/allinone
docker run batfish/allinone
docker container exec -it <container> bash
Познакомимся с библиотекой через интерактивный режим python:
root@ea9a1559d88e:/# python3
--------------------
>>> from pybatfish.client.commands import bf_logger, bf_init_snapshot
>>> from pybatfish.question.question import load_questions
>>> from pybatfish.question import bfq
>>> import logging
>>> bf_logger.setLevel(logging.ERROR)
>>> load_questions()
>>> bf_init_snapshot('tmp/habr')
'ss_e8065858-a911-4f8a-b020-49c9b96d0381'
bf_init_snapshot('tmp/habr') — функция загружает конфигурационные файлы в Batfish и подготавливает их к анализу.
/tmp/habr – директория с конфигурационными файлами роутеров.
root@ea9a1559d88e:/tmp/habr# tree
.
`-- configs
|-- AMS-CORE-01.cfg
|-- HKI-CORE-01.cfg
|-- LDN-CORE-01.cfg
|-- MSK-CORE-01.cfg
|-- SPB-CORE-01.cfg
`-- STH-CORE-01.cfg
1 directory, 6 files
Теперь давайте определим статус BGP-сессий на роутере LDN-CORE-01:
>>> bgp_peers = bfq.bgpSessionStatus(nodes='LDN-CORE-01').answer().frame()
>>> bgp_peers
Node VRF Local_AS Local_IP Remote_AS Remote_Node Remote_IP Session_Type Est_Status
0 ldn-core-01 default 41214 172.20.20.1 41214 sth-core-01 172.20.20.2 IBGP EST
1 ldn-core-01 default 41214 172.20.20.1 41214 ams-core-01 172.20.20.3 IBGP EST
2 ldn-core-01 default 41214 172.20.20.1 41214 hki-core-01 172.20.20.4 IBGP EST
Ну, как? Похоже на правду?
LDN-CORE-01#show ip bgp summary
…
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd
172.20.20.2 4 41214 629 669 9 0 0 00:56:51 0
172.20.20.3 4 41214 826 827 9 0 0 01:10:18 0
172.20.20.4 4 41214 547 583 9 0 0 00:49:24 1
Теперь давайте посмотрим, какие IS-IS маршруты есть в RIB на маршрутизаторе HKI-CORE-01 по мнению Batfish:
>>> isis_routes = bfq.routes(nodes='HKI-CORE-01', protocols='isis').answer().frame()
>>> isis_routes
Node VRF Network Next_Hop Next_Hop_IP Protocol Admin_Distance Metric Tag
0 hki-core-01 default 172.20.20.3/32 ams-core-01 10.0.0.6 isisL2 18 20 None
1 hki-core-01 default 172.20.20.1/32 ams-core-01 10.0.0.6 isisL2 18 30 None
2 hki-core-01 default 172.20.20.2/32 sth-core-01 10.0.0.4 isisL2 18 10 None
3 hki-core-01 default 172.20.20.1/32 sth-core-01 10.0.0.4 isisL2 18 30 None
4 hki-core-01 default 10.0.0.0/31 sth-core-01 10.0.0.4 isisL2 18 20 None
5 hki-core-01 default 10.0.0.2/31 ams-core-01 10.0.0.6 isisL2 18 20 None
В командной строке:
showroute@HKI-CORE-01# run show route table inet.0 protocol isis
inet.0: 18 destinations, 18 routes (18 active, 0 holddown, 0 hidden)
+ = Active Route, - = Last Active, * = Both
10.0.0.0/31 *[IS-IS/18] 00:51:25, metric 20
> to 10.0.0.4 via ge-0/0/0.0
10.0.0.2/31 *[IS-IS/18] 00:51:45, metric 20
> to 10.0.0.6 via ge-0/0/1.0
172.20.20.1/32 *[IS-IS/18] 00:51:25, metric 30
to 10.0.0.4 via ge-0/0/0.0
> to 10.0.0.6 via ge-0/0/1.0
172.20.20.2/32 *[IS-IS/18] 00:51:25, metric 10
> to 10.0.0.4 via ge-0/0/0.0
172.20.20.3/32 *[IS-IS/18] 00:51:45, metric 20
> to 10.0.0.6 via ge-0/0/1.0
Отлично! Полагаю, Вам стало яснее, что есть Batfish.
В начале статьи я писал, что Batfish можно использовать для проверки конфигурационных изменений перед их внесением в «боевую» сеть. Теперь я предлагаю рассмотреть процесс проведения тестирования сети на базе RobotFramework. Для этого я написал небольшой модуль на основе PyBatfish, позволяющий выполнять следующие проверки:
- Определять статус BGP-сессий в сети
- Определять состояние IS-IS соседей
- Проверять наличие end-to-end связности между узлами в сети с демонстрацией трассировки
- Определять размер RIB на роутере для определенного протокола динамической маршрутизации
import logging
from pybatfish.client.commands import bf_logger, bf_init_snapshot
from pybatfish.question.question import load_questions, list_questions
from pybatfish.question import bfq
from pybatfish.datamodel.flow import HeaderConstraints, PathConstraints
from robot.api import logger
class LibraryBatfish(object):
def __init__(self, snapshot):
bf_logger.setLevel(logging.ERROR)
load_questions()
bf_init_snapshot(snapshot)
def check_bgp_peers(self):
not_established_peers = list()
bgp_peers = bfq.bgpSessionStatus().answer()
for peer in bgp_peers.rows:
if peer.get('Established_Status') != 'ESTABLISHED':
not_established_peers.append(dict.fromkeys(peer.get('Local_IP').split(), peer.get('Remote_IP').get('value')))
if len(not_established_peers) == 0:
return 1
else:
logger.warn('BGP neighbors are not in an established state:')
for neighborship in not_established_peers:
for peer in neighborship:
logger.warn('{} - {}'.format(peer, neighborship.get(peer)))
return 0
def check_routes(self, node, protocol):
routes = bfq.routes(nodes=node, protocols=protocol).answer()
return len(routes.rows)
def check_isis_neighbors(self, description):
not_isis_enabled_links = list()
for link in self._get_isis_enabled_links(description):
if link not in self._get_isis_neighbors():
not_isis_enabled_links.append(link)
if len(not_isis_enabled_links) == 0:
return 1
else:
for link in not_isis_enabled_links:
logger.warn('{} {} has no IS-IS neighbor'.format(link.get('hostname'), link.get('interface')))
return 0
def ping(self, source_ip, destination_ip):
ip_owners = bfq.ipOwners().answer()
traceroute = self._get_traceroute_status(source_ip, destination_ip, ip_owners)
reverse_traceroute = self._get_traceroute_status(destination_ip, source_ip, ip_owners)
if traceroute == True and reverse_traceroute == True:
self._show_trace(source_ip, destination_ip, ip_owners)
return 1
else:
logger.warn('Ping {} -> {} failed'.format(source_ip, destination_ip))
return 0
def _get_traceroute_status(self, source_ip, destination_ip, addresses):
tracert = self._unidirectional_virtual_traceroute(source_ip, destination_ip, addresses)
isAccepted = True
if tracert != None:
for trace in tracert.rows[0].get('Traces'):
if trace.get('disposition') != 'ACCEPTED':
isAccepted = False
if isAccepted == True:
return True
else:
return False
def _get_paths(self, source_ip, destination_ip, addresses):
tracert = self._unidirectional_virtual_traceroute(source_ip, destination_ip, addresses)
traces = tracert.rows[0].get('Traces')
paths = dict()
path_number = 1
for trace in traces:
if trace.get('disposition') == 'ACCEPTED':
path = list()
for hop in trace.get('hops'):
path.append(hop.get('node').get('name'))
paths[path_number] = path
path_number += 1
return paths
def _unidirectional_virtual_traceroute(self, source_ip, destination_ip, addresses):
for address in addresses.rows:
if address.get('IP') == source_ip:
node = address.get('Node').get('name')
int = address.get('Interface')
headers = HeaderConstraints(srcIps=source_ip, dstIps=destination_ip, ipProtocols=['ICMP'])
try:
tracert = bfq.traceroute(startLocation="{}[{}]".format(node,int), headers=headers).answer()
return tracert
except:
logger.warn('{} address has not been found'.format(source_ip))
def _get_isis_enabled_links(self, description='core-link'):
isis_enabled_links = list()
interfaces = bfq.interfaceProperties().answer()
for int in interfaces.rows:
if int.get('Description') != None and description in int.get('Description'):
isis_enabled_links.append({'hostname' : int.get('Interface').get('hostname'),
'interface' : int.get('Interface').get('interface')})
return isis_enabled_links
def _get_isis_neighbors(self):
isis_neighbors = list()
isis_adjacencies = bfq.edges(edgeType='isis').answer()
for neighbor in isis_adjacencies.rows:
isis_neighbors.append(neighbor.get('Interface'))
return isis_neighbors
def _show_trace(self, source_ip, destination_ip, addresses):
logger.console('nTraceroute to {} from {}'.format(destination_ip, source_ip))
paths = self._get_paths(source_ip, destination_ip, addresses)
path_num = 1
for path in paths:
n = 1
logger.console('n Path N{}'.format(path_num))
for hop in paths.get(path):
logger.console(' {} {}'.format(n, hop))
n += 1
path_num += 1
Сценарий N1
Под моим управлением находится все та же сеть. Допустим, мне требуется привести в порядок фильтры на границе AS 41214 и AS 10631 и заблокировать на стыке пакеты, содержащие в source или destination ip адреса из диапазона BOGONS.
Запускаем тест до внесения изменений.
Тесты пройдены.
Внесем изменения в тестовую конфигурацию роутера HKI-CORE-01 — /tmp/habr/configs/HKI-CORE-01.cfg:
set firewall family inet filter BOGONS term TERM010 from address 0.0.0.0/8
set firewall family inet filter BOGONS term TERM010 from address 10.0.0.0/8
set firewall family inet filter BOGONS term TERM010 from address 100.64.0.0/10
set firewall family inet filter BOGONS term TERM010 from address 127.0.0.0/8
set firewall family inet filter BOGONS term TERM010 from address 169.254.0.0/16
set firewall family inet filter BOGONS term TERM010 from address 172.16.0.0/12
set firewall family inet filter BOGONS term TERM010 from address 192.0.2.0/24
set firewall family inet filter BOGONS term TERM010 from address 192.88.99.0/24
set firewall family inet filter BOGONS term TERM010 from address 192.168.0.0/16
set firewall family inet filter BOGONS term TERM010 from address 198.18.0.0/15
set firewall family inet filter BOGONS term TERM010 from address 198.51.100.0/24
set firewall family inet filter BOGONS term TERM010 from address 203.0.113.0/24
set firewall family inet filter BOGONS term TERM010 from address 224.0.0.0/4
set firewall family inet filter BOGONS term TERM010 from address 240.0.0.0/4
set firewall family inet filter BOGONS term TERM010 then discard
set firewall family inet filter BOGONS term PERMIT-IP-ANY-ANY then accept
set interfaces ge-0/0/2.0 family inet filter input BOGONS
set interfaces ge-0/0/2.0 family inet filter output BOGONS
Запускаем тест.
Я был очень близок, но как показывает вывод теста, после внесенных изменений BGP соседство 192.168.30.0 – 192.168.30.1 находится не в состоянии Established -> как следствие, теряется IP связность между точками 135.65.0.1 <-> 140.0.0.1. Что же не так? Смотрим внимательно в конфигурацию HKI-CORE-01 и видим, что eBGP пиринг установлен на приватных адресах:
showroute@HKI-CORE-01# show interfaces ge-0/0/2 | display set
set interfaces ge-0/0/2 description SPB-CORE-01
set interfaces ge-0/0/2 unit 0 family inet filter input BOGONS
set interfaces ge-0/0/2 unit 0 family inet filter output BOGONS
set interfaces ge-0/0/2 unit 0 family inet address 192.168.30.0/31
Вывод: необходимо поменять адреса на стыке или добавить в исключение подсеть 192.168.30.0/31.
Добавлю сеть на стыке в исключение, вновь обновлю /tmp/habr/configs/HKI-CORE-01.cfg:
set firewall family inet filter BOGONS term TERM005 from address 192.168.0.0/31
set firewall family inet filter BOGONS term TERM005 then accept
Запускаем тест.
Теперь нежелательный трафик не пройдет через ebgp стык AS 41214 – AS 10631. Можно смело вносить изменения, не опасаясь последствий.
Сценарий N2
Здесь мне необходимо затерминировать сеть 150.0.0.0/24 на роутере MSK-CORE-01 и обеспечить связность между точками 135.65.0.1 и 150.0.0.1
Добавляю следующие строки в тестовую конфигурацию маршрутизатора MSK-CORE-01 — tmp/habr/configs/MSK-CORE-01.cfg:
interface Loopback2
ip address 150.0.0.1 255.255.255.255
!
ip route 150.0.0.0 255.255.255.0 Null0
!
router bgp 10631
!
address-family ipv4
network 150.0.0.0 mask 255.255.255.0
!
Изменяю тестовый сценарий и запускаю проверку:
git diff HEAD~
diff --git a/batfish-robot.robot b/batfish-robot.robot
index 8d963c5..ce8cb6a 100644
--- a/batfish-robot.robot
+++ b/batfish-robot.robot
@@ -5,7 +5,7 @@ Library LibraryBatfish.py tmp/habr
${ISIS-ENABLED-LINK-DESCRIPTION} ISIS-LINK
${NODE} HKI-CORE-01
${PROTOCOL} ebgp
-${RIB-SIZE} 1
+${RIB-SIZE} 2
*** Test Cases ***
ISIS
@@ -27,3 +27,8 @@ Ping
[Documentation] Test end-to-end ICMP connectivity & show traceroute
${result}= Ping 135.65.0.1 140.0.0.1
Should Be Equal As Integers ${result} 1
+
+Ping2
+ [Documentation] Test end-to-end ICMP connectivity & show traceroute
+ ${result}= Ping 135.65.0.1 150.0.0.1
+ Should Be Equal As Integers ${result} 1
теперь я ожидаю увидеть два eBGP маршрута на роутере HKI-CORE-01, так же добавлена дополнительная проверка связности
Связности между 135.65.0.1 и 150.0.0.1 нет, к тому же на маршрутизаторе HKI-CORE-01 всего один eBGP маршрут, вместо двух.
Проверяем содержание RIB на HKI-CORE-01 при добавлении новой конфигурации на роутер MSK-CORE-01:
showroute@HKI-CORE-01# run show route table inet.0 protocol bgp
inet.0: 20 destinations, 20 routes (19 active, 0 holddown, 1 hidden)
+ = Active Route, - = Last Active, * = Both
135.65.0.0/19 *[BGP/170] 02:25:38, MED 0, localpref 100, from 172.20.20.1
AS path: I, validation-state: unverified
> to 10.0.0.4 via ge-0/0/0.0
to 10.0.0.6 via ge-0/0/1.0
140.0.0.0/24 *[BGP/170] 01:38:02, localpref 100
AS path: 10631 I, validation-state: unverified
> to 192.168.30.1 via ge-0/0/2.0
showroute@HKI-CORE-01# run show route table inet.0 protocol bgp hidden detail
inet.0: 20 destinations, 20 routes (19 active, 0 holddown, 1 hidden)
150.0.0.0/24 (1 entry, 0 announced)
BGP /-101
Next hop type: Router, Next hop index: 563
Address: 0x940f43c
Next-hop reference count: 4
Source: 192.168.30.1
Next hop: 192.168.30.1 via ge-0/0/2.0, selected
Session Id: 0x9
State: <Hidden Ext>
Local AS: 41214 Peer AS: 10631
Age: 1:42:03
Validation State: unverified
Task: BGP_10631.192.168.30.1+179
AS path: 10631 I
Localpref: 100
Router ID: 10.68.1.1
Hidden reason: rejected by import policy
Обратите внимание на политику импорта префиксов, полученных от SPB-CORE-01:
set protocols bgp group AS10631 import FROM-AS10631
set protocols bgp group AS10631 neighbor 192.168.30.1 description SPB-CORE-01
set protocols bgp group AS10631 neighbor 192.168.30.1 peer-as 10631
set policy-options policy-statement FROM-AS10631 term TERM010 from route-filter 140.0.0.0/24 exact
set policy-options policy-statement FROM-AS10631 term TERM010 then accept
set policy-options policy-statement FROM-AS10631 term DENY then reject
Не хватает правила, разрешающего 150.0.0.0/24. Добавляем его в тестовую конфигурацию и запускаем проверку:
showroute@HKI-CORE-01# show | compare
[edit policy-options policy-statement FROM-AS10631 term TERM010 from]
route-filter 140.0.0.0/24 exact { ... }
+ route-filter 150.0.0.0/24 exact;
[edit]
Отлично, связность между сетями есть, все тесты пройдены! Значить можно внести данные изменения в работу «боевой» сети.
Заключение
На мой взгляд, Batfish — это мощнейщий инструмент с огромным потенциалом. Попробуйте и убедитесь в этом сами.
Если данная тема Вам интересна — присоединяйтесь в slack чат, разработчики Batfish с удовольсвтием отвечают на любые вопросы и быстро правят баги.
Благодарю за внимание.
Ссылки
www.youtube.com/channel/UCA-OUW_3IOt9U_s60KvmJYA
media.readthedocs.org/pdf/pybatfish/latest/pybatfish.pdf
github.com/showroute/batfish-habr
Автор: Dmitry