MVP Single Node with Manual Config in Docker

This commit is contained in:
ayurishchev committed 2026-08-16 21:43:02 +03:00
commit 8212699f23
27 files changed
+3818

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
# Контекст сборки — корень репозитория: в образ едет и демон health-check.
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY hc/go.mod ./
COPY hc/*.go ./
RUN CGO_ENABLED=0 go build -trimpath -o /out/hcd .
FROM debian:bookworm-slim
# openvswitch-switch — ovsdb-server и ovs-vswitchd; kmod — modprobe openvswitch;
# curl — обращения к API демона из lbctl; остальное — диагностика стенда.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
openvswitch-switch \
openvswitch-common \
iproute2 \
kmod \
tcpdump \
iputils-ping \
conntrack \
curl \
procps \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /out/hcd /usr/local/bin/hcd
COPY lb/topology.env lb/pipeline.sh lb/apply.sh lb/entrypoint.sh lb/lbctl.sh lb/hc-config.sh /opt/lb/
RUN chmod +x /opt/lb/*.sh && ln -s /opt/lb/lbctl.sh /usr/local/bin/lbctl
HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=3 \
CMD ovs-ofctl -O OpenFlow15 dump-flows br-lb table=11 >/dev/null 2>&1 \
&& curl -fsS --max-time 3 http://127.0.0.1:9111/status >/dev/null || exit 1
ENTRYPOINT ["/opt/lb/entrypoint.sh"]
Executable
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# Генерация и применение OpenFlow-пайплайна.
# Вызывается из entrypoint.sh при старте и вручную (make flows) при правках.
#
# apply.sh применить пайплайн и попросить hcd перезалить слоты
# apply.sh --no-reapply только пайплайн (демон ещё не запущен)
set -euo pipefail
LB_DIR=/opt/lb
# shellcheck disable=SC1091
source "$LB_DIR/topology.env"
FLOWS=/var/run/openvswitch/flows.txt
# Группа балансировки шага 1 больше не используется: её место заняла таблица
# слотов (таблица 11), которую ведёт демон hcd.
ovs-ofctl -O "$OF" del-groups "$BR" 2>/dev/null || true
# Правила применяются атомарным бандлом: датапас не проходит через
# промежуточное состояние с частично залитым пайплайном.
"$LB_DIR/pipeline.sh" > "$FLOWS"
ovs-ofctl -O "$OF" --bundle replace-flows "$BR" "$FLOWS"
echo "[lb-router] пайплайн применён: $(grep -cv '^\s*\(#\|$\)' "$FLOWS") правил"
# replace-flows стирает и таблицу слотов, поэтому демона просят залить её
# заново — в актуальном составе пула, а не в полном.
if [ "${1:-}" != "--no-reapply" ]; then
if out=$(curl -fsS --max-time 5 "http://$HC_API/reapply" 2>&1); then
echo "[lb-router] $out"
else
echo "[lb-router] hcd недоступен ($out) — таблица слотов пуста, датапас в fail-close" >&2
fi
fi
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
# Подъём Open vSwitch внутри контейнера и сборка моста br-lb.
#
# Контейнер работает как чистый OpenFlow-роутер: IP-адреса на интерфейсах не
# настраиваются, сетевой стек ядра в форвардинге не участвует. Вся L3-логика
# (ARP, маршрутизация, DNAT) живёт в таблицах OpenFlow — см. pipeline.sh.
set -euo pipefail
LB_DIR=/opt/lb
# shellcheck disable=SC1091
source "$LB_DIR/topology.env"
log() { echo "[lb-router] $*"; }
die() { echo "[lb-router] ОШИБКА: $*" >&2; exit 1; }
# --- 1. Kernel datapath ------------------------------------------------------
log "загрузка модуля openvswitch"
modprobe openvswitch 2>/dev/null || true
grep -q '^openvswitch ' /proc/modules || die \
"модуль openvswitch не загружен. Выполните на хосте: modprobe openvswitch (см. scripts/host-prereq.sh)"
# --- 2. Запуск ovsdb-server и ovs-vswitchd -----------------------------------
mkdir -p /var/run/openvswitch /var/log/openvswitch /etc/openvswitch
rm -f /var/run/openvswitch/*.pid
if [ ! -f /etc/openvswitch/conf.db ]; then
log "инициализация OVSDB"
ovsdb-tool create /etc/openvswitch/conf.db /usr/share/openvswitch/vswitch.ovsschema
fi
log "запуск ovsdb-server"
ovsdb-server /etc/openvswitch/conf.db \
--remote=punix:/var/run/openvswitch/db.sock \
--remote=db:Open_vSwitch,Open_vSwitch,manager_options \
--pidfile --detach --log-file
ovs-vsctl --no-wait init
log "запуск ovs-vswitchd"
ovs-vswitchd --pidfile --detach --log-file
# --- 3. Мост -----------------------------------------------------------------
log "создание моста $BR"
ovs-vsctl --may-exist add-br "$BR" \
-- set bridge "$BR" fail_mode=secure \
-- set bridge "$BR" protocols=OpenFlow13,OpenFlow15 \
-- set bridge "$BR" other-config:disable-in-band=true
ip link set dev "$BR" up
# --- 4. Перенос интерфейсов Docker в мост ------------------------------------
# Интерфейсы опознаются по MAC (имена eth0/eth1/eth2 Docker раздаёт в
# непредсказуемом порядке), переименовываются в осмысленные и лишаются IP:
# адрес нужен только Docker IPAM, чтобы зарезервировать его за контейнером.
find_dev_by_mac() {
local mac="$1" d
for d in /sys/class/net/*; do
[ -f "$d/address" ] || continue
[ "$(cat "$d/address")" = "$mac" ] && { basename "$d"; return 0; }
done
return 1
}
attach_port() {
local mac="$1" name="$2" ofport="$3" dev
dev=$(find_dev_by_mac "$mac") || die "интерфейс с MAC $mac не найден"
if [ "$dev" != "$name" ]; then
ip link set dev "$dev" down
ip addr flush dev "$dev"
ip link set dev "$dev" name "$name"
else
ip addr flush dev "$name"
fi
ip link set dev "$name" up
ovs-vsctl --may-exist add-port "$BR" "$name" \
-- set Interface "$name" ofport_request="$ofport"
log "порт $name (MAC $mac) -> $BR, ofport $ofport"
}
attach_port "$PUB_MAC" "$PUB_IFNAME" "$PUB_OFPORT"
attach_port "$P2_MAC" "$P2_IFNAME" "$P2_OFPORT"
attach_port "$P3_MAC" "$P3_IFNAME" "$P3_OFPORT"
# --- 4a. Порты-источники health-проб -----------------------------------------
# Единственные адреса, которые узел держит в ядре: с них уходят пробы (§7.1
# дизайна v1 — источник проб должен быть уникальным адресом узла, не VIP).
# MAC бэкенда прописывается статически: ARP-резолвер узлу не нужен, MAC членов
# пула и так зафиксированы в docker-compose.yml.
hc_port() {
local name="$1" ofport="$2" mac="$3" ip="$4" net="$5"
shift 5
ovs-vsctl --may-exist add-port "$BR" "$name" \
-- set Interface "$name" type=internal ofport_request="$ofport" \
-- set Interface "$name" mac="\"$mac\""
ip link set dev "$name" address "$mac"
ip addr replace "$ip/${net##*/}" dev "$name"
ip link set dev "$name" up
# Остальные аргументы — пары «адрес MAC» членов пула в этом сегменте.
local peers=""
while [ $# -ge 2 ]; do
ip neigh replace "$1" lladdr "$2" dev "$name" nud permanent
peers="$peers $1"
shift 2
done
log "порт $name ($ip) — источник health-проб для$peers"
}
hc_port "$HC2_IFNAME" "$HC2_OFPORT" "$HC2_MAC" "$HC2_IP" "$P2_NET" \
"$BE1_IP" "$BE1_MAC" "$BE3_IP" "$BE3_MAC"
hc_port "$HC3_IFNAME" "$HC3_OFPORT" "$HC3_MAC" "$HC3_IP" "$P3_NET" \
"$BE2_IP" "$BE2_MAC" "$BE4_IP" "$BE4_MAC"
# Ждём, пока vswitchd реально привяжет порты к датапасу.
for name in "$PUB_IFNAME" "$P2_IFNAME" "$P3_IFNAME"; do
for _ in $(seq 30); do
ofport=$(ovs-vsctl --if-exists get Interface "$name" ofport || echo -1)
[ "$ofport" -gt 0 ] 2>/dev/null && break
sleep 0.5
done
[ "${ofport:-0}" -gt 0 ] 2>/dev/null || die "порт $name не привязался к датапасу: $(ovs-vsctl get Interface "$name" error 2>/dev/null || true)"
done
# --- 5. Пайплайн -------------------------------------------------------------
# Таблица слотов здесь пуста (fail-close) — её наполнит hcd после первых
# успешных проб, поэтому apply.sh на этом этапе демона ещё не дёргает.
"$LB_DIR/apply.sh" --no-reapply
log "готово. Порты: $(ovs-vsctl list-ports "$BR" | tr '\n' ' ')"
# --- 6. Health-check ---------------------------------------------------------
# Демон становится основным процессом контейнера; ovsdb-server и ovs-vswitchd
# работают демонами рядом. Логи проб и переходов видны в docker logs.
CONF=$("$LB_DIR/hc-config.sh")
log "конфигурация health-check: $CONF"
tail -F --pid=$$ /var/log/openvswitch/ovs-vswitchd.log 2>/dev/null &
exec hcd -config "$CONF"
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# Генерация конфигурации демона hcd из topology.env — единственного источника
# правды по адресам стенда. Пока пул задан статически; на следующем шаге его
# место займёт модель Load_Balancer -> Listener -> Pool -> Member в OVSDB.
set -euo pipefail
LB_DIR=/opt/lb
# shellcheck disable=SC1091
source "$LB_DIR/topology.env"
OUT=${1:-/var/run/openvswitch/hc.json}
cat > "$OUT" <<EOF
{
"bridge": "$BR",
"of_version": "$OF",
"pool_id": $POOL_ID,
"slots": $SLOTS,
"slot_table": 11,
"dnat_table": 12,
"probe": "$HC_PROBE",
"http_path": "$HC_HTTP_PATH",
"interval": "$HC_INTERVAL",
"timeout": "$HC_TIMEOUT",
"rise": $HC_RISE,
"fall": $HC_FALL,
"api": "$HC_API",
"apply_script": "$LB_DIR/apply.sh",
"members": [
{
"id": 1,
"name": "be1",
"address": "$BE1_IP",
"port": $BE1_PORT,
"weight": 1,
"source": "$HC2_IP"
},
{
"id": 2,
"name": "be2",
"address": "$BE2_IP",
"port": $BE2_PORT,
"weight": 1,
"source": "$HC3_IP"
},
{
"id": 3,
"name": "be3",
"address": "$BE3_IP",
"port": $BE3_PORT,
"weight": 1,
"source": "$HC2_IP"
},
{
"id": 4,
"name": "be4",
"address": "$BE4_IP",
"port": $BE4_PORT,
"weight": 1,
"source": "$HC3_IP"
}
]
}
EOF
echo "$OUT"
Executable
+83
View File
@@ -0,0 +1,83 @@
#!/bin/bash
# Инструмент осмотра датапаса. Запускается внутри контейнера lb-router:
# docker compose exec lb-router lbctl <команда>
set -euo pipefail
LB_DIR=/opt/lb
# shellcheck disable=SC1091
source "$LB_DIR/topology.env"
usage() {
cat <<USAGE
lbctl — осмотр датапаса стенда hpnn_v2
ports порты моста и их ofport
flows [табл] правила (все или конкретной таблицы), со счётчиками
health состояние членов пула по данным health-проб
slots раскладка таблицы слотов и счётчики пакетов
drain <член> вывести член из балансировки (пробы продолжаются)
enable <член> вернуть член в балансировку
metrics метрики в формате Prometheus
conns таблица соединений ct (зона $CT_ZONE)
trace <src_ip> [src_port]
ofproto/trace для TCP-сессии клиент -> VIP:$VIP_PORT
reload перегенерировать и применить пайплайн
USAGE
}
api() { curl -fsS --max-time 5 "http://$HC_API$1"; }
case "${1:-}" in
ports)
ovs-vsctl show
ovs-ofctl -O "$OF" show "$BR" | grep -E '^\s+[0-9]+\('
;;
flows)
if [ -n "${2:-}" ]; then
ovs-ofctl -O "$OF" dump-flows "$BR" "table=$2"
else
ovs-ofctl -O "$OF" dump-flows "$BR"
fi
;;
health)
api /status.txt
;;
status)
api /status
;;
slots)
echo "Раскладка по данным датапаса (счётчики правил дают per-member статистику):"
ovs-ofctl -O "$OF" dump-flows "$BR" table=11 \
| sed -n 's/.*n_packets=\([0-9]*\).*set_field:\(0x[0-9a-f]*\)->reg2.*/\2 \1/p' \
| awk '{slots[$1]++; pkts[$1]+=$2}
END {for (m in slots) printf " член id=%s: слотов %d, пакетов %d\n", m, slots[m], pkts[m]}' \
| sort
drops=$(ovs-ofctl -O "$OF" dump-flows "$BR" table=11 | sed -n 's/.*n_packets=\([0-9]*\).*priority=0.*/\1/p')
echo " отброшено правилом fail-close: ${drops:-0}"
api /status.txt | sed -n '2p'
;;
drain)
api "/drain?member=${2:?укажите имя члена}"
;;
enable)
api "/enable?member=${2:?укажите имя члена}"
;;
metrics)
api /metrics
;;
conns)
ovs-appctl dpctl/dump-conntrack | grep -F "zone=$CT_ZONE" || echo "соединений нет"
;;
trace)
src_ip="${2:?укажите IP клиента}"
src_port="${3:-40000}"
ovs-appctl ofproto/trace "$BR" \
"in_port=$PUB_OFPORT,dl_src=00:11:22:33:44:55,dl_dst=$PUB_MAC,dl_type=0x0800,nw_src=$src_ip,nw_dst=$VIP,nw_proto=6,nw_ttl=64,tp_src=$src_port,tp_dst=$VIP_PORT,tcp_flags=syn"
;;
reload)
"$LB_DIR/apply.sh"
;;
*)
usage
;;
esac
Executable
+202
View File
@@ -0,0 +1,202 @@
#!/bin/bash
# Генератор OpenFlow-пайплайна br-lb. Печатает flow-файл в stdout.
# Применяется атомарным бандлом: ovs-ofctl --bundle -O OpenFlow15 replace-flows.
#
# Карта таблиц:
# 0 — классификация по in_port и типу трафика, обучение MAC клиентов
# 5 — ARP-респондер для собственных адресов узла и VIP
# 6 — ICMP echo-респондер для тех же адресов
# 10 — листенеры: хэш сессии в слот (multipath), прочий IP -> маршрутизация
# 11 — таблица слотов: слот -> член пула. Заливает и обновляет демон hcd
# 12 — применение DNAT выбранным членом пула (по reg2)
# 15 — обратный путь из приватных сегментов (снятие DNAT через ct)
# 16 — пост-ct hook (счётчики, место под будущие проверки)
# 20 — маршрутизация (LPM через приоритет = длина префикса)
# 21 — adjacency: next-hop MAC + выходной порт
#
# Регистры: reg1 — номер слота, reg2 — идентификатор члена пула.
#
# Нумерация не произвольна: goto_table в OpenFlow разрешает переход только
# вперёд, поэтому обратный путь (15/16) стоит до общей маршрутизации (20).
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")
HC2_MAC_H=$(mac2hex "$HC2_MAC")
HC3_MAC_H=$(mac2hex "$HC3_MAC")
HC2_IP_H=$(ip2hex "$HC2_IP")
HC3_IP_H=$(ip2hex "$HC3_IP")
# Действие обучения: по IP-адресу отправителя создаёт в таблице 21 запись
# adjacency для обратного пути — куда и с каким MAC отправлять ответы этому
# клиенту. Заменяет ARP-резолвер, которого у чисто-OpenFlow узла нет.
LEARN="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:$PUB_MAC_H->NXM_OF_ETH_SRC[],output:NXM_OF_IN_PORT[])"
# arp_responder <ip> <ip_hex> <mac> <mac_hex>
arp_responder() {
echo "table=5,priority=100,arp,arp_op=1,arp_tpa=$1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],mod_dl_src:$3,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:$4->NXM_NX_ARP_SHA[],load:$2->NXM_OF_ARP_SPA[],IN_PORT"
}
# icmp_responder <ip> <ip_hex> <mac_hex>
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"
}
cat <<EOF
# =============================================================================
# Таблица 0 — классификация
# =============================================================================
# ARP обрабатывает собственный респондер (таблица 5): ядро в форвардинге не
# участвует, поэтому штатного ARP-стека у узла нет.
table=0,priority=200,arp actions=goto_table:5
# ICMP echo-request на собственные адреса узла и на VIP — в респондер.
table=0,priority=190,icmp,nw_dst=$LB_PUB_IP,icmp_type=8 actions=goto_table:6
table=0,priority=190,icmp,nw_dst=$VIP,icmp_type=8 actions=goto_table:6
table=0,priority=190,icmp,nw_dst=$LB_P2_IP,icmp_type=8 actions=goto_table:6
table=0,priority=190,icmp,nw_dst=$LB_P3_IP,icmp_type=8 actions=goto_table:6
# Вход из публичного сегмента: запоминаем MAC отправителя как adjacency для
# обратного пути (замена ARP-резолвера на клиентской стороне), затем к
# листенерам. Записи живут 300 с и обновляются каждым пакетом клиента.
#
# Обучение включено только для трафика, адресованного стенду (VIP, адрес узла,
# приватные сегменты). Иначе в таблицу 21 попадал бы весь широковещательный
# шум LAN — macvlan-порт видит DHCP, mDNS и SSDP всех соседей по сегменту.
table=0,priority=110,in_port=$PUB_OFPORT,ip,nw_dst=$VIP actions=$LEARN,goto_table:10
table=0,priority=110,in_port=$PUB_OFPORT,ip,nw_dst=$LB_PUB_IP actions=$LEARN,goto_table:10
table=0,priority=110,in_port=$PUB_OFPORT,ip,nw_dst=$P2_NET actions=$LEARN,goto_table:10
table=0,priority=110,in_port=$PUB_OFPORT,ip,nw_dst=$P3_NET actions=$LEARN,goto_table:10
table=0,priority=100,in_port=$PUB_OFPORT,ip actions=drop
# Вход из приватных сегментов: обратный путь балансируемых сессий и транзит.
table=0,priority=100,in_port=$P2_OFPORT,ip actions=goto_table:15
table=0,priority=100,in_port=$P3_OFPORT,ip actions=goto_table:15
# Health-пробы, порождённые самим узлом: сразу в маршрутизацию, без ct и без
# обучения — это трафик из стека узла, а не клиентская сессия.
table=0,priority=100,in_port=$HC2_OFPORT,ip actions=goto_table:20
table=0,priority=100,in_port=$HC3_OFPORT,ip actions=goto_table:20
# Всё остальное (не-IP, не-ARP: broadcast, IPv6, DHCP) в прототипе не нужно.
table=0,priority=0 actions=drop
# =============================================================================
# Таблица 5 — ARP-респондер
# =============================================================================
$(arp_responder "$LB_PUB_IP" "$LB_PUB_IP_H" "$PUB_MAC" "$PUB_MAC_H")
$(arp_responder "$VIP" "$VIP_H" "$PUB_MAC" "$PUB_MAC_H")
$(arp_responder "$LB_P2_IP" "$LB_P2_IP_H" "$P2_MAC" "$P2_MAC_H")
$(arp_responder "$LB_P3_IP" "$LB_P3_IP_H" "$P3_MAC" "$P3_MAC_H")
# Адреса источника health-проб: бэкенд резолвит их, отвечая на пробу.
$(arp_responder "$HC2_IP" "$HC2_IP_H" "$HC2_MAC" "$HC2_MAC_H")
$(arp_responder "$HC3_IP" "$HC3_IP_H" "$HC3_MAC" "$HC3_MAC_H")
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")
table=6,priority=0 actions=drop
# =============================================================================
# Таблица 10 — листенеры
# =============================================================================
# Единственный листенер прототипа: TCP $VIP:$VIP_PORT.
# multipath раскладывает сессию в один из $SLOTS слотов по симметричному
# L4-хэшу; basis = POOL_ID, поэтому разные пулы дают независимые раскладки.
# Хэш симметричен, то есть прямое и обратное направление дают один слот —
# свойство, необходимое для будущего stateless-датапаса.
table=10,priority=200,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,ip,nw_dst=$VIP actions=drop
# Прочий IP из публичного сегмента маршрутизируется обычным образом
# (клиент может обратиться напрямую к бэкенду, минуя балансировку).
table=10,priority=100,ip actions=goto_table:20
table=10,priority=0 actions=drop
# =============================================================================
# Таблица 11 — таблица слотов
# =============================================================================
# Содержимое ($SLOTS правил вида reg1=<слот> -> reg2=<член пула>) рассчитывает
# и заливает демон hcd по Maglev-раскладке живых членов пула. Здесь только
# основание fail-close: пока пул пуст, трафик на VIP отбрасывается со
# счётчиком, а не уходит на заведомо мёртвый бэкенд.
#
# Счётчики правил этой таблицы дают бесплатную per-member статистику.
table=11,priority=0 actions=drop
# =============================================================================
# Таблица 12 — DNAT выбранным членом пула
# =============================================================================
# Слот положил идентификатор члена в reg2. SNAT не выполняется: бэкенд видит
# реальный адрес клиента.
table=12,priority=100,ip,reg2=0x1 actions=ct(commit,zone=$CT_ZONE,nat(dst=$BE1_IP:$BE1_PORT),table=20)
table=12,priority=100,ip,reg2=0x2 actions=ct(commit,zone=$CT_ZONE,nat(dst=$BE2_IP:$BE2_PORT),table=20)
table=12,priority=100,ip,reg2=0x3 actions=ct(commit,zone=$CT_ZONE,nat(dst=$BE3_IP:$BE3_PORT),table=20)
table=12,priority=100,ip,reg2=0x4 actions=ct(commit,zone=$CT_ZONE,nat(dst=$BE4_IP:$BE4_PORT),table=20)
table=12,priority=0 actions=drop
# =============================================================================
# Таблица 15 — обратный путь из приватных сегментов
# =============================================================================
# ct(nat) без commit снимает ранее выполненный DNAT: source возвращается к VIP.
# Для сессий, которых нет в таблице соединений (трафик be1<->be2, обращения
# бэкендов к клиентским префиксам), пакет проходит без изменений.
table=15,priority=100,tcp actions=ct(table=16,zone=$CT_ZONE,nat)
table=15,priority=50,ip actions=goto_table:20
table=15,priority=0 actions=drop
# =============================================================================
# Таблица 16 — после ct
# =============================================================================
table=16,priority=100,ip actions=goto_table:20
table=16,priority=0 actions=drop
# =============================================================================
# Таблица 20 — маршрутизация
# =============================================================================
# Приоритет = длина префикса, что даёт longest-prefix match средствами
# OpenFlow. Дефолтного маршрута нет: неизвестное назначение отбрасывается.
#
# Собственные адреса узла (/32, приоритет 32) — ответы бэкендов на health-пробы.
# TTL для них не уменьшается: пакет адресован самому узлу, а не транзитный.
table=20,priority=32,ip,nw_dst=$HC2_IP actions=goto_table:21
table=20,priority=32,ip,nw_dst=$HC3_IP actions=goto_table:21
table=20,priority=24,ip,nw_dst=$P2_NET actions=dec_ttl,mod_dl_src:$P2_MAC,goto_table:21
table=20,priority=24,ip,nw_dst=$P3_NET actions=dec_ttl,mod_dl_src:$P3_MAC,goto_table:21
table=20,priority=24,ip,nw_dst=$PUB_NET actions=dec_ttl,mod_dl_src:$PUB_MAC,goto_table:21
table=20,priority=0 actions=drop
# =============================================================================
# Таблица 21 — adjacency (next-hop MAC + выходной порт)
# =============================================================================
# Бэкенды прописаны статически: их MAC фиксирован в docker-compose.yml.
table=21,priority=100,ip,nw_dst=$BE1_IP actions=mod_dl_dst:$BE1_MAC,output:$P2_OFPORT
table=21,priority=100,ip,nw_dst=$BE2_IP actions=mod_dl_dst:$BE2_MAC,output:$P3_OFPORT
table=21,priority=100,ip,nw_dst=$BE3_IP actions=mod_dl_dst:$BE3_MAC,output:$P2_OFPORT
table=21,priority=100,ip,nw_dst=$BE4_IP actions=mod_dl_dst:$BE4_MAC,output:$P3_OFPORT
# Ответы на health-пробы — в стек узла через internal-порты.
table=21,priority=100,ip,nw_dst=$HC2_IP actions=mod_dl_dst:$HC2_MAC,output:$HC2_OFPORT
table=21,priority=100,ip,nw_dst=$HC3_IP actions=mod_dl_dst:$HC3_MAC,output:$HC3_OFPORT
# priority=90 — записи клиентов, устанавливаемые действием learn из таблицы 0.
table=21,priority=0 actions=drop
EOF
+70
View File
@@ -0,0 +1,70 @@
# Единственный источник правды по адресам стенда шага 1.
# Используется entrypoint.sh (сборка моста) и pipeline.sh (генерация OpenFlow).
# Значения здесь должны совпадать с docker-compose.yml.
BR=br-lb
OF=OpenFlow15
CT_ZONE=1
# --- сеть 1: публичный сегмент (macvlan поверх enp3s0, реальный L2 LAN) -------
# IP-адреса ниже существуют ТОЛЬКО в OpenFlow-правилах: ARP-респондер отвечает
# на них MAC-адресом PUB_MAC. На интерфейсе адрес не настраивается.
PUB_IFNAME=pub0
PUB_OFPORT=1
PUB_MAC=02:42:c0:a8:05:14
PUB_NET=192.168.5.0/24
LB_PUB_IP=192.168.5.20
VIP=192.168.5.21
VIP_PORT=80
# --- сеть 2: приватный сегмент бэкенда be1 -----------------------------------
P2_IFNAME=p2
P2_OFPORT=2
P2_MAC=02:42:0a:14:00:01
P2_NET=10.20.0.0/24
LB_P2_IP=10.20.0.1
BE1_IP=10.20.0.2
BE1_MAC=02:42:0a:14:00:02
BE1_PORT=8080
BE3_IP=10.20.0.3
BE3_MAC=02:42:0a:14:00:03
BE3_PORT=8080
# Уникальный адрес узла в сегменте — источник health-проб (§7.1 дизайна v1).
# Живёт на internal-порту OVS: это единственный адрес, который узел держит в
# ядре, весь остальной форвардинг идёт мимо стека.
HC2_IFNAME=hcif-p2
HC2_OFPORT=4
HC2_MAC=02:42:0a:14:00:fd
HC2_IP=10.20.0.253
# --- сеть 3: приватный сегмент бэкенда be2 -----------------------------------
P3_IFNAME=p3
P3_OFPORT=3
P3_MAC=02:42:0a:1e:00:01
P3_NET=10.30.0.0/24
LB_P3_IP=10.30.0.1
BE2_IP=10.30.0.2
BE2_MAC=02:42:0a:1e:00:02
BE2_PORT=8080
BE4_IP=10.30.0.3
BE4_MAC=02:42:0a:1e:00:03
BE4_PORT=8080
HC3_IFNAME=hcif-p3
HC3_OFPORT=5
HC3_MAC=02:42:0a:1e:00:fd
HC3_IP=10.30.0.253
# --- пул и таблица слотов ----------------------------------------------------
# POOL_ID служит basis хэша multipath: разные пулы дают независимые раскладки.
POOL_ID=1
# Слотов на пул (§5.1 дизайна v1). Определяет гранулярность весов.
SLOTS=1024
# --- health-check ------------------------------------------------------------
HC_PROBE=http # http | tcp
HC_HTTP_PATH=/healthz
HC_INTERVAL=2s
HC_TIMEOUT=1s
HC_RISE=2 # успехов подряд для перевода в up
HC_FALL=3 # неудач подряд для перевода в down
HC_API=127.0.0.1:9111