
Одной из проблем современных сетей является их хрупкость. Множество правил фильтраций, политик обмена маршрутной информации, протоколов динамического роутинга делают сети запутанными и подверженными влиянию человеческого фактора. Авария на сети может произойти ненамеренно при внесении изменений в 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 на роутере для определенного протокола динамической маршрутизации
LibraryBatfish.py
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
batfish-test.robot

Сценарий 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 с удовольсвтием отвечают на любые вопросы и быстро правят баги.
batfish-org.slack.com
Благодарю за внимание.
Ссылки
www.batfish.org
www.youtube.com/channel/UCA-OUW_3IOt9U_s60KvmJYA
github.com/batfish/batfish
media.readthedocs.org/pdf/pybatfish/latest/pybatfish.pdf
github.com/showroute/batfish-habr
