| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- package sdwan
- import (
- "encoding/json"
- "fmt"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "sync"
- "time"
- )
- const DefaultStorePath = "/etc/debtik/sdwan.json"
- // Global resolver registered for firewall integration
- var GlobalManager *Manager
- // Manager coordinates SD-WAN group configurations, kernel routing, and SLA health monitoring.
- type Manager struct {
- mu sync.RWMutex
- storePath string
- groups map[string]*SDWANGroup
- engine *KernelEngine
- monitor *HealthMonitor
- }
- // NewManager creates an SD-WAN manager with persistent storage.
- func NewManager(customStore ...string) *Manager {
- store := DefaultStorePath
- if len(customStore) > 0 && customStore[0] != "" {
- store = customStore[0]
- }
- mgr := &Manager{
- storePath: store,
- groups: make(map[string]*SDWANGroup),
- engine: NewKernelEngine(),
- }
- mgr.monitor = NewHealthMonitor(func() {
- mgr.mu.RLock()
- groups := mgr.listLocked()
- mgr.mu.RUnlock()
- _ = mgr.engine.Sync(groups)
- })
- mgr.load()
- mgr.SyncAll()
- mgr.monitor.Start(func() []*SDWANGroup {
- return mgr.List()
- })
- GlobalManager = mgr
- return mgr
- }
- func (m *Manager) Stop() {
- if m.monitor != nil {
- m.monitor.Stop()
- }
- if m.engine != nil {
- m.engine.mu.Lock()
- for dev := range m.engine.virtualDevices {
- m.engine.removeVirtualDevice(dev)
- }
- m.engine.virtualDevices = make(map[string]bool)
- m.engine.cleanupMangle()
- m.engine.cleanupIPRules()
- m.engine.mu.Unlock()
- }
- }
- // Has checks if an SD-WAN group exists by ID or name.
- func (m *Manager) Has(idOrName string) bool {
- _, found := m.Get(idOrName)
- return found
- }
- func (m *Manager) load() {
- m.mu.Lock()
- defer m.mu.Unlock()
- data, err := os.ReadFile(m.storePath)
- if err != nil {
- return
- }
- var list []*SDWANGroup
- if err := json.Unmarshal(data, &list); err == nil {
- for _, g := range list {
- m.groups[g.ID] = g
- }
- }
- }
- func (m *Manager) save() error {
- _ = os.MkdirAll(filepath.Dir(m.storePath), 0755)
- list := make([]*SDWANGroup, 0, len(m.groups))
- for _, g := range m.groups {
- list = append(list, g)
- }
- data, err := json.MarshalIndent(list, "", " ")
- if err != nil {
- return err
- }
- return os.WriteFile(m.storePath, data, 0644)
- }
- func (m *Manager) listLocked() []*SDWANGroup {
- list := make([]*SDWANGroup, 0, len(m.groups))
- for _, g := range m.groups {
- list = append(list, g)
- }
- sort.Slice(list, func(i, j int) bool {
- return list[i].Name < list[j].Name
- })
- return list
- }
- // List returns a sorted copy of all configured SD-WAN groups.
- func (m *Manager) List() []*SDWANGroup {
- m.mu.RLock()
- defer m.mu.RUnlock()
- return m.listLocked()
- }
- // Get returns an SD-WAN group by ID or group name.
- func (m *Manager) Get(idOrName string) (*SDWANGroup, bool) {
- m.mu.RLock()
- defer m.mu.RUnlock()
- if g, ok := m.groups[idOrName]; ok {
- return g, true
- }
- for _, g := range m.groups {
- if strings.EqualFold(g.Name, idOrName) {
- return g, true
- }
- }
- return nil, false
- }
- // Save validates, stores, and synchronizes an SD-WAN group.
- func (m *Manager) Save(g *SDWANGroup) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if g.Name == "" {
- g.Name = fmt.Sprintf("sdwan%d", len(m.groups))
- }
- if g.ID == "" {
- g.ID = fmt.Sprintf("sdwan-%d", time.Now().UnixNano())
- }
- if g.Mode == "" {
- g.Mode = ModeWeighted
- }
- if g.ProbeInterval <= 0 {
- g.ProbeInterval = 3
- }
- // Validate name collisions
- for _, existing := range m.groups {
- if existing.ID != g.ID && strings.EqualFold(existing.Name, g.Name) {
- return fmt.Errorf("sd-wan group with name %q already exists", g.Name)
- }
- }
- // Preserve existing members if not specified in update payload
- if existing, ok := m.groups[g.ID]; ok {
- if g.Members == nil && existing.Members != nil {
- g.Members = existing.Members
- }
- }
- // Ensure member IDs and initial statuses
- for _, mem := range g.Members {
- if mem.ID == "" {
- mem.ID = fmt.Sprintf("mem-%d-%s", time.Now().UnixNano(), mem.Interface)
- }
- if mem.Weight <= 0 {
- mem.Weight = 50
- }
- if mem.Priority <= 0 {
- mem.Priority = 1
- }
- if mem.Status == "" {
- mem.Status = StatusHealthy
- }
- }
- m.groups[g.ID] = g
- if err := m.save(); err != nil {
- return err
- }
- return m.syncLocked()
- }
- func (m *Manager) syncLocked() error {
- err := m.engine.Sync(m.listLocked())
- _ = m.save()
- return err
- }
- // SetState enables or disables an SD-WAN group.
- func (m *Manager) SetState(idOrName string, disabled bool) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- g := m.findLocked(idOrName)
- if g == nil {
- return fmt.Errorf("sd-wan group %q not found", idOrName)
- }
- g.Disabled = disabled
- if err := m.save(); err != nil {
- return err
- }
- return m.syncLocked()
- }
- // Delete removes an SD-WAN group.
- func (m *Manager) Delete(idOrName string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- g := m.findLocked(idOrName)
- if g == nil {
- return fmt.Errorf("sd-wan group %q not found", idOrName)
- }
- delete(m.groups, g.ID)
- if err := m.save(); err != nil {
- return err
- }
- return m.syncLocked()
- }
- // AddMember adds an interface member to a group.
- func (m *Manager) AddMember(groupIDOrName string, mem *SDWANMember) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- g := m.findLocked(groupIDOrName)
- if g == nil {
- return fmt.Errorf("sd-wan group %q not found", groupIDOrName)
- }
- if mem.Interface == "" {
- return fmt.Errorf("member interface is required")
- }
- // Check duplicates
- for _, existing := range g.Members {
- if existing.Interface == mem.Interface {
- return fmt.Errorf("interface %s already a member of %s", mem.Interface, g.Name)
- }
- }
- if mem.ID == "" {
- mem.ID = fmt.Sprintf("mem-%d-%s", time.Now().UnixNano(), mem.Interface)
- }
- if mem.Weight <= 0 {
- mem.Weight = 50
- }
- if mem.Priority <= 0 {
- mem.Priority = 1
- }
- if mem.TargetProbe == "" {
- mem.TargetProbe = "1.1.1.1"
- }
- mem.Status = StatusHealthy
- g.Members = append(g.Members, mem)
- if err := m.save(); err != nil {
- return err
- }
- return m.syncLocked()
- }
- // UpdateMember modifies a member's configuration.
- func (m *Manager) UpdateMember(groupIDOrName string, mem *SDWANMember) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- g := m.findLocked(groupIDOrName)
- if g == nil {
- return fmt.Errorf("sd-wan group %q not found", groupIDOrName)
- }
- idx := -1
- for i, existing := range g.Members {
- if (mem.ID != "" && existing.ID == mem.ID) || (mem.Interface != "" && existing.Interface == mem.Interface) {
- idx = i
- break
- }
- }
- if idx == -1 {
- return fmt.Errorf("member %s not found in group %s", mem.Interface, g.Name)
- }
- // Preserve existing ID & Interface if not specified
- if mem.ID == "" {
- mem.ID = g.Members[idx].ID
- }
- if mem.Interface == "" {
- mem.Interface = g.Members[idx].Interface
- }
- if mem.Weight <= 0 {
- mem.Weight = g.Members[idx].Weight
- }
- if mem.Priority <= 0 {
- mem.Priority = g.Members[idx].Priority
- }
- if mem.TargetProbe == "" {
- mem.TargetProbe = g.Members[idx].TargetProbe
- }
- if mem.MaxLatency <= 0 {
- mem.MaxLatency = g.Members[idx].MaxLatency
- }
- if mem.MaxLoss <= 0 {
- mem.MaxLoss = g.Members[idx].MaxLoss
- }
- // Preserve live telemetry
- mem.Status = g.Members[idx].Status
- if mem.Status == "" {
- mem.Status = StatusHealthy
- }
- mem.LiveLatency = g.Members[idx].LiveLatency
- mem.PacketLoss = g.Members[idx].PacketLoss
- mem.Jitter = g.Members[idx].Jitter
- mem.ConsecutiveFails = g.Members[idx].ConsecutiveFails
- g.Members[idx] = mem
- if err := m.save(); err != nil {
- return err
- }
- return m.syncLocked()
- }
- // DeleteMember removes a member from a group.
- func (m *Manager) DeleteMember(groupIDOrName, memberIDOrIface string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- g := m.findLocked(groupIDOrName)
- if g == nil {
- return fmt.Errorf("sd-wan group %q not found", groupIDOrName)
- }
- idx := -1
- for i, mem := range g.Members {
- if mem.ID == memberIDOrIface || mem.Interface == memberIDOrIface {
- idx = i
- break
- }
- }
- if idx == -1 {
- return fmt.Errorf("member %s not found in group %s", memberIDOrIface, g.Name)
- }
- g.Members = append(g.Members[:idx], g.Members[idx+1:]...)
- if err := m.save(); err != nil {
- return err
- }
- return m.syncLocked()
- }
- // ResolveGroupInterfaces resolves an SD-WAN group to its member interfaces (used by Firewall).
- func (m *Manager) ResolveGroupInterfaces(groupName string) []string {
- m.mu.RLock()
- defer m.mu.RUnlock()
- var res []string
- for _, g := range m.groups {
- if strings.EqualFold(g.Name, groupName) && !g.Disabled {
- for _, mem := range g.Members {
- if !mem.Disabled && mem.Interface != "" {
- res = append(res, mem.Interface)
- }
- }
- break
- }
- }
- sort.Strings(res)
- return res
- }
- // ResolveAllSDWANGroups returns a mapping of all group names to their member interfaces.
- func (m *Manager) ResolveAllSDWANGroups() map[string][]string {
- m.mu.RLock()
- defer m.mu.RUnlock()
- res := make(map[string][]string)
- for _, g := range m.groups {
- if !g.Disabled {
- var members []string
- for _, mem := range g.Members {
- if !mem.Disabled && mem.Interface != "" {
- members = append(members, mem.Interface)
- }
- }
- sort.Strings(members)
- res[g.Name] = members
- }
- }
- return res
- }
- // GetSummary returns high-level telemetry across all SD-WAN groups.
- func (m *Manager) GetSummary() TelemetrySummary {
- return m.GetTelemetrySummary()
- }
- // GetTelemetrySummary returns high-level telemetry across all SD-WAN groups.
- func (m *Manager) GetTelemetrySummary() TelemetrySummary {
- m.mu.RLock()
- defer m.mu.RUnlock()
- sum := TelemetrySummary{
- TotalGroups: len(m.groups),
- }
- for _, g := range m.groups {
- if !g.Disabled {
- sum.ActiveGroups++
- }
- for _, mem := range g.Members {
- sum.TotalMembers++
- switch mem.Status {
- case StatusHealthy:
- sum.HealthyMembers++
- case StatusDegraded:
- sum.DegradedMembers++
- case StatusDown:
- sum.DownMembers++
- }
- }
- }
- return sum
- }
- // SyncAll reconciles kernel routing and iptables for all groups.
- func (m *Manager) SyncAll() {
- m.mu.Lock()
- defer m.mu.Unlock()
- _ = m.engine.Sync(m.listLocked())
- }
- func (m *Manager) findLocked(idOrName string) *SDWANGroup {
- if g, ok := m.groups[idOrName]; ok {
- return g
- }
- for _, g := range m.groups {
- if strings.EqualFold(g.Name, idOrName) {
- return g
- }
- }
- return nil
- }
- // RenameInterface updates SD-WAN member references when an interface is renamed.
- func (m *Manager) RenameInterface(oldName, newName string) {
- m.mu.Lock()
- defer m.mu.Unlock()
- modified := false
- for _, g := range m.groups {
- for _, mem := range g.Members {
- if mem.Interface == oldName {
- mem.Interface = newName
- modified = true
- }
- }
- }
- if modified {
- _ = m.save()
- _ = m.engine.Sync(m.listLocked())
- }
- }
|