wg-portal/internal/app/wireguard/wireguard_sync.go

165 lines
6.3 KiB
Go
Raw 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 wireguard
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"strings"
"github.com/fedor-git/wg-portal-2/internal/app"
"github.com/fedor-git/wg-portal-2/internal/domain"
)
type peerLister interface {
GetAllPeers(ctx context.Context) ([]domain.Peer, error)
}
func (m Manager) SyncAllPeersFromDB(ctx context.Context) (int, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return 0, err
}
if m.db == nil { return 0, fmt.Errorf("db repo is nil") }
if m.wg == nil { return 0, fmt.Errorf("wg controller is nil") }
ifaces, err := m.db.GetAllInterfaces(ctx)
if err != nil {
return 0, fmt.Errorf("list interfaces: %w", err)
}
applied := 0
for _, in := range ifaces {
// 1) за потреби відновили/привели інтерфейс у консистентний стан
if err := m.RestoreInterfaceState(ctx, true, in.Identifier); err != nil {
slog.ErrorContext(ctx, "restore interface state failed", "iface", in.Identifier, "err", err)
continue
}
// 2) дістали бажаний список пірів з БД (фільтруємо disabled)
peers, err := m.db.GetInterfacePeers(ctx, in.Identifier)
if err != nil {
slog.ErrorContext(ctx, "peer sync: failed to load peers", "iface", in.Identifier, "err", err)
continue
}
// >>> ДОДАЙ ЦЕ: якщо peers немає прибираємо всіх на інтерфейсі
if len(peers) == 0 {
if err := m.clearPeers(ctx, in.Identifier); err != nil {
slog.ErrorContext(ctx, "clear peers failed", "iface", in.Identifier, "err", err)
}
// не публікуємо івенти, якщо це fanout-sync
if !app.NoFanout(ctx) {
m.bus.Publish(app.TopicPeerUpdated)
}
continue
}
desired := make([]domain.Peer, 0, len(peers))
for i := range peers {
if !peers[i].IsDisabled() {
desired = append(desired, peers[i])
}
}
// 3) ЗАСТОСОВУЄМО ПОВНУ ЗАМІНУ (ключове!)
if err := m.replacePeers(ctx, in.Identifier, desired); err != nil {
// якщо інтерфейсу не існує/файл відсутній пробуємо ще раз після restore
if isNoSuchFile(err) {
slog.WarnContext(ctx, "replacePeers failed (no iface/file), restoring and retrying",
"iface", in.Identifier, "err", err)
if rErr := m.RestoreInterfaceState(ctx, true, in.Identifier); rErr != nil {
slog.ErrorContext(ctx, "retry restore interface failed", "iface", in.Identifier, "err", rErr)
continue
}
if r2 := m.replacePeers(ctx, in.Identifier, desired); r2 != nil {
slog.ErrorContext(ctx, "replacePeers retry failed", "iface", in.Identifier, "err", r2)
continue
}
} else {
slog.ErrorContext(ctx, "replacePeers failed", "iface", in.Identifier, "err", err)
continue
}
}
applied += len(desired)
}
return applied, nil
}
// replacePeers робить повну заміну складу peer-ів на інтерфейсі.
// Усередині має викликати бекенд з ReplacePeers=true.
// Реалізацію підженете під ваш controller (wgctrl, локальний тощо).
func (m Manager) replacePeers(ctx context.Context, iface domain.InterfaceIdentifier, peers []domain.Peer) error {
// ВАРІАНТ A: якщо контролер уміє "Replace" напряму:
// return m.wg.ReplacePeers(ctx, string(iface), peers)
// ВАРІАНТ B: якщо є низькорівневий доступ до wgctrl:
// - зібрати []wgtypes.PeerConfig з domain.Peer
// - викликати ConfigureDevice(..., wgtypes.Config{ReplacePeers: true, Peers: pcs})
//
// ВАРІАНТ C (fallback, якщо немає Replace API):
// - спочатку "очистити" пірів (ReplacePeers: true, Peers: nil)
// - потім додати кожного з desired через існуючий m.savePeers(ctx, &p)
// Нижче універсальний fallback «очистити і додати»:
if err := m.clearPeers(ctx, iface); err != nil {
return err
}
for i := range peers {
if err := m.savePeers(ctx, &peers[i]); err != nil {
return fmt.Errorf("add peer %s on %s: %w", peers[i].Identifier, iface, err)
}
// ВАЖЛИВО: під час sync не публікуємо події, аби не ловити шторм fanout
// (перенесіть publish із savePeers в той шар, де є user-driven зміни).
}
return nil
}
func (m Manager) clearPeers(ctx context.Context, iface domain.InterfaceIdentifier) error {
return m.wg.ClearPeers(ctx, string(iface))
}
// func (m Manager) applyPeers(ctx context.Context, peers []domain.Peer) error {
// var firstErr error
// for i := range peers {
// p := &peers[i]
// if p.IsDisabled() {
// continue
// }
// if err := m.savePeers(ctx, p); err != nil {
// if firstErr == nil {
// firstErr = fmt.Errorf("apply peer %s (iface %s): %w",
// p.Identifier, p.InterfaceIdentifier, err)
// }
// continue
// }
// m.bus.Publish(app.TopicPeerUpdated, *p)
// }
// return firstErr
// }
func (m Manager) applyPeers(ctx context.Context, peers []domain.Peer) error {
var firstErr error
for i := range peers {
p := &peers[i]
if p.IsDisabled() { continue }
if err := m.savePeers(ctx, p); err != nil {
if firstErr == nil {
firstErr = fmt.Errorf("apply peer %s (iface %s): %w", p.Identifier, p.InterfaceIdentifier, err)
}
continue
}
// <-- тут головне
if !app.NoFanout(ctx) {
m.bus.Publish(app.TopicPeerUpdated)
}
}
return firstErr
}
func isNoSuchFile(err error) bool {
if err == nil {
return false
}
return errors.Is(err, os.ErrNotExist) || strings.Contains(err.Error(), "file does not exist")
}