SRv6. Часть 2. TE.
Продолжаю развлекаться с SRv6 в связке с FRR и containerlab поверх WSL — первая часть тут, если пропустили. Так вот, пошёл я значит дальше, как и обещал, теперь история про TE, хочется её поковырять немного. Собрал вот такую лабу:
По сути ничего сложного:
- есть 2 СЕ в одном VRF RED, второй VRF BLUE убрал, т.к. он тут будет только мешать, плюс каждый СЕ подключен к своей паре РЕ
- РЕ подключены ещё к двум Р, между ними собран андерлей на IS-IS, а поверх растянут BGP vpnv4, чтоб обмениваться собственно оверлейными префиксами c SRv6, ну и растянут не фулл мешем, т.к. у меня там iBGP между PE и P, а через RRы, которые поднял на P1 и P3, таким образом все PE подключаются к этим двум коробкам и уже через них обмениваются друг с другом.
- Конфиги не секрет, всё вот тут: github.com/like-a-bus/srv6-frr-lab/tree/main/te
Пошёл пробовать пинг - на прямую всё прошло, отлично!
$ sudo docker exec clab-srv6-lab2-ce1 ping -c 3 -I 10.1.0.1 10.2.0.1
PING 10.2.0.1 (10.2.0.1) from 10.1.0.1: 56 data bytes
64 bytes from 10.2.0.1: seq=0 ttl=63 time=0.175 ms
64 bytes from 10.2.0.1: seq=1 ttl=63 time=0.104 ms
64 bytes from 10.2.0.1: seq=2 ttl=63 time=0.105 ms
--- 10.2.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.104/0.128/0.175 ms
Т.е. что мы тут видим:
- с CE1 пускаем пакетик, он локально его резолвит через PE1/PE2, отправляет в один из линков
- дальше PE оборачивают в IPv6 + SRH и передают на P
- P в свою очередь просто по underlay перекладывают до указанного DST.IP - это либо PE3, либо PE4
- PE3/PE4 получает пакетик, применяет End.DT4 - снимает IPv6 заголовок и форвардит IPv4 в VRF, т.е. в сторону CE2
- CE2 получает чистый IPv4 - ICMP в одну сторону дошёл
- ну и дальше всё в обратном порядке для ICMP reply
В общем, всё работает, но мы ведь хотели посмотреть TE.
Сначала изучил, что вообще в этом плане умеет FRR и выяснилось, что если говорить про SRv6 dataplane, то динамических вариантов нет вообще, все они относятся к MPLS, поэтому остался вариант только со статиками. В целом не проблема, тут речь только про статическую запись для конкретного префикса, которым мы хотим управлять - т.е. мы можем оставить маршрут от BGP как лучший и использовать его для доставки by-default, а для ТЕ использовать статику. При учете того, что в целом маршрутами должен управлять некий контроллер, это не выглядит как большая проблема, скорее будет некрасиво в конфигурации и плюс будет вечно плодить вам дифф на фабрике, если используется какая-то другая автоматика, но тут можно тоже поискать варианты…
В общем, для лабы нас это более чем устраивает!
Давайте ещё раз повторю, как минимум чтобы уж и для себя закрепить:
- Разворнули IS-IS, он раздал SRv6 locators + адреса Loopback
- Поверх этого добра построили VPNv4 до RR (P1, P3), для обмена VPN маршрутами
- От PE1/PE2 подняли BGP-Opt.A до CE1, приняли 10.1.0.1/32 в VRF RED
- От PE3/PE4 подняли BGP-Opt.A до CE2, приняли 10.2.0.1/32 в VRF RED
Вот тут уже имеем рабочую лабу.
Ок, идём в ТЕ:
Будем пробовать вот такую топологию:
- синим выделен трафик для TE, тот который должен быть определён внутри segment list - для нас это будет ICMP request
- красным выделен трафик идущий по дефолтному пути, там по сути есть некая вариативность за счет ECMP, но т.к. я проверил, то обратно он идёт всегда так, как показано на рисунке - и для нас это ICMP reply.
Добавляем статику на PE1, где описываем точным образом путь трафика, если уж совсем грубо, считайте это как LSP с ERO
ip route 10.2.0.1/32 eth3 segments fc00:1:0:0:1::/fc00:2:0:0:1::/fc00:4:0:0:3::/fc00:13:0:0:1:: vrf RED
Проверяем и убираем балансировку на СЕ1, иначе поток может уйти на PE2, где ТЕ-маршрута нет
$ sudo docker exec clab-srv6-lab2-ce1 vtysh -c "show ip route 10.2.0.1"
Routing entry for 10.2.0.1/32
Known via "bgp", distance 20, metric 0, best
* 192.168.11.2, via eth1, weight 1
* 192.168.12.2, via eth2, weight 1
$ sudo docker exec clab-srv6-lab2-ce1 vtysh -c "conf t" \
-c "router bgp 65001" \
-c "address-family ipv4 unicast" \
-c "neighbor 192.168.11.2 weight 200"
$ sudo docker exec clab-srv6-lab2-ce1 vtysh -c "clear bgp *"
$ sudo docker exec clab-srv6-lab2-ce1 vtysh -c "show ip route 10.2.0.1"
Routing entry for 10.2.0.1/32
Known via "bgp", distance 20, metric 0, best
* 192.168.11.2, via eth1, weight 1
Достаточно сделать через weight в сторону PE1, т.к. нам важно поуправлять локально исходящим трафиком, как видите всё ок.
Пробуем!
$ sudo docker exec clab-srv6-lab2-ce1 ping -c 3 -I 10.1.0.1 10.2.0.1
PING 10.2.0.1 (10.2.0.1) from 10.1.0.1: 56 data bytes
--- 10.2.0.1 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss
$
Не идёт.
Ну не работает.
Смотрим в чем дело…
Маршрут встал с новыми сегментами?
$ sudo docker exec clab-srv6-lab2-pe1 vtysh -c "show ip route vrf RED 10.2.0.1/32"
Routing entry for 10.2.0.1/32
Known via "static", distance 1, metric 0, vrf RED, best
Last update 00:05:05 ago
Flags: Recursion Selected RR Distance
Status: Installed
* directly connected, eth3, weight 1
Да, есть, вот он.
Правда самих сегментов тут не видно, баг FRR, вывод их не показывает вообще, надо лезть в таблицу ядра:
$ sudo docker exec clab-srv6-lab2-pe1 ip route show vrf RED 10.2.0.1
10.2.0.1 nhid 65 encap seg6 mode encap segs 4 [ fc00:1:0:0:4:: fc00:2:0:0:3:: fc00:4:0:0:2:: fc00:13:0:0:1:: ] dev eth3 proto 196 metric 20
Ну раз маршрут есть, то и пакет отсюда должен выйти, давайте собирать дамп и смотреть. Снимаем на P1 сразу со всех интерфейсов:
$ sudo docker exec clab-srv6-lab2-p1 tcpdump -nni any ip6
eth1 In IP6 fc00::11 > fc00:1:0:0:3:: RT6 (segleft=4, ...)
eth1 Out ICMP6, neighbor solicitation, who has fe80::a8c1:abff:fe1d:3fd2
eth1 Out ICMP6, destination unreachable, unreachable address fc00:2:0:0:1::
Ага, вот и наш пакетик, значит PE1 отправляет, причем вон и segleft=4, но есть нюанс…, что это за ND???
P1 получает его от PE1 и дальше пытается адрес P2 отрезолвить через интерфейс в сторону PE1!!, при этом P2 у него уже есть в ND таблице!
$ sudo docker exec clab-srv6-lab2-p1 ip -6 neigh
fe80::a8c1:abff:fe1d:3fd2 dev eth1 FAILED
fe80::a8c1:abff:fe1d:3fd2 dev eth4 lladdr aa:c1:ab:1d:3f:d2 router STALE
вот, видите, да?
но он сука есть УЖЕ! ШЛИ ТУДА ПАКЕТ!
короче, вот тут я ещё посидел часов много, в итоге выяснил следующее:
- это известный баг ещё обнаружился в 2020 году (https://lkml.iu.edu/hypermail/linux/kernel/2010.1/04199.html)
- в том же 2020 году его обещали починить и добавить в поддержку ядра дополнительный параметр oif, явно указывающий на исходящий интерфейс, чтобы не приходилось выполнять lookup по link-local (https://lkml.iu.edu/hypermail/linux/kernel/2010.2/04443.html)
- увы, что-то пошло не так, фикс приехал только в 2025 - через ветку net-next попал в мастер и вышел в релизе 6.17 (https://kernelnewbies.org/Linux_6.17)
и снова я оказался на развилке - искать какой-то костыль и к слову он нашёлся через статику на ULA и без использования link-local, либо обновлять ядро и верить, что всё заработает…
Конечно же я пошёл вторым путём, тут уже речь не про статический TE, который в любом случае приходится описывать руками, а про статический IPv6 роут на каждом транзитном узле, для каждого из имеющихся интерфейсов, который ещё и менять нужно, если хочется чтобы трафик пошёл в другую сторону. Вообще не масштабируемая и не управляемая история.
Мы уже умеем пересобирать ядра для WSL, самое время применить навык ещё разок.
Теперь пересобираем на 6.17+, а точнее на 6.18 как самое актуальное для WSL в этой ветке.
Инструкция была в первой части, разница тут лишь в том, что берём ветку linux-msft-wsl-6.18.y.
Ок, собрали:
$ uname -r
6.18.40.1-microsoft-standard-WSL2+
Перезапускаем нашу лабу и сразу лезем в таблицу ядра на P1:
$ sudo docker exec clab-srv6-lab2-p1 ip -6 route show | grep seg6local
fc00:1:0:0:1:: encap seg6local action End.X nh6 fe80::a8c1:abff:fe7f:b568 oif eth1 dev eth1 proto isis
fc00:1:0:0:2:: encap seg6local action End.X nh6 fe80::a8c1:abff:fec4:3bc2 oif eth3 dev eth3 proto isis
fc00:1:0:0:3:: encap seg6local action End.X nh6 fe80::a8c1:abff:fe56:19ab oif eth2 dev eth2 proto isis
fc00:1:0:0:4:: encap seg6local action End.X nh6 fe80::a8c1:abff:fec6:36a oif eth4 dev eth4 proto isis
Опа, а вот и oif - тот самый атрибут, которого до 6.17 в ядре не было. К слову, FRR его ставил всегда,
а ядро молча выбрасывало при добавлении, потому что понятия о нем не имело.
В общем, в 6.18 починено! Параметр есть.
Добавляем наш ТЕ префикс на PE1:
$ sudo docker exec clab-srv6-lab2-pe1 vtysh -c "conf t" \
-c "ip route 10.2.0.1/32 eth3 segments fc00:1:0:0:4::/fc00:2:0:0:3::/fc00:4:0:0:2::/fc00:13:0:0:1:: vrf RED"
И проверяем, что он лёг в ядро вместе с сегментами:
$ sudo docker exec clab-srv6-lab2-pe1 ip route show vrf RED 10.2.0.1
10.2.0.1 nhid 65 encap seg6 mode encap segs 4 [ fc00:1:0:0:4:: fc00:2:0:0:3:: fc00:4:0:0:2:: fc00:13:0:0:1:: ] dev eth3 proto 196 metric 20
Пробуем!
$ sudo docker exec clab-srv6-lab2-ce1 ping -c 5 -I 10.1.0.1 10.2.0.1
PING 10.2.0.1 (10.2.0.1) from 10.1.0.1: 56 data bytes
64 bytes from 10.2.0.1: seq=0 ttl=63 time=0.149 ms
64 bytes from 10.2.0.1: seq=1 ttl=63 time=0.126 ms
64 bytes from 10.2.0.1: seq=2 ttl=63 time=0.152 ms
64 bytes from 10.2.0.1: seq=3 ttl=63 time=0.142 ms
64 bytes from 10.2.0.1: seq=4 ttl=63 time=0.121 ms
--- 10.2.0.1 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.121/0.138/0.152 ms
БАБАХ! ПОШЛО!
Ух, уровень эндорфинов приближается к пиковому, но не время слишком сильно радоваться, надо ведь ещё убедиться в дампе, как оно там!
Собираем картину на всем пути трафика:
$ cd ~/srv6-frr-lab/te
$ bash capture.sh
Скрипт снимает трафик на каждом интерфейсе каждого узла в отдельный pcap,
прогоняет пинг и печатает сводку - кто что видел. Файлы кладёт в te/pcap/.
Смотрим что внутри, request должен идти только по пути CE1-PE1-P1-P2-P4-P3-PE3-CE2, а reply вернётся обычным маршрутом, без всякого TE - т.е. чуть короче и через другие узлы. Чуть выше была картинка.
Дальше следим за двумя вещами: за segleft и за тем, на каком интерфейсе пакет
вошёл и на каком вышел. Дампы привожу по одному пакету, чтоб не было простыни.
PE1 получает чистый IPv4 из VRF и заворачивает его в IPv6:
$ tcpdump -nr pcap/pe1-eth3.pcap
IP 10.1.0.1 > 10.2.0.1: ICMP echo request, id 192, seq 0, length 64
$ tcpdump -nr pcap/pe1-eth1.pcap
IP6 fc00::11 > fc00:1:0:0:4:: RT6 (segleft=3, last-entry=3,
[0]fc00:13:0:0:1::, [1]fc00:4:0:0:2::, [2]fc00:2:0:0:3::, [3]fc00:1:0:0:4::)
IP 10.1.0.1 > 10.2.0.1: ICMP echo request, seq 0
P1 - вошёл в eth1, вышел в eth4, segleft 3 → 2:
$ tcpdump -nr pcap/p1-eth1.pcap
IP6 fc00::11 > fc00:1:0:0:4:: RT6 (segleft=3, ...)
$ tcpdump -nr pcap/p1-eth4.pcap
IP6 fc00::11 > fc00:2:0:0:3:: RT6 (segleft=2, ...)
P2 - вошёл в eth4, вышел в eth3, segleft 2 → 1:
$ tcpdump -nr pcap/p2-eth4.pcap
IP6 fc00::11 > fc00:2:0:0:3:: RT6 (segleft=2, ...)
$ tcpdump -nr pcap/p2-eth3.pcap
IP6 fc00::11 > fc00:4:0:0:2:: RT6 (segleft=1, ...)
P4 - вошёл в eth3, вышел в eth4, segleft 1 → 0. Сегменты кончились, в DA встал последний - DT4-сид на PE3:
$ tcpdump -nr pcap/p4-eth3.pcap
IP6 fc00::11 > fc00:4:0:0:2:: RT6 (segleft=1, ...)
$ tcpdump -nr pcap/p4-eth4.pcap
IP6 fc00::11 > fc00:13:0:0:1:: RT6 (segleft=0, ...)
P3 - вошёл в eth4, вышел в eth1. Сегментов тут больше не было, так что на входе и выходе одно и то же:
$ tcpdump -nr pcap/p3-eth4.pcap
IP6 fc00::11 > fc00:13:0:0:1:: RT6 (segleft=0, ...)
$ tcpdump -nr pcap/p3-eth1.pcap
IP6 fc00::11 > fc00:13:0:0:1:: RT6 (segleft=0, ...)
PE3 применяет End.DT4, снимает IPv6 и отдаёт чистый IPv4 в VRF:
$ tcpdump -nr pcap/pe3-eth1.pcap
IP6 fc00::11 > fc00:13:0:0:1:: RT6 (segleft=0, ...)
$ tcpdump -nr pcap/pe3-RED.pcap
IP 10.1.0.1 > 10.2.0.1: ICMP echo request, seq 0
CE2 получает обычный пинг:
$ tcpdump -nr pcap/ce2-eth1.pcap
IP 10.1.0.1 > 10.2.0.1: ICMP echo request, seq 0
Итого: CE1 → PE1 → P1 → P2 → P4 → P3 → PE3 → CE2, тот путь, который мы описали сегментами. Всё чётко.
Теперь идём в обратную сторону и ищем echo reply.
PE3 отправляет ответ всего с ОДНИМ сегментом, т.к. у него нет никакого TE, и он просто по ECMP решил закинуть в сторону PE2, соотвественно и подставил его DT4-сид в DA (fc00:12:0:0:1)
$ tcpdump -nr pcap/pe3-eth2.pcap
IP6 fc00::13 > fc00:12:0:0:1:: RT6 (segleft=0, last-entry=0, [0]fc00:12:0:0:1::)
IP 10.2.0.1 > 10.1.0.1: ICMP echo reply, seq 0
Дальше пакет просто проходит по underlay:
$ tcpdump -nr pcap/p4-eth1.pcap # вошёл от PE3
IP6 fc00::13 > fc00:12:0:0:1:: RT6 (segleft=0, ...)
$ tcpdump -nr pcap/p4-eth3.pcap # вышел на P2
IP6 fc00::13 > fc00:12:0:0:1:: RT6 (segleft=0, ...)
$ tcpdump -nr pcap/p2-eth3.pcap # вошёл от P4
IP6 fc00::13 > fc00:12:0:0:1:: RT6 (segleft=0, ...)
$ tcpdump -nr pcap/p2-eth2.pcap # вышел на PE2
IP6 fc00::13 > fc00:12:0:0:1:: RT6 (segleft=0, ...)
PE2 снимает заголовок и отдаёт в VRF, дальше на CE1:
$ tcpdump -nr pcap/pe2-eth2.pcap
IP6 fc00::13 > fc00:12:0:0:1:: RT6 (segleft=0, ...)
$ tcpdump -nr pcap/pe2-RED.pcap
IP 10.2.0.1 > 10.1.0.1: ICMP echo reply, seq 0
Обратный путь: CE2 → PE3 → P4 → P2 → PE2 → CE1.
Идеально, давненько такого не было, Серёжа в инженерно-дофаминовой коме, добрый вечер.
Пишу это всё в свободное время. Если статья пригодилась — можно закинуть на пиво. Или просто подписаться на канал.