mirror of https://github.com/h44z/wg-portal.git
165 lines
6.3 KiB
Go
165 lines
6.3 KiB
Go
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")
|
||
} |