Go-based network automation with YANG models, gRPC, Ansible, Terraform, and Kubernetes integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|