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() } } 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) } } // 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.engine.Sync(m.listLocked()) } // 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.engine.Sync(m.listLocked()) } // 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.engine.Sync(m.listLocked()) } // 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.engine.Sync(m.listLocked()) } // 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 existing.ID == mem.ID || 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 live telemetry mem.Status = g.Members[idx].Status 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.engine.Sync(m.listLocked()) } // 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.engine.Sync(m.listLocked()) } // 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 }