manager.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. package sdwan
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "sort"
  8. "strings"
  9. "sync"
  10. "time"
  11. )
  12. const DefaultStorePath = "/etc/debtik/sdwan.json"
  13. // Global resolver registered for firewall integration
  14. var GlobalManager *Manager
  15. // ResolvedRouteNexthop represents a target egress interface/gateway for routing.
  16. type ResolvedRouteNexthop struct {
  17. Gateway string `json:"gateway"`
  18. Interface string `json:"interface"`
  19. Weight int `json:"weight"`
  20. }
  21. var (
  22. stateChangeCallbacks []func()
  23. stateChangeMu sync.Mutex
  24. )
  25. // RegisterStateChangeCallback registers a function to be invoked when SD-WAN link health or topology changes.
  26. func RegisterStateChangeCallback(cb func()) {
  27. if cb == nil {
  28. return
  29. }
  30. stateChangeMu.Lock()
  31. defer stateChangeMu.Unlock()
  32. stateChangeCallbacks = append(stateChangeCallbacks, cb)
  33. }
  34. // NotifyStateChange invokes all registered state change callbacks.
  35. func NotifyStateChange() {
  36. stateChangeMu.Lock()
  37. cbs := make([]func(), len(stateChangeCallbacks))
  38. copy(cbs, stateChangeCallbacks)
  39. stateChangeMu.Unlock()
  40. for _, cb := range cbs {
  41. go cb()
  42. }
  43. }
  44. // Manager coordinates SD-WAN group configurations, kernel routing, and SLA health monitoring.
  45. type Manager struct {
  46. mu sync.RWMutex
  47. storePath string
  48. groups map[string]*SDWANGroup
  49. engine *KernelEngine
  50. monitor *HealthMonitor
  51. }
  52. // NewManager creates an SD-WAN manager with persistent storage.
  53. func NewManager(customStore ...string) *Manager {
  54. store := DefaultStorePath
  55. if len(customStore) > 0 && customStore[0] != "" {
  56. store = customStore[0]
  57. }
  58. mgr := &Manager{
  59. storePath: store,
  60. groups: make(map[string]*SDWANGroup),
  61. engine: NewKernelEngine(),
  62. }
  63. mgr.monitor = NewHealthMonitor(func() {
  64. mgr.mu.RLock()
  65. groups := mgr.listLocked()
  66. mgr.mu.RUnlock()
  67. _ = mgr.engine.Sync(groups)
  68. NotifyStateChange()
  69. })
  70. mgr.load()
  71. mgr.SyncAll()
  72. mgr.monitor.Start(func() []*SDWANGroup {
  73. return mgr.List()
  74. })
  75. GlobalManager = mgr
  76. return mgr
  77. }
  78. func (m *Manager) Stop() {
  79. if m.monitor != nil {
  80. m.monitor.Stop()
  81. }
  82. if m.engine != nil {
  83. m.engine.mu.Lock()
  84. for dev := range m.engine.virtualDevices {
  85. m.engine.removeVirtualDevice(dev)
  86. }
  87. m.engine.virtualDevices = make(map[string]bool)
  88. m.engine.cleanupMangle()
  89. m.engine.cleanupIPRules()
  90. m.engine.mu.Unlock()
  91. }
  92. }
  93. // Has checks if an SD-WAN group exists by ID or name.
  94. func (m *Manager) Has(idOrName string) bool {
  95. _, found := m.Get(idOrName)
  96. return found
  97. }
  98. func (m *Manager) load() {
  99. m.mu.Lock()
  100. defer m.mu.Unlock()
  101. data, err := os.ReadFile(m.storePath)
  102. if err != nil {
  103. return
  104. }
  105. var list []*SDWANGroup
  106. if err := json.Unmarshal(data, &list); err == nil {
  107. for _, g := range list {
  108. m.groups[g.ID] = g
  109. }
  110. }
  111. }
  112. func (m *Manager) save() error {
  113. _ = os.MkdirAll(filepath.Dir(m.storePath), 0755)
  114. list := make([]*SDWANGroup, 0, len(m.groups))
  115. for _, g := range m.groups {
  116. list = append(list, g)
  117. }
  118. data, err := json.MarshalIndent(list, "", " ")
  119. if err != nil {
  120. return err
  121. }
  122. return os.WriteFile(m.storePath, data, 0644)
  123. }
  124. func (m *Manager) listLocked() []*SDWANGroup {
  125. list := make([]*SDWANGroup, 0, len(m.groups))
  126. for _, g := range m.groups {
  127. list = append(list, g)
  128. }
  129. sort.Slice(list, func(i, j int) bool {
  130. return list[i].Name < list[j].Name
  131. })
  132. return list
  133. }
  134. // List returns a sorted copy of all configured SD-WAN groups.
  135. func (m *Manager) List() []*SDWANGroup {
  136. m.mu.RLock()
  137. defer m.mu.RUnlock()
  138. return m.listLocked()
  139. }
  140. // Get returns an SD-WAN group by ID or group name.
  141. func (m *Manager) Get(idOrName string) (*SDWANGroup, bool) {
  142. m.mu.RLock()
  143. defer m.mu.RUnlock()
  144. if g, ok := m.groups[idOrName]; ok {
  145. return g, true
  146. }
  147. for _, g := range m.groups {
  148. if strings.EqualFold(g.Name, idOrName) {
  149. return g, true
  150. }
  151. }
  152. return nil, false
  153. }
  154. // Save validates, stores, and synchronizes an SD-WAN group.
  155. func (m *Manager) Save(g *SDWANGroup) error {
  156. m.mu.Lock()
  157. defer m.mu.Unlock()
  158. if g.Name == "" {
  159. g.Name = fmt.Sprintf("sdwan%d", len(m.groups))
  160. }
  161. if g.ID == "" {
  162. g.ID = fmt.Sprintf("sdwan-%d", time.Now().UnixNano())
  163. }
  164. if g.Mode == "" {
  165. g.Mode = ModeWeighted
  166. }
  167. if g.ProbeInterval <= 0 {
  168. g.ProbeInterval = 3
  169. }
  170. // Validate name collisions
  171. for _, existing := range m.groups {
  172. if existing.ID != g.ID && strings.EqualFold(existing.Name, g.Name) {
  173. return fmt.Errorf("sd-wan group with name %q already exists", g.Name)
  174. }
  175. }
  176. // Preserve existing members if not specified in update payload
  177. if existing, ok := m.groups[g.ID]; ok {
  178. if g.Members == nil && existing.Members != nil {
  179. g.Members = existing.Members
  180. }
  181. }
  182. // Ensure member IDs and initial statuses
  183. for _, mem := range g.Members {
  184. if mem.ID == "" {
  185. mem.ID = fmt.Sprintf("mem-%d-%s", time.Now().UnixNano(), mem.Interface)
  186. }
  187. if mem.Weight <= 0 {
  188. mem.Weight = 50
  189. }
  190. if mem.Priority <= 0 {
  191. mem.Priority = 1
  192. }
  193. if mem.Status == "" {
  194. mem.Status = StatusHealthy
  195. }
  196. }
  197. m.groups[g.ID] = g
  198. if err := m.save(); err != nil {
  199. return err
  200. }
  201. return m.syncLocked()
  202. }
  203. func (m *Manager) syncLocked() error {
  204. err := m.engine.Sync(m.listLocked())
  205. _ = m.save()
  206. NotifyStateChange()
  207. return err
  208. }
  209. // SetState enables or disables an SD-WAN group.
  210. func (m *Manager) SetState(idOrName string, disabled bool) error {
  211. m.mu.Lock()
  212. defer m.mu.Unlock()
  213. g := m.findLocked(idOrName)
  214. if g == nil {
  215. return fmt.Errorf("sd-wan group %q not found", idOrName)
  216. }
  217. g.Disabled = disabled
  218. if err := m.save(); err != nil {
  219. return err
  220. }
  221. return m.syncLocked()
  222. }
  223. // Delete removes an SD-WAN group.
  224. func (m *Manager) Delete(idOrName string) error {
  225. m.mu.Lock()
  226. defer m.mu.Unlock()
  227. g := m.findLocked(idOrName)
  228. if g == nil {
  229. return fmt.Errorf("sd-wan group %q not found", idOrName)
  230. }
  231. delete(m.groups, g.ID)
  232. if err := m.save(); err != nil {
  233. return err
  234. }
  235. return m.syncLocked()
  236. }
  237. // AddMember adds an interface member to a group.
  238. func (m *Manager) AddMember(groupIDOrName string, mem *SDWANMember) error {
  239. m.mu.Lock()
  240. defer m.mu.Unlock()
  241. g := m.findLocked(groupIDOrName)
  242. if g == nil {
  243. return fmt.Errorf("sd-wan group %q not found", groupIDOrName)
  244. }
  245. if mem.Interface == "" {
  246. return fmt.Errorf("member interface is required")
  247. }
  248. // Check duplicates
  249. for _, existing := range g.Members {
  250. if existing.Interface == mem.Interface {
  251. return fmt.Errorf("interface %s already a member of %s", mem.Interface, g.Name)
  252. }
  253. }
  254. if mem.ID == "" {
  255. mem.ID = fmt.Sprintf("mem-%d-%s", time.Now().UnixNano(), mem.Interface)
  256. }
  257. if mem.Weight <= 0 {
  258. mem.Weight = 50
  259. }
  260. if mem.Priority <= 0 {
  261. mem.Priority = 1
  262. }
  263. if mem.TargetProbe == "" {
  264. mem.TargetProbe = "1.1.1.1"
  265. }
  266. mem.Status = StatusHealthy
  267. g.Members = append(g.Members, mem)
  268. if err := m.save(); err != nil {
  269. return err
  270. }
  271. return m.syncLocked()
  272. }
  273. // UpdateMember modifies a member's configuration.
  274. func (m *Manager) UpdateMember(groupIDOrName string, mem *SDWANMember) error {
  275. m.mu.Lock()
  276. defer m.mu.Unlock()
  277. g := m.findLocked(groupIDOrName)
  278. if g == nil {
  279. return fmt.Errorf("sd-wan group %q not found", groupIDOrName)
  280. }
  281. idx := -1
  282. for i, existing := range g.Members {
  283. if (mem.ID != "" && existing.ID == mem.ID) || (mem.Interface != "" && existing.Interface == mem.Interface) {
  284. idx = i
  285. break
  286. }
  287. }
  288. if idx == -1 {
  289. return fmt.Errorf("member %s not found in group %s", mem.Interface, g.Name)
  290. }
  291. // Preserve existing ID & Interface if not specified
  292. if mem.ID == "" {
  293. mem.ID = g.Members[idx].ID
  294. }
  295. if mem.Interface == "" {
  296. mem.Interface = g.Members[idx].Interface
  297. }
  298. if mem.Weight <= 0 {
  299. mem.Weight = g.Members[idx].Weight
  300. }
  301. if mem.Priority <= 0 {
  302. mem.Priority = g.Members[idx].Priority
  303. }
  304. if mem.TargetProbe == "" {
  305. mem.TargetProbe = g.Members[idx].TargetProbe
  306. }
  307. if mem.MaxLatency <= 0 {
  308. mem.MaxLatency = g.Members[idx].MaxLatency
  309. }
  310. if mem.MaxLoss <= 0 {
  311. mem.MaxLoss = g.Members[idx].MaxLoss
  312. }
  313. // Preserve live telemetry
  314. mem.Status = g.Members[idx].Status
  315. if mem.Status == "" {
  316. mem.Status = StatusHealthy
  317. }
  318. mem.LiveLatency = g.Members[idx].LiveLatency
  319. mem.PacketLoss = g.Members[idx].PacketLoss
  320. mem.Jitter = g.Members[idx].Jitter
  321. mem.ConsecutiveFails = g.Members[idx].ConsecutiveFails
  322. g.Members[idx] = mem
  323. if err := m.save(); err != nil {
  324. return err
  325. }
  326. return m.syncLocked()
  327. }
  328. // DeleteMember removes a member from a group.
  329. func (m *Manager) DeleteMember(groupIDOrName, memberIDOrIface string) error {
  330. m.mu.Lock()
  331. defer m.mu.Unlock()
  332. g := m.findLocked(groupIDOrName)
  333. if g == nil {
  334. return fmt.Errorf("sd-wan group %q not found", groupIDOrName)
  335. }
  336. idx := -1
  337. for i, mem := range g.Members {
  338. if mem.ID == memberIDOrIface || mem.Interface == memberIDOrIface {
  339. idx = i
  340. break
  341. }
  342. }
  343. if idx == -1 {
  344. return fmt.Errorf("member %s not found in group %s", memberIDOrIface, g.Name)
  345. }
  346. g.Members = append(g.Members[:idx], g.Members[idx+1:]...)
  347. if err := m.save(); err != nil {
  348. return err
  349. }
  350. return m.syncLocked()
  351. }
  352. // ResolveGroupInterfaces resolves an SD-WAN group to its member interfaces (used by Firewall).
  353. func (m *Manager) ResolveGroupInterfaces(groupName string) []string {
  354. m.mu.RLock()
  355. defer m.mu.RUnlock()
  356. var res []string
  357. for _, g := range m.groups {
  358. if strings.EqualFold(g.Name, groupName) && !g.Disabled {
  359. for _, mem := range g.Members {
  360. if !mem.Disabled && mem.Interface != "" {
  361. res = append(res, mem.Interface)
  362. }
  363. }
  364. break
  365. }
  366. }
  367. sort.Strings(res)
  368. return res
  369. }
  370. // ResolveAllSDWANGroups returns a mapping of all group names to their member interfaces.
  371. func (m *Manager) ResolveAllSDWANGroups() map[string][]string {
  372. m.mu.RLock()
  373. defer m.mu.RUnlock()
  374. res := make(map[string][]string)
  375. for _, g := range m.groups {
  376. if !g.Disabled {
  377. var members []string
  378. for _, mem := range g.Members {
  379. if !mem.Disabled && mem.Interface != "" {
  380. members = append(members, mem.Interface)
  381. }
  382. }
  383. sort.Strings(members)
  384. res[g.Name] = members
  385. }
  386. }
  387. return res
  388. }
  389. // GetSummary returns high-level telemetry across all SD-WAN groups.
  390. func (m *Manager) GetSummary() TelemetrySummary {
  391. return m.GetTelemetrySummary()
  392. }
  393. // GetTelemetrySummary returns high-level telemetry across all SD-WAN groups.
  394. func (m *Manager) GetTelemetrySummary() TelemetrySummary {
  395. m.mu.RLock()
  396. defer m.mu.RUnlock()
  397. sum := TelemetrySummary{
  398. TotalGroups: len(m.groups),
  399. }
  400. for _, g := range m.groups {
  401. if !g.Disabled {
  402. sum.ActiveGroups++
  403. }
  404. for _, mem := range g.Members {
  405. sum.TotalMembers++
  406. switch mem.Status {
  407. case StatusHealthy:
  408. sum.HealthyMembers++
  409. case StatusDegraded:
  410. sum.DegradedMembers++
  411. case StatusDown:
  412. sum.DownMembers++
  413. }
  414. }
  415. }
  416. return sum
  417. }
  418. // SyncAll reconciles kernel routing and iptables for all groups.
  419. func (m *Manager) SyncAll() {
  420. m.mu.Lock()
  421. defer m.mu.Unlock()
  422. _ = m.engine.Sync(m.listLocked())
  423. }
  424. func (m *Manager) findLocked(idOrName string) *SDWANGroup {
  425. if g, ok := m.groups[idOrName]; ok {
  426. return g
  427. }
  428. for _, g := range m.groups {
  429. if strings.EqualFold(g.Name, idOrName) {
  430. return g
  431. }
  432. }
  433. return nil
  434. }
  435. // RenameInterface updates SD-WAN member references when an interface is renamed.
  436. func (m *Manager) RenameInterface(oldName, newName string) {
  437. m.mu.Lock()
  438. defer m.mu.Unlock()
  439. modified := false
  440. for _, g := range m.groups {
  441. for _, mem := range g.Members {
  442. if mem.Interface == oldName {
  443. mem.Interface = newName
  444. modified = true
  445. }
  446. }
  447. }
  448. if modified {
  449. _ = m.save()
  450. _ = m.engine.Sync(m.listLocked())
  451. NotifyStateChange()
  452. }
  453. }
  454. // ResolveGroupForRouting resolves an SD-WAN group name or ID to active egress nexthops for kernel routing.
  455. func (m *Manager) ResolveGroupForRouting(groupNameOrID string) ([]ResolvedRouteNexthop, bool) {
  456. m.mu.RLock()
  457. defer m.mu.RUnlock()
  458. g := m.findLocked(groupNameOrID)
  459. if g == nil || g.Disabled || len(g.Members) == 0 {
  460. return nil, false
  461. }
  462. var candidates []*SDWANMember
  463. for _, mem := range g.Members {
  464. if !mem.Disabled && mem.Interface != "" {
  465. candidates = append(candidates, mem)
  466. }
  467. }
  468. if len(candidates) == 0 {
  469. return nil, false
  470. }
  471. var online []*SDWANMember
  472. for _, mem := range candidates {
  473. if mem.Status != StatusDown {
  474. online = append(online, mem)
  475. }
  476. }
  477. // Fallback to all enabled candidates if all are down so traffic isn't completely blackholed
  478. if len(online) == 0 {
  479. online = candidates
  480. }
  481. switch g.Mode {
  482. case ModeFailover:
  483. // Sort by priority (1 is highest priority)
  484. sort.Slice(online, func(i, j int) bool {
  485. p1, p2 := online[i].Priority, online[j].Priority
  486. if p1 <= 0 {
  487. p1 = 100
  488. }
  489. if p2 <= 0 {
  490. p2 = 100
  491. }
  492. return p1 < p2
  493. })
  494. primary := online[0]
  495. gw := primary.Gateway
  496. if gw == "" {
  497. gw = m.engine.detectInterfaceGateway(primary.Interface)
  498. }
  499. return []ResolvedRouteNexthop{{
  500. Gateway: gw,
  501. Interface: primary.Interface,
  502. Weight: 1,
  503. }}, true
  504. case ModeLowestLatency:
  505. best := online[0]
  506. for _, mem := range online[1:] {
  507. if mem.LiveLatency > 0 && (best.LiveLatency <= 0 || mem.LiveLatency < best.LiveLatency) {
  508. best = mem
  509. }
  510. }
  511. gw := best.Gateway
  512. if gw == "" {
  513. gw = m.engine.detectInterfaceGateway(best.Interface)
  514. }
  515. return []ResolvedRouteNexthop{{
  516. Gateway: gw,
  517. Interface: best.Interface,
  518. Weight: 1,
  519. }}, true
  520. case ModeLowestLoss:
  521. best := online[0]
  522. for _, mem := range online[1:] {
  523. if mem.PacketLoss < best.PacketLoss {
  524. best = mem
  525. }
  526. }
  527. gw := best.Gateway
  528. if gw == "" {
  529. gw = m.engine.detectInterfaceGateway(best.Interface)
  530. }
  531. return []ResolvedRouteNexthop{{
  532. Gateway: gw,
  533. Interface: best.Interface,
  534. Weight: 1,
  535. }}, true
  536. case ModeWeighted, ModePCC:
  537. fallthrough
  538. default:
  539. var res []ResolvedRouteNexthop
  540. for _, mem := range online {
  541. gw := mem.Gateway
  542. if gw == "" {
  543. gw = m.engine.detectInterfaceGateway(mem.Interface)
  544. }
  545. w := mem.Weight
  546. if w <= 0 {
  547. w = 1
  548. }
  549. res = append(res, ResolvedRouteNexthop{
  550. Gateway: gw,
  551. Interface: mem.Interface,
  552. Weight: w,
  553. })
  554. }
  555. return res, true
  556. }
  557. }
  558. // ResolveGroupForRouting resolves an SD-WAN group name or ID via GlobalManager or file fallback.
  559. func ResolveGroupForRouting(groupNameOrID string) ([]ResolvedRouteNexthop, bool) {
  560. if GlobalManager != nil {
  561. return GlobalManager.ResolveGroupForRouting(groupNameOrID)
  562. }
  563. // Fallback to reading file if GlobalManager is not yet initialized
  564. data, err := os.ReadFile(DefaultStorePath)
  565. if err != nil {
  566. return nil, false
  567. }
  568. var groups []*SDWANGroup
  569. if err := json.Unmarshal(data, &groups); err != nil {
  570. return nil, false
  571. }
  572. for _, g := range groups {
  573. if strings.EqualFold(g.Name, groupNameOrID) || g.ID == groupNameOrID {
  574. if g.Disabled || len(g.Members) == 0 {
  575. return nil, false
  576. }
  577. var res []ResolvedRouteNexthop
  578. for _, m := range g.Members {
  579. if !m.Disabled && m.Interface != "" {
  580. res = append(res, ResolvedRouteNexthop{
  581. Gateway: m.Gateway,
  582. Interface: m.Interface,
  583. Weight: m.Weight,
  584. })
  585. }
  586. }
  587. return res, len(res) > 0
  588. }
  589. }
  590. return nil, false
  591. }
  592. // IsSDWANGroup reports whether the given name corresponds to an active or configured SD-WAN group.
  593. func IsSDWANGroup(name string) bool {
  594. clean := strings.TrimSpace(name)
  595. if clean == "" {
  596. return false
  597. }
  598. if GlobalManager != nil {
  599. return GlobalManager.Has(clean)
  600. }
  601. data, err := os.ReadFile(DefaultStorePath)
  602. if err != nil {
  603. return false
  604. }
  605. var groups []*SDWANGroup
  606. if err := json.Unmarshal(data, &groups); err != nil {
  607. return false
  608. }
  609. for _, g := range groups {
  610. if strings.EqualFold(g.Name, clean) || g.ID == clean {
  611. return true
  612. }
  613. }
  614. return false
  615. }