#!/bin/bash # Генератор OpenFlow-пайплайна br-lb. Печатает flow-файл в stdout. # Применяется атомарным бандлом: ovs-ofctl --bundle -O OpenFlow15 replace-flows. # # Карта таблиц: # 0 — определение сегмента (reg0) по in_port, обучение MAC и adjacency # 1 — классификация: ARP, ICMP, листенер, L3-трафик к шлюзу, коммутация # 5 — ARP-респондер для собственных адресов узла и VIP, затем коммутация # 6 — ICMP echo-респондер для тех же адресов # 10 — листенеры: хэш сессии в слот (multipath), прочий IP -> маршрутизация # 11 — таблица слотов: слот -> член пула. Заливает и обновляет демон hcd # 12 — применение выбранного члена: DNAT + выдача (L2 либо маршрутизация) # 15 — обратный путь L3 из приватных сегментов (снятие DNAT через ct) # 16 — пост-ct hook (счётчики, место под будущие проверки) # 20 — маршрутизация (LPM через приоритет = длина префикса) # 21 — adjacency: next-hop MAC + выходной порт # 24 — обратная трансляция коммутируемого трафика сегмента (шаг 3) # 25 — L2-коммутация сегмента: FDB, broadcast и unknown flood (шаг 3) # # Регистры: reg0 — сегмент (1 — публичный, 2 и 3 — приватные), reg1 — номер # слота, reg2 — идентификатор члена пула. # # Нумерация не произвольна: goto_table в OpenFlow разрешает переход только # вперёд, поэтому обратный путь (15/16) стоит до общей маршрутизации (20), а # коммутация сегмента (24/25) — после неё: в неё попадают и кадры, прошедшие # DNAT в таблице 12. # # Ключевое отличие шага 3: приватный сегмент коммутирует сам br-lb, каждая ВМ # подключена отдельным портом. Поэтому ответ бэкенда клиенту-соседу по подсети # проходит через таблицу 24, где ct снимает DNAT, — при этом ни в ОС ВМ, ни в # адресации сегмента ничего не меняется, а внутрисегментный трафик не # блокируется. set -euo pipefail # shellcheck disable=SC1091 source "$(dirname "$0")/topology.env" ip2hex() { local IFS=.; read -ra o <<<"$1"; printf '0x%02x%02x%02x%02x' "${o[0]}" "${o[1]}" "${o[2]}" "${o[3]}"; } mac2hex() { echo "0x${1//:/}"; } PUB_MAC_H=$(mac2hex "$PUB_MAC") P2_MAC_H=$(mac2hex "$P2_MAC") P3_MAC_H=$(mac2hex "$P3_MAC") LB_PUB_IP_H=$(ip2hex "$LB_PUB_IP") VIP_H=$(ip2hex "$VIP") LB_P2_IP_H=$(ip2hex "$LB_P2_IP") LB_P3_IP_H=$(ip2hex "$LB_P3_IP") P2_VIP_H=$(ip2hex "$P2_VIP") P3_VIP_H=$(ip2hex "$P3_VIP") HC2_MAC_H=$(mac2hex "$HC2_MAC") HC3_MAC_H=$(mac2hex "$HC3_MAC") HC2_IP_H=$(ip2hex "$HC2_IP") HC3_IP_H=$(ip2hex "$HC3_IP") # --- описание сегментов ------------------------------------------------------ # Производные величины: номер сегмента в reg0, состав портов и члены пула. P2_REG=0x2 P3_REG=0x3 P2_PORTS="$P2_OFPORT $HC2_OFPORT $BE1_OFPORT $BE3_OFPORT $CLI2_OFPORT" P3_PORTS="$P3_OFPORT $HC3_OFPORT $BE2_OFPORT $BE4_OFPORT $CLI3_OFPORT" P2_MEMBERS="BE1 BE3" P3_MEMBERS="BE2 BE4" # flood_ports <список ofport> — действие рассылки по сегменту. Порт входа OVS # исключает сам, поэтому отдельного «кроме in_port» не требуется. flood_ports() { local out="" p for p in $1; do out="$out,output:$p"; done echo "${out#,}" } # Включён ли приватный листенер в сегменте. listener_on() { local seg="$1" s for s in $PRIV_LISTENERS; do [ "$s" = "$seg" ] && return 0; done return 1 } # Действие обучения FDB: eth_src -> порт. Заполняет таблицу 25, то есть делает # из br-lb обычный обучающийся коммутатор в пределах сегмента. l2_learn() { echo "learn(table=25,priority=100,idle_timeout=300,NXM_NX_REG0[]=$1,NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],output:NXM_OF_IN_PORT[])" } # Действие обучения adjacency: по IP-адресу отправителя создаёт в таблице 21 # запись «куда и с каким MAC отправлять ответы этому адресу». Заменяет # ARP-резолвер, которого у чисто-OpenFlow узла нет. Нужна маршрутизируемому # трафику: ответ бэкенда из соседнего сегмента приходит к клиенту через L3. l3_learn() { echo "learn(table=21,priority=90,hard_timeout=300,eth_type=0x0800,NXM_OF_IP_DST[]=NXM_OF_IP_SRC[],load:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],load:$1->NXM_OF_ETH_SRC[],output:NXM_OF_IN_PORT[])" } LEARN_PUB=$(l3_learn "$PUB_MAC_H") # arp_responder arp_responder() { echo "table=5,priority=100,reg0=$1,arp,arp_op=1,arp_tpa=$2 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],mod_dl_src:$4,load:0x2->NXM_OF_ARP_OP[],move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[],move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[],load:$5->NXM_NX_ARP_SHA[],load:$3->NXM_OF_ARP_SPA[],IN_PORT" } # icmp_responder icmp_responder() { echo "table=6,priority=100,icmp,nw_dst=$1,icmp_type=8 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],load:$3->NXM_OF_ETH_SRC[],move:NXM_OF_IP_SRC[]->NXM_OF_IP_DST[],load:$2->NXM_OF_IP_SRC[],load:0->NXM_OF_ICMP_TYPE[],IN_PORT" } # segment_ingress <сеть> <порты> — таблица 0 для сегмента. # Обучение FDB идёт для любого кадра, обучение adjacency — только для адресов # самого сегмента: иначе с аплинка в таблицу 21 попал бы весь внешний мир. segment_ingress() { local reg="$1" gwmac_h="$2" net="$3" ports="$4" p local l2 l3 l2=$(l2_learn "$reg") l3=$(l3_learn "$gwmac_h") for p in $ports; do echo "table=0,priority=200,in_port=$p,ip,nw_src=$net actions=load:$reg->NXM_NX_REG0[],$l2,$l3,goto_table:1" echo "table=0,priority=190,in_port=$p actions=load:$reg->NXM_NX_REG0[],$l2,goto_table:1" done } # segment_classify <сегмент> — таблица 1 для сегмента. segment_classify() { local seg="$1" reg="$2" gwmac="$3" vip="$4" echo "# --- сегмент $seg ---" if listener_on "$seg"; then echo "table=1,priority=130,reg0=$reg,dl_dst=$gwmac,tcp,nw_dst=$vip,tp_dst=$PRIV_VIP_PORT actions=goto_table:10" fi # Трафик на VIP, для которого листенера нет, отбрасывается со счётчиком — # так видно, что адрес занят стендом, а листенер выключен. echo "table=1,priority=125,reg0=$reg,dl_dst=$gwmac,ip,nw_dst=$vip actions=drop" # Кадр адресован маршрутизатору сегмента: транзит, ответы балансируемых # сессий из другого сегмента, обращения наружу через LB. echo "table=1,priority=120,reg0=$reg,dl_dst=$gwmac,ip actions=goto_table:15" # Всё остальное — трафик между ВМ сегмента: коммутация, при необходимости # с обратной трансляцией. echo "table=1,priority=100,reg0=$reg actions=goto_table:24" } # segment_untranslate <члены> — таблица 24. segment_untranslate() { local reg="$1" m ip port for m in $2; do eval "ip=\$${m}_IP; port=\$${m}_PORT" echo "table=24,priority=100,reg0=$reg,tcp,nw_src=$ip,tp_src=$port actions=ct(table=25,zone=$CT_ZONE,nat)" done } cat <NXM_NX_REG0[],goto_table:1 # Приватные сегменты: каждая ВМ — отдельный порт, br-lb работает коммутатором. $(segment_ingress "$P2_REG" "$P2_MAC_H" "$P2_NET" "$P2_PORTS") $(segment_ingress "$P3_REG" "$P3_MAC_H" "$P3_NET" "$P3_PORTS") table=0,priority=0 actions=drop # ============================================================================= # Таблица 1 — классификация # ============================================================================= # ARP обрабатывает собственный респондер (таблица 5): ядро в форвардинге не # участвует, поэтому штатного ARP-стека у узла нет. table=1,priority=200,arp actions=goto_table:5 # ICMP echo-request на собственные адреса узла и на VIP — в респондер. table=1,priority=190,icmp,nw_dst=$LB_PUB_IP,icmp_type=8 actions=goto_table:6 table=1,priority=190,icmp,nw_dst=$VIP,icmp_type=8 actions=goto_table:6 table=1,priority=190,icmp,nw_dst=$LB_P2_IP,icmp_type=8 actions=goto_table:6 table=1,priority=190,icmp,nw_dst=$LB_P3_IP,icmp_type=8 actions=goto_table:6 table=1,priority=190,icmp,nw_dst=$P2_VIP,icmp_type=8 actions=goto_table:6 table=1,priority=190,icmp,nw_dst=$P3_VIP,icmp_type=8 actions=goto_table:6 # Вход из публичного сегмента: запоминаем MAC отправителя как adjacency для # обратного пути, затем к листенерам. Записи живут 300 с и обновляются каждым # пакетом клиента. # # Обучение включено только для трафика, адресованного стенду (VIP, адрес узла, # приватные сегменты). Иначе в таблицу 21 попадал бы весь широковещательный # шум LAN — macvlan-порт видит DHCP, mDNS и SSDP всех соседей по сегменту. table=1,priority=110,reg0=0x1,ip,nw_dst=$VIP actions=$LEARN_PUB,goto_table:10 table=1,priority=110,reg0=0x1,ip,nw_dst=$LB_PUB_IP actions=$LEARN_PUB,goto_table:10 table=1,priority=110,reg0=0x1,ip,nw_dst=$P2_NET actions=$LEARN_PUB,goto_table:10 table=1,priority=110,reg0=0x1,ip,nw_dst=$P3_NET actions=$LEARN_PUB,goto_table:10 table=1,priority=100,reg0=0x1,ip actions=drop $(segment_classify p2 "$P2_REG" "$P2_MAC" "$P2_VIP") $(segment_classify p3 "$P3_REG" "$P3_MAC" "$P3_VIP") # Всё остальное (не-IP, не-ARP с публичного порта) в прототипе не нужно. table=1,priority=0 actions=drop # ============================================================================= # Таблица 5 — ARP-респондер # ============================================================================= $(arp_responder 0x1 "$LB_PUB_IP" "$LB_PUB_IP_H" "$PUB_MAC" "$PUB_MAC_H") $(arp_responder 0x1 "$VIP" "$VIP_H" "$PUB_MAC" "$PUB_MAC_H") $(arp_responder "$P2_REG" "$LB_P2_IP" "$LB_P2_IP_H" "$P2_MAC" "$P2_MAC_H") $(arp_responder "$P3_REG" "$LB_P3_IP" "$LB_P3_IP_H" "$P3_MAC" "$P3_MAC_H") # VIP приватных листенеров отвечают тем же MAC, что и шлюз сегмента: по нему # таблица 1 отличает трафик к маршрутизатору от трафика между ВМ. $(arp_responder "$P2_REG" "$P2_VIP" "$P2_VIP_H" "$P2_MAC" "$P2_MAC_H") $(arp_responder "$P3_REG" "$P3_VIP" "$P3_VIP_H" "$P3_MAC" "$P3_MAC_H") # Адреса источника health-проб: бэкенд резолвит их, отвечая на пробу. $(arp_responder "$P2_REG" "$HC2_IP" "$HC2_IP_H" "$HC2_MAC" "$HC2_MAC_H") $(arp_responder "$P3_REG" "$HC3_IP" "$HC3_IP_H" "$HC3_MAC" "$HC3_MAC_H") # Остальной ARP сегмента — обычная коммутация: ВМ резолвят друг друга сами, # балансировщик в это не вмешивается. table=5,priority=10,reg0=$P2_REG actions=goto_table:25 table=5,priority=10,reg0=$P3_REG actions=goto_table:25 table=5,priority=0 actions=drop # ============================================================================= # Таблица 6 — ICMP echo-респондер # ============================================================================= $(icmp_responder "$LB_PUB_IP" "$LB_PUB_IP_H" "$PUB_MAC_H") $(icmp_responder "$VIP" "$VIP_H" "$PUB_MAC_H") $(icmp_responder "$LB_P2_IP" "$LB_P2_IP_H" "$P2_MAC_H") $(icmp_responder "$LB_P3_IP" "$LB_P3_IP_H" "$P3_MAC_H") $(icmp_responder "$P2_VIP" "$P2_VIP_H" "$P2_MAC_H") $(icmp_responder "$P3_VIP" "$P3_VIP_H" "$P3_MAC_H") table=6,priority=0 actions=drop # ============================================================================= # Таблица 10 — листенеры # ============================================================================= # multipath раскладывает сессию в один из $SLOTS слотов по симметричному # L4-хэшу; basis = POOL_ID, поэтому разные пулы дают независимые раскладки. # Хэш симметричен, то есть прямое и обратное направление дают один слот — # свойство, необходимое для будущего stateless-датапаса. # # Публичный листенер: TCP $VIP:$VIP_PORT. table=10,priority=200,reg0=0x1,tcp,nw_dst=$VIP,tp_dst=$VIP_PORT actions=multipath(symmetric_l4,$POOL_ID,modulo_n,$SLOTS,0,NXM_NX_REG1[]),goto_table:11 # Трафик на VIP, для которого листенера нет, — отбрасываем со счётчиком. table=10,priority=150,reg0=0x1,ip,nw_dst=$VIP actions=drop # Прочий IP из публичного сегмента маршрутизируется обычным образом # (клиент может обратиться напрямую к бэкенду, минуя балансировку). table=10,priority=100,reg0=0x1,ip actions=goto_table:20 EOF # Приватные листенеры: тот же пул, та же таблица слотов, тот же хэш. Отличие # только в адресе и в том, что выдача пойдёт коммутацией (таблица 12). for seg in p2 p3; do listener_on "$seg" || continue if [ "$seg" = p2 ]; then reg=$P2_REG; vip=$P2_VIP; else reg=$P3_REG; vip=$P3_VIP; fi echo "table=10,priority=200,reg0=$reg,tcp,nw_dst=$vip,tp_dst=$PRIV_VIP_PORT actions=multipath(symmetric_l4,$POOL_ID,modulo_n,$SLOTS,0,NXM_NX_REG1[]),goto_table:11" done cat < -> reg2=<член пула>) рассчитывает # и заливает демон hcd по Maglev-раскладке живых членов пула. Здесь только # основание fail-close: пока пул пуст, трафик на VIP отбрасывается со # счётчиком, а не уходит на заведомо мёртвый бэкенд. # # Счётчики правил этой таблицы дают бесплатную per-member статистику. table=11,priority=0 actions=drop # ============================================================================= # Таблица 12 — применение выбранного члена пула # ============================================================================= # Слот положил идентификатор члена в reg2. SNAT не выполняется ни в одном из # путей: бэкенд видит реальный адрес клиента. # # Член в том же сегменте, что и клиент (приоритет 200) — выдача коммутацией: # меняется только MAC назначения, TTL не уменьшается, маршрутизации нет. Для # ВМ клиент и бэкенд остаются L2-соседями, какими и были. EOF for seg_reg in "$P2_REG:$P2_MEMBERS" "$P3_REG:$P3_MEMBERS"; do reg="${seg_reg%%:*}" for m in ${seg_reg#*:}; do eval "mip=\$${m}_IP; mport=\$${m}_PORT; mmac=\$${m}_MAC; mid=\$${m}_ID" echo "table=12,priority=200,reg0=$reg,ip,reg2=$mid actions=mod_dl_dst:$mmac,ct(commit,zone=$CT_ZONE,nat(dst=$mip:$mport),table=25)" done done cat <be, ответы на # health-пробы, прямые обращения клиента к бэкенду. $(segment_untranslate "$P2_REG" "$P2_MEMBERS") $(segment_untranslate "$P3_REG" "$P3_MEMBERS") table=24,priority=0 actions=goto_table:25 # ============================================================================= # Таблица 25 — L2-коммутация сегмента # ============================================================================= # priority=100 — записи FDB, устанавливаемые действием learn из таблицы 0. # Ниже — рассылка по сегменту: broadcast и multicast отдельным правилом ради # счётчика, неизвестный unicast — тем же способом. Порт входа OVS исключает # из рассылки сам. table=25,priority=50,reg0=$P2_REG,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=$(flood_ports "$P2_PORTS") table=25,priority=50,reg0=$P3_REG,dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=$(flood_ports "$P3_PORTS") table=25,priority=10,reg0=$P2_REG actions=$(flood_ports "$P2_PORTS") table=25,priority=10,reg0=$P3_REG actions=$(flood_ports "$P3_PORTS") table=25,priority=0 actions=drop EOF