Files
ayurishchevandClaude Opus 5 fa389f119b Шаг 5: динамическое изучение MAC/ARP для бэкендов
MAC каждого члена пула больше не статическая константа в topology.env,
а резолвится демоном hcd через обычный ARP ядра — в реальном
окружении MAC бэкенда заранее не известен (сервер ещё не подключён,
NIC может замениться), топология не описывается статически, в отличие
от стенда.

hcif-порты (единственные адреса узла в ядре) уже были настоящими
L3-интерфейсами в тех же сегментах, что и бэкенды — единственное, что
мешало обычному ARP, это permanent-записи ip neigh в entrypoint.sh.
Убрав их и добавив hc/neigh.go (читает ip -json neigh show, точечно
заливает бандл в таблицу 21 на том же тикере, что и health-пробы),
получили резолвер без нового OpenFlow-контроллера.

Таблица 21 стала единственным источником MAC для обоих путей —
маршрутизируемого и коммутируемого (шаг 3): таблица 12 больше не
дублирует MAC инлайново, ct(commit) ведёт сразу в таблицу 21.

Исправлен попутно найденный баг: после apply.sh (replace-flows)
таблица 21 не восстанавливалась, поскольку syncNeighbors сравнивал
MAC с памятью демона, а не с датапасом. Добавлен force-режим по
аналогии с Agent.apply(), плюс регрессионная проверка в verify.sh.

Проверено на живом стенде: MAC всех четырёх членов резолвлен и
совпадает с реальными интерфейсами; смена MAC "железа" обнаружена и
применена без вмешательства за счёт штатного старения ARP ядра.
Регрессия: 94 из 94 проверок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 12:33:48 +03:00

135 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"time"
)
// Резолв MAC членов пула через neigh-таблицу ядра — шаг 5.
//
// Адреса членов известны из конфигурации заранее, их MAC — нет (в реальном
// окружении сервер ещё не подключён, NIC может замениться). hcif-порты —
// настоящие L3-интерфейсы ядра в том же сегменте, что и члены пула, поэтому
// ядро само резолвит их MAC обычным ARP (entrypoint.sh больше не подсовывает
// статические permanent-записи). hcd лишь читает то, что уже узнало ядро, и
// отражает это в единственном месте датапаса, где применяется MAC следующего
// перехода, — в таблице 21 (см. pipeline.sh).
//
// Для клиентов (адрес заранее не известен и не enumerable) резолв остаётся
// пассивным действием learn в самом OpenFlow — здесь не участвует.
// neighEntry — одна запись `ip -json neigh show`.
type neighEntry struct {
Dst string `json:"dst"`
Dev string `json:"dev"`
Lladdr string `json:"lladdr"`
State []string `json:"state"`
}
// resolvedStates — состояния ARP, при которых MAC считается годным к
// использованию. FAILED и INCOMPLETE (или отсутствие записи) означают, что
// адрес пока не резолвлен.
var resolvedStates = map[string]bool{
"REACHABLE": true,
"STALE": true,
"DELAY": true,
"PROBE": true,
"PERMANENT": true,
}
// resolveMAC возвращает MAC адреса ip на интерфейсе iface, если ядро его уже
// резолвило. Пустая строка — адрес пока не резолвлен либо недостижим.
func resolveMAC(iface, ip string) (string, error) {
out, err := exec.Command("ip", "-json", "neigh", "show", "dev", iface, "to", ip).Output()
if err != nil {
return "", err
}
var entries []neighEntry
if err := json.Unmarshal(out, &entries); err != nil {
return "", fmt.Errorf("разбор ip -json neigh: %w", err)
}
for _, e := range entries {
if e.Lladdr == "" {
continue
}
for _, s := range e.State {
if resolvedStates[s] {
return e.Lladdr, nil
}
}
}
return "", nil
}
// syncNeighbors проходит по всем членам, резолвит их MAC через ядро и для
// каждого изменения атомарно обновляет его правило в таблице adjacency —
// точечным бандлом, не трогая записи остальных членов. Вызывается на том же
// тикере, что и health-пробы.
//
// force заливает правило заново, даже если MAC не изменился с прошлого раза:
// нужно после apply.sh (replace-flows стирает весь пайплайн, включая уже
// залитые сюда правила, а resolveMAC() в следующий раз вернёт тот же MAC и
// без force заливка не повторится). Тот же приём, что у Agent.apply(force).
func (a *Agent) syncNeighbors(force bool) {
for _, m := range a.cfg.Members {
mac, err := resolveMAC(m.Iface, m.Address)
if err != nil {
log.Printf("neigh: %s (%s@%s): %v", m.Name, m.Address, m.Iface, err)
continue
}
m.mu.Lock()
changed := force || mac != m.mac
if mac != m.mac {
m.macSince = time.Now()
}
m.mac = mac
m.mu.Unlock()
if !changed {
continue
}
if mac == "" {
log.Printf("neigh: %s (%s) MAC утерян — снимаю запись adjacency", m.Name, m.Address)
if err := a.applyNeigh(m); err != nil {
log.Printf("neigh: %s: ошибка снятия записи: %v", m.Name, err)
}
continue
}
log.Printf("neigh: %s (%s@%s) -> %s", m.Name, m.Address, m.Iface, mac)
if err := a.applyNeigh(m); err != nil {
log.Printf("neigh: %s: ошибка применения: %v", m.Name, err)
}
}
}
// applyNeigh заливает точечный бандл для ОДНОГО члена: снимает прежнее
// правило adjacency (если было) и, если MAC резолвлен, ставит новое. Формат
// файла бандла — тот же, что и для таблицы слотов (FlowBundle в maglev.go).
func (a *Agent) applyNeigh(m *Member) error {
m.mu.Lock()
mac := m.mac
m.mu.Unlock()
table := a.cfg.AdjTable
out := fmt.Sprintf("flow delete table=%d,ip,nw_dst=%s\n", table, m.Address)
if mac != "" {
out += fmt.Sprintf("flow add table=%d,priority=100,ip,nw_dst=%s actions=mod_dl_dst:%s,output:%d\n",
table, m.Address, mac, m.OFPort)
}
path := fmt.Sprintf("/var/run/openvswitch/neigh-%s.bundle", m.Name)
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
return err
}
res, err := exec.Command("ovs-ofctl", "-O", a.cfg.OFVersion, "bundle", a.cfg.Bridge, path).CombinedOutput()
if err != nil {
return fmt.Errorf("ovs-ofctl bundle: %v: %s", err, res)
}
return nil
}