Files

134 lines
5.4 KiB
Go
Raw Permalink Normal View History

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
}