kedge/internal/mesh/manager.go
Tyler King 6058e62348 Initial commit: Kedge network automation platform
Go-based network automation with YANG models, gRPC, Ansible,
Terraform, and Kubernetes integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 12:09:30 -05:00

153 lines
3.6 KiB
Go

package mesh
import (
"context"
"fmt"
"sync"
"time"
"go.uber.org/zap"
"golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/guildhouse-co/kedge/internal/config"
"github.com/guildhouse-co/kedge/internal/quartermaster"
)
// Manager manages WireGuard tunnel lifecycle for overlay mode.
type Manager struct {
cfg config.MeshConfig
qm *quartermaster.Client
log *zap.SugaredLogger
client *wgctrl.Client
mu sync.RWMutex
peers map[string]*Peer // keyed by public key
}
// NewManager creates a new WireGuard mesh manager.
func NewManager(cfg config.MeshConfig, qm *quartermaster.Client, log *zap.SugaredLogger) *Manager {
return &Manager{
cfg: cfg,
qm: qm,
log: log,
peers: make(map[string]*Peer),
}
}
// Run starts the mesh manager loop. It initializes the WireGuard interface,
// configures initial peers, and monitors tunnel health.
func (m *Manager) Run(ctx context.Context) error {
if !m.cfg.Enabled {
m.log.Info("overlay mode disabled, mesh manager idle")
<-ctx.Done()
return nil
}
var err error
m.client, err = wgctrl.New()
if err != nil {
return fmt.Errorf("failed to create wireguard client: %w", err)
}
defer m.client.Close()
// Ensure WireGuard interface exists.
if err := m.ensureInterface(); err != nil {
return fmt.Errorf("failed to ensure wg interface: %w", err)
}
// Configure initial peers from mesh config.
if err := m.configureInitialPeers(); err != nil {
return fmt.Errorf("failed to configure initial peers: %w", err)
}
// Health monitoring loop.
ticker := time.NewTicker(m.cfg.HealthInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
m.log.Info("mesh manager shutting down")
return nil
case <-ticker.C:
m.checkPeerHealth()
}
}
}
// PeerCount returns the number of active peers.
func (m *Manager) PeerCount() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.peers)
}
// AddPeer adds or updates a WireGuard peer.
func (m *Manager) AddPeer(pubKey string, endpoint string, allowedIPs []string) error {
m.mu.Lock()
defer m.mu.Unlock()
key, err := wgtypes.ParseKey(pubKey)
if err != nil {
return fmt.Errorf("invalid public key: %w", err)
}
peer := &Peer{
PublicKey: pubKey,
Endpoint: endpoint,
AllowedIPs: allowedIPs,
State: PeerStateActive,
LastSeen: time.Now(),
}
m.peers[pubKey] = peer
_ = key // TODO: Apply to wgctrl config.
m.log.Infow("peer added", "pubkey", pubKey[:8]+"...", "endpoint", endpoint)
return nil
}
func (m *Manager) ensureInterface() error {
// TODO: Create WireGuard interface if it doesn't exist.
// Use netlink to create the interface, then wgctrl to configure it.
m.log.Infof("ensuring wireguard interface %s", m.cfg.InterfaceName)
return nil
}
func (m *Manager) configureInitialPeers() error {
for _, p := range m.cfg.InitialPeers {
if err := m.AddPeer(p.PublicKey, p.Endpoint, p.AllowedIPs); err != nil {
m.log.Warnw("failed to add initial peer", "pubkey", p.PublicKey[:8]+"...", "error", err)
}
}
return nil
}
func (m *Manager) checkPeerHealth() {
m.mu.Lock()
defer m.mu.Unlock()
device, err := m.client.Device(m.cfg.InterfaceName)
if err != nil {
m.log.Warnw("failed to get wg device", "error", err)
return
}
for _, wgPeer := range device.Peers {
pubKey := wgPeer.PublicKey.String()
peer, ok := m.peers[pubKey]
if !ok {
continue
}
if !wgPeer.LastHandshakeTime.IsZero() {
peer.LastSeen = wgPeer.LastHandshakeTime
peer.State = PeerStateActive
}
if time.Since(peer.LastSeen) > m.cfg.DeadPeerTimeout {
peer.State = PeerStateDead
m.log.Warnw("peer dead", "pubkey", pubKey[:8]+"...", "last_seen", peer.LastSeen)
}
}
}