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()) } }