manager.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  1. package dynamicrouting
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "sort"
  10. "strings"
  11. "sync"
  12. "time"
  13. )
  14. const (
  15. defaultConfigPath = "/etc/proxmox-router/routing/dynamic.json"
  16. frrConfigPath = "/etc/frr/frr.conf"
  17. )
  18. type Manager struct {
  19. mu sync.RWMutex
  20. configPath string
  21. config DynamicConfig
  22. }
  23. func NewManager(configPath string) *Manager {
  24. if configPath == "" {
  25. configPath = defaultConfigPath
  26. }
  27. m := &Manager{
  28. configPath: configPath,
  29. }
  30. if err := m.load(); err != nil {
  31. log.Printf("[DynamicRouting] Config load warning: %v, initializing defaults", err)
  32. m.initDefaults()
  33. _ = m.save()
  34. }
  35. // Apply initial configuration to FRR
  36. if err := m.applyFRRConfig(); err != nil {
  37. log.Printf("[DynamicRouting] Initial FRR apply warning: %v", err)
  38. }
  39. return m
  40. }
  41. func (m *Manager) initDefaults() {
  42. m.config = DynamicConfig{
  43. BGPInstance: BGPInstance{
  44. Enabled: false,
  45. AS: 65001,
  46. RouterID: "192.168.131.56",
  47. HoldTime: 180,
  48. KeepaliveTime: 60,
  49. RedistributeConnected: false,
  50. RedistributeStatic: false,
  51. RedistributeOSPF: false,
  52. },
  53. BGPPeers: []BGPPeer{},
  54. BGPNetworks: []BGPNetwork{},
  55. OSPFInstance: OSPFInstance{
  56. Enabled: false,
  57. RouterID: "192.168.131.56",
  58. RedistributeConnected: false,
  59. RedistributeStatic: false,
  60. RedistributeBGP: false,
  61. DefaultMetric: 10,
  62. },
  63. OSPFAreas: []OSPFArea{
  64. {
  65. ID: "area-backbone",
  66. Name: "backbone",
  67. AreaID: "0.0.0.0",
  68. AreaType: "default",
  69. Comment: "Default OSPF Backbone Area",
  70. },
  71. },
  72. OSPFInterfaces: []OSPFInterface{},
  73. RoutingFilters: []RoutingFilterRule{},
  74. }
  75. }
  76. func (m *Manager) load() error {
  77. m.mu.Lock()
  78. defer m.mu.Unlock()
  79. data, err := os.ReadFile(m.configPath)
  80. if err != nil {
  81. return err
  82. }
  83. return json.Unmarshal(data, &m.config)
  84. }
  85. func (m *Manager) save() error {
  86. dir := filepath.Dir(m.configPath)
  87. if err := os.MkdirAll(dir, 0755); err != nil {
  88. return err
  89. }
  90. data, err := json.MarshalIndent(m.config, "", " ")
  91. if err != nil {
  92. return err
  93. }
  94. return os.WriteFile(m.configPath, data, 0644)
  95. }
  96. // applyFRRConfig generates /etc/frr/frr.conf and reloads FRR daemons
  97. func (m *Manager) applyFRRConfig() error {
  98. var sb strings.Builder
  99. sb.WriteString("frr version 10.3\n")
  100. sb.WriteString("frr defaults traditional\n")
  101. sb.WriteString("hostname WAN-Balancer\n")
  102. sb.WriteString("log syslog informational\n")
  103. sb.WriteString("service integrated-vtysh-config\n!\n")
  104. // 1. Interfaces section for OSPF (cost, timers, authentication)
  105. interfaceConfigs := make(map[string]bool)
  106. if m.config.OSPFInstance.Enabled {
  107. for _, iface := range m.config.OSPFInterfaces {
  108. if iface.Disabled || iface.Interface == "" || interfaceConfigs[iface.Interface] {
  109. continue
  110. }
  111. interfaceConfigs[iface.Interface] = true
  112. sb.WriteString(fmt.Sprintf("interface %s\n", iface.Interface))
  113. if iface.Cost > 0 {
  114. sb.WriteString(fmt.Sprintf(" ip ospf cost %d\n", iface.Cost))
  115. }
  116. if iface.Priority > 0 {
  117. sb.WriteString(fmt.Sprintf(" ip ospf priority %d\n", iface.Priority))
  118. }
  119. if iface.HelloInterval > 0 {
  120. sb.WriteString(fmt.Sprintf(" ip ospf hello-interval %d\n", iface.HelloInterval))
  121. }
  122. if iface.DeadInterval > 0 {
  123. sb.WriteString(fmt.Sprintf(" ip ospf dead-interval %d\n", iface.DeadInterval))
  124. }
  125. if iface.AuthType == "md5" && iface.AuthKey != "" {
  126. sb.WriteString(" ip ospf authentication message-digest\n")
  127. sb.WriteString(fmt.Sprintf(" ip ospf message-digest-key 1 md5 %s\n", iface.AuthKey))
  128. } else if iface.AuthType == "simple" && iface.AuthKey != "" {
  129. sb.WriteString(" ip ospf authentication\n")
  130. sb.WriteString(fmt.Sprintf(" ip ospf authentication-key %s\n", iface.AuthKey))
  131. }
  132. sb.WriteString("!\n")
  133. }
  134. }
  135. // 2. BGP Router Section
  136. if m.config.BGPInstance.Enabled && m.config.BGPInstance.AS > 0 {
  137. inst := m.config.BGPInstance
  138. sb.WriteString(fmt.Sprintf("router bgp %d\n", inst.AS))
  139. if inst.RouterID != "" {
  140. sb.WriteString(fmt.Sprintf(" bgp router-id %s\n", inst.RouterID))
  141. }
  142. keepalive := 60
  143. hold := 180
  144. if inst.KeepaliveTime > 0 {
  145. keepalive = inst.KeepaliveTime
  146. }
  147. if inst.HoldTime > 0 {
  148. hold = inst.HoldTime
  149. }
  150. sb.WriteString(fmt.Sprintf(" timers bgp %d %d\n", keepalive, hold))
  151. // Peers definition
  152. for _, peer := range m.config.BGPPeers {
  153. if peer.Disabled || peer.RemoteAddress == "" || peer.RemoteAS == 0 {
  154. continue
  155. }
  156. sb.WriteString(fmt.Sprintf(" neighbor %s remote-as %d\n", peer.RemoteAddress, peer.RemoteAS))
  157. if peer.Name != "" {
  158. sb.WriteString(fmt.Sprintf(" neighbor %s description \"%s\"\n", peer.RemoteAddress, peer.Name))
  159. }
  160. if peer.Multihop {
  161. sb.WriteString(fmt.Sprintf(" neighbor %s ebgp-multihop 255\n", peer.RemoteAddress))
  162. }
  163. if peer.Passive {
  164. sb.WriteString(fmt.Sprintf(" neighbor %s passive\n", peer.RemoteAddress))
  165. }
  166. if peer.Password != "" {
  167. sb.WriteString(fmt.Sprintf(" neighbor %s password %s\n", peer.RemoteAddress, peer.Password))
  168. }
  169. if peer.LocalAddress != "" {
  170. sb.WriteString(fmt.Sprintf(" neighbor %s update-source %s\n", peer.RemoteAddress, peer.LocalAddress))
  171. }
  172. }
  173. // Address Family IPv4 Unicast
  174. sb.WriteString(" address-family ipv4 unicast\n")
  175. // Advertised Networks
  176. for _, net := range m.config.BGPNetworks {
  177. if !net.Disabled && net.Network != "" {
  178. sb.WriteString(fmt.Sprintf(" network %s\n", net.Network))
  179. }
  180. }
  181. // Activate peers in IPv4 unicast
  182. for _, peer := range m.config.BGPPeers {
  183. if peer.Disabled || peer.RemoteAddress == "" || peer.RemoteAS == 0 {
  184. continue
  185. }
  186. sb.WriteString(fmt.Sprintf(" neighbor %s activate\n", peer.RemoteAddress))
  187. if peer.NexthopSelf {
  188. sb.WriteString(fmt.Sprintf(" neighbor %s next-hop-self\n", peer.RemoteAddress))
  189. }
  190. if peer.InFilter != "" {
  191. sb.WriteString(fmt.Sprintf(" neighbor %s route-map %s in\n", peer.RemoteAddress, peer.InFilter))
  192. }
  193. if peer.OutFilter != "" {
  194. sb.WriteString(fmt.Sprintf(" neighbor %s route-map %s out\n", peer.RemoteAddress, peer.OutFilter))
  195. }
  196. }
  197. // Redistribute
  198. if inst.RedistributeConnected {
  199. sb.WriteString(" redistribute connected\n")
  200. }
  201. if inst.RedistributeStatic {
  202. sb.WriteString(" redistribute static\n")
  203. }
  204. if inst.RedistributeOSPF {
  205. sb.WriteString(" redistribute ospf\n")
  206. }
  207. sb.WriteString(" exit-address-family\n!\n")
  208. }
  209. // 3. OSPF Router Section
  210. if m.config.OSPFInstance.Enabled {
  211. inst := m.config.OSPFInstance
  212. sb.WriteString("router ospf\n")
  213. if inst.RouterID != "" {
  214. sb.WriteString(fmt.Sprintf(" ospf router-id %s\n", inst.RouterID))
  215. }
  216. if inst.DefaultMetric > 0 {
  217. sb.WriteString(fmt.Sprintf(" default-metric %d\n", inst.DefaultMetric))
  218. }
  219. if inst.RedistributeConnected {
  220. sb.WriteString(" redistribute connected\n")
  221. }
  222. if inst.RedistributeStatic {
  223. sb.WriteString(" redistribute static\n")
  224. }
  225. if inst.RedistributeBGP {
  226. sb.WriteString(" redistribute bgp\n")
  227. }
  228. for _, iface := range m.config.OSPFInterfaces {
  229. if iface.Disabled || iface.Network == "" {
  230. continue
  231. }
  232. area := iface.Area
  233. if area == "" {
  234. area = "0.0.0.0"
  235. }
  236. sb.WriteString(fmt.Sprintf(" network %s area %s\n", iface.Network, area))
  237. if iface.Passive && iface.Interface != "" {
  238. sb.WriteString(fmt.Sprintf(" passive-interface %s\n", iface.Interface))
  239. }
  240. }
  241. sb.WriteString("!\n")
  242. }
  243. // 4. Routing Filters (Prefix lists, AS-Path ACLs, Community lists, Route Maps)
  244. chainRules := make(map[string][]RoutingFilterRule)
  245. for _, rule := range m.config.RoutingFilters {
  246. if rule.Disabled || strings.TrimSpace(rule.Chain) == "" {
  247. continue
  248. }
  249. chain := strings.TrimSpace(rule.Chain)
  250. chainRules[chain] = append(chainRules[chain], rule)
  251. }
  252. for chain, rules := range chainRules {
  253. // Sort rules by RuleOrder
  254. sort.Slice(rules, func(i, j int) bool {
  255. return rules[i].RuleOrder < rules[j].RuleOrder
  256. })
  257. safeChain := strings.ReplaceAll(chain, "-", "_")
  258. for _, rule := range rules {
  259. order := rule.RuleOrder
  260. if order <= 0 {
  261. order = 10
  262. }
  263. act := "permit"
  264. if rule.Action == "discard" || rule.Action == "reject" {
  265. act = "deny"
  266. }
  267. // Generate Prefix List if prefix specified
  268. pfxName := fmt.Sprintf("pfx_%s_%d", safeChain, order)
  269. if rule.Prefix != "" {
  270. pfxClause := formatPrefixListClause(rule.Action, rule.Prefix, rule.PrefixLen)
  271. sb.WriteString(fmt.Sprintf("ip prefix-list %s seq 5 %s\n", pfxName, pfxClause))
  272. }
  273. // Generate AS-Path ACL if specified
  274. asPathName := fmt.Sprintf("asp_%s_%d", safeChain, order)
  275. if rule.MatchASPath != "" {
  276. sb.WriteString(fmt.Sprintf("bgp as-path access-list %s permit %s\n", asPathName, rule.MatchASPath))
  277. }
  278. // Generate Community List if specified
  279. commName := fmt.Sprintf("comm_%s_%d", safeChain, order)
  280. if rule.MatchCommunity != "" {
  281. sb.WriteString(fmt.Sprintf("bgp community-list standard %s permit %s\n", commName, rule.MatchCommunity))
  282. }
  283. // Generate Route-Map
  284. sb.WriteString(fmt.Sprintf("route-map %s %s %d\n", chain, act, order))
  285. if rule.Prefix != "" {
  286. sb.WriteString(fmt.Sprintf(" match ip address prefix-list %s\n", pfxName))
  287. }
  288. if rule.MatchASPath != "" {
  289. sb.WriteString(fmt.Sprintf(" match as-path %s\n", asPathName))
  290. }
  291. if rule.MatchCommunity != "" {
  292. sb.WriteString(fmt.Sprintf(" match community %s\n", commName))
  293. }
  294. // Set modifications if action is permit/accept or passthrough
  295. if act == "permit" {
  296. if rule.SetLocalPref > 0 {
  297. sb.WriteString(fmt.Sprintf(" set local-preference %d\n", rule.SetLocalPref))
  298. }
  299. if rule.SetMetric > 0 {
  300. sb.WriteString(fmt.Sprintf(" set metric %d\n", rule.SetMetric))
  301. }
  302. if rule.SetASPrepend > 0 {
  303. sb.WriteString(fmt.Sprintf(" set as-path prepend last-as %d\n", rule.SetASPrepend))
  304. }
  305. if rule.SetCommunity != "" {
  306. sb.WriteString(fmt.Sprintf(" set community %s\n", rule.SetCommunity))
  307. }
  308. if rule.SetNextHop != "" {
  309. sb.WriteString(fmt.Sprintf(" set ip next-hop %s\n", rule.SetNextHop))
  310. }
  311. }
  312. sb.WriteString("!\n")
  313. }
  314. }
  315. sb.WriteString("end\n")
  316. // Write frr.conf
  317. if err := os.WriteFile(frrConfigPath, []byte(sb.String()), 0640); err != nil {
  318. return fmt.Errorf("failed to write %s: %w", frrConfigPath, err)
  319. }
  320. // Change ownership to frr:frr
  321. _ = exec.Command("chown", "frr:frr", frrConfigPath).Run()
  322. // Reload FRR using vtysh or systemctl
  323. out, err := exec.Command("vtysh", "-f", frrConfigPath).CombinedOutput()
  324. if err != nil {
  325. log.Printf("[DynamicRouting] vtysh -f reload warning: %v, output: %s", err, string(out))
  326. // fallback to systemctl reload frr
  327. _ = exec.Command("systemctl", "reload", "frr").Run()
  328. }
  329. return nil
  330. }
  331. func vtyshExec(cmd string) ([]byte, error) {
  332. return exec.Command("vtysh", "-c", cmd).Output()
  333. }
  334. // -------------------------------------------------------------
  335. // BGP API & Operations
  336. // -------------------------------------------------------------
  337. func (m *Manager) GetBGPInstance() BGPInstance {
  338. m.mu.RLock()
  339. defer m.mu.RUnlock()
  340. inst := m.config.BGPInstance
  341. // Check if BGP is currently running in FRR
  342. out, err := vtyshExec("show ip bgp summary json")
  343. if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
  344. inst.Running = true
  345. }
  346. return inst
  347. }
  348. func (m *Manager) UpdateBGPInstance(inst BGPInstance) error {
  349. m.mu.Lock()
  350. m.config.BGPInstance = inst
  351. _ = m.save()
  352. m.mu.Unlock()
  353. return m.applyFRRConfig()
  354. }
  355. func (m *Manager) ListBGPPeers() []BGPPeer {
  356. m.mu.RLock()
  357. defer m.mu.RUnlock()
  358. // Make a copy of configured peers
  359. peers := make([]BGPPeer, len(m.config.BGPPeers))
  360. copy(peers, m.config.BGPPeers)
  361. // Fetch live peer telemetry from vtysh
  362. out, err := vtyshExec("show ip bgp summary json")
  363. if err == nil {
  364. var summary struct {
  365. IPv4Unicast struct {
  366. Peers map[string]struct {
  367. RemoteAs uint32 `json:"remoteAs"`
  368. State string `json:"state"`
  369. PeerUptime string `json:"peerUptime"`
  370. PfxRcd int `json:"pfxRcd"`
  371. PfxSnt int `json:"pfxSnt"`
  372. } `json:"peers"`
  373. } `json:"ipv4Unicast"`
  374. }
  375. if err := json.Unmarshal(out, &summary); err == nil && summary.IPv4Unicast.Peers != nil {
  376. for i := range peers {
  377. if live, ok := summary.IPv4Unicast.Peers[peers[i].RemoteAddress]; ok {
  378. peers[i].State = live.State
  379. peers[i].Uptime = live.PeerUptime
  380. peers[i].PrefixReceived = live.PfxRcd
  381. peers[i].PrefixSent = live.PfxSnt
  382. } else if peers[i].Disabled {
  383. peers[i].State = "Disabled"
  384. } else {
  385. peers[i].State = "Idle"
  386. }
  387. }
  388. }
  389. }
  390. return peers
  391. }
  392. func (m *Manager) SaveBGPPeer(peer BGPPeer) error {
  393. m.mu.Lock()
  394. defer m.mu.Unlock()
  395. if peer.ID == "" {
  396. peer.ID = fmt.Sprintf("bgp-peer-%d", time.Now().UnixNano())
  397. }
  398. found := false
  399. for i, p := range m.config.BGPPeers {
  400. if p.ID == peer.ID {
  401. m.config.BGPPeers[i] = peer
  402. found = true
  403. break
  404. }
  405. }
  406. if !found {
  407. m.config.BGPPeers = append(m.config.BGPPeers, peer)
  408. }
  409. if err := m.save(); err != nil {
  410. return err
  411. }
  412. return m.applyFRRConfig()
  413. }
  414. func (m *Manager) DeleteBGPPeer(id string) error {
  415. m.mu.Lock()
  416. defer m.mu.Unlock()
  417. newPeers := make([]BGPPeer, 0, len(m.config.BGPPeers))
  418. for _, p := range m.config.BGPPeers {
  419. if p.ID != id {
  420. newPeers = append(newPeers, p)
  421. }
  422. }
  423. m.config.BGPPeers = newPeers
  424. if err := m.save(); err != nil {
  425. return err
  426. }
  427. return m.applyFRRConfig()
  428. }
  429. func (m *Manager) SetBGPPeerState(id string, disabled bool) error {
  430. m.mu.Lock()
  431. defer m.mu.Unlock()
  432. for i := range m.config.BGPPeers {
  433. if m.config.BGPPeers[i].ID == id {
  434. m.config.BGPPeers[i].Disabled = disabled
  435. break
  436. }
  437. }
  438. if err := m.save(); err != nil {
  439. return err
  440. }
  441. return m.applyFRRConfig()
  442. }
  443. func (m *Manager) ListBGPNetworks() []BGPNetwork {
  444. m.mu.RLock()
  445. defer m.mu.RUnlock()
  446. return m.config.BGPNetworks
  447. }
  448. func (m *Manager) SaveBGPNetwork(net BGPNetwork) error {
  449. m.mu.Lock()
  450. defer m.mu.Unlock()
  451. if net.ID == "" {
  452. net.ID = fmt.Sprintf("bgp-net-%d", time.Now().UnixNano())
  453. }
  454. found := false
  455. for i, n := range m.config.BGPNetworks {
  456. if n.ID == net.ID {
  457. m.config.BGPNetworks[i] = net
  458. found = true
  459. break
  460. }
  461. }
  462. if !found {
  463. m.config.BGPNetworks = append(m.config.BGPNetworks, net)
  464. }
  465. if err := m.save(); err != nil {
  466. return err
  467. }
  468. return m.applyFRRConfig()
  469. }
  470. func (m *Manager) DeleteBGPNetwork(id string) error {
  471. m.mu.Lock()
  472. defer m.mu.Unlock()
  473. newNets := make([]BGPNetwork, 0, len(m.config.BGPNetworks))
  474. for _, n := range m.config.BGPNetworks {
  475. if n.ID != id {
  476. newNets = append(newNets, n)
  477. }
  478. }
  479. m.config.BGPNetworks = newNets
  480. if err := m.save(); err != nil {
  481. return err
  482. }
  483. return m.applyFRRConfig()
  484. }
  485. func (m *Manager) GetBGPRoutes() []BGPRoute {
  486. var routes []BGPRoute
  487. out, err := vtyshExec("show ip bgp json")
  488. if err != nil {
  489. return routes
  490. }
  491. var parsed struct {
  492. Routes map[string][]struct {
  493. Metric int `json:"metric"`
  494. Weight int `json:"weight"`
  495. Path string `json:"path"`
  496. Origin string `json:"origin"`
  497. Nexthops []struct {
  498. IP string `json:"ip"`
  499. Used bool `json:"used"`
  500. } `json:"nexthops"`
  501. } `json:"routes"`
  502. }
  503. if err := json.Unmarshal(out, &parsed); err == nil && parsed.Routes != nil {
  504. for prefix, items := range parsed.Routes {
  505. for _, item := range items {
  506. nh := ""
  507. if len(item.Nexthops) > 0 {
  508. nh = item.Nexthops[0].IP
  509. }
  510. routes = append(routes, BGPRoute{
  511. Prefix: prefix,
  512. NextHop: nh,
  513. Metric: item.Metric,
  514. Weight: item.Weight,
  515. Path: item.Path,
  516. Origin: item.Origin,
  517. Best: true,
  518. })
  519. }
  520. }
  521. }
  522. return routes
  523. }
  524. // -------------------------------------------------------------
  525. // OSPF API & Operations
  526. // -------------------------------------------------------------
  527. func (m *Manager) GetOSPFInstance() OSPFInstance {
  528. m.mu.RLock()
  529. defer m.mu.RUnlock()
  530. inst := m.config.OSPFInstance
  531. out, err := vtyshExec("show ip ospf json")
  532. if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
  533. inst.Running = true
  534. }
  535. return inst
  536. }
  537. func (m *Manager) UpdateOSPFInstance(inst OSPFInstance) error {
  538. m.mu.Lock()
  539. m.config.OSPFInstance = inst
  540. _ = m.save()
  541. m.mu.Unlock()
  542. return m.applyFRRConfig()
  543. }
  544. func (m *Manager) ListOSPFAreas() []OSPFArea {
  545. m.mu.RLock()
  546. defer m.mu.RUnlock()
  547. return m.config.OSPFAreas
  548. }
  549. func (m *Manager) SaveOSPFArea(area OSPFArea) error {
  550. m.mu.Lock()
  551. defer m.mu.Unlock()
  552. if area.ID == "" {
  553. area.ID = fmt.Sprintf("area-%d", time.Now().UnixNano())
  554. }
  555. found := false
  556. for i, a := range m.config.OSPFAreas {
  557. if a.ID == area.ID {
  558. m.config.OSPFAreas[i] = area
  559. found = true
  560. break
  561. }
  562. }
  563. if !found {
  564. m.config.OSPFAreas = append(m.config.OSPFAreas, area)
  565. }
  566. if err := m.save(); err != nil {
  567. return err
  568. }
  569. return m.applyFRRConfig()
  570. }
  571. func (m *Manager) DeleteOSPFArea(id string) error {
  572. m.mu.Lock()
  573. defer m.mu.Unlock()
  574. newAreas := make([]OSPFArea, 0, len(m.config.OSPFAreas))
  575. for _, a := range m.config.OSPFAreas {
  576. if a.ID != id {
  577. newAreas = append(newAreas, a)
  578. }
  579. }
  580. m.config.OSPFAreas = newAreas
  581. if err := m.save(); err != nil {
  582. return err
  583. }
  584. return m.applyFRRConfig()
  585. }
  586. func (m *Manager) ListOSPFInterfaces() []OSPFInterface {
  587. m.mu.RLock()
  588. defer m.mu.RUnlock()
  589. return m.config.OSPFInterfaces
  590. }
  591. func (m *Manager) SaveOSPFInterface(iface OSPFInterface) error {
  592. m.mu.Lock()
  593. defer m.mu.Unlock()
  594. if iface.ID == "" {
  595. iface.ID = fmt.Sprintf("ospf-iface-%d", time.Now().UnixNano())
  596. }
  597. if iface.Cost <= 0 {
  598. iface.Cost = 10
  599. }
  600. if iface.Priority <= 0 {
  601. iface.Priority = 1
  602. }
  603. if iface.HelloInterval <= 0 {
  604. iface.HelloInterval = 10
  605. }
  606. if iface.DeadInterval <= 0 {
  607. iface.DeadInterval = 40
  608. }
  609. found := false
  610. for i, item := range m.config.OSPFInterfaces {
  611. if item.ID == iface.ID {
  612. m.config.OSPFInterfaces[i] = iface
  613. found = true
  614. break
  615. }
  616. }
  617. if !found {
  618. m.config.OSPFInterfaces = append(m.config.OSPFInterfaces, iface)
  619. }
  620. if err := m.save(); err != nil {
  621. return err
  622. }
  623. return m.applyFRRConfig()
  624. }
  625. func (m *Manager) DeleteOSPFInterface(id string) error {
  626. m.mu.Lock()
  627. defer m.mu.Unlock()
  628. newInterfaces := make([]OSPFInterface, 0, len(m.config.OSPFInterfaces))
  629. for _, item := range m.config.OSPFInterfaces {
  630. if item.ID != id {
  631. newInterfaces = append(newInterfaces, item)
  632. }
  633. }
  634. m.config.OSPFInterfaces = newInterfaces
  635. if err := m.save(); err != nil {
  636. return err
  637. }
  638. return m.applyFRRConfig()
  639. }
  640. func (m *Manager) SetOSPFInterfaceState(id string, disabled bool) error {
  641. m.mu.Lock()
  642. defer m.mu.Unlock()
  643. for i := range m.config.OSPFInterfaces {
  644. if m.config.OSPFInterfaces[i].ID == id {
  645. m.config.OSPFInterfaces[i].Disabled = disabled
  646. break
  647. }
  648. }
  649. if err := m.save(); err != nil {
  650. return err
  651. }
  652. return m.applyFRRConfig()
  653. }
  654. func (m *Manager) GetOSPFNeighbors() []OSPFNeighbor {
  655. var neighbors []OSPFNeighbor
  656. out, err := vtyshExec("show ip ospf neighbor json")
  657. if err != nil {
  658. return neighbors
  659. }
  660. var parsed struct {
  661. Neighbors map[string][]struct {
  662. NbrState string `json:"nbrState"`
  663. Address string `json:"address"`
  664. IfaceName string `json:"ifaceName"`
  665. Area string `json:"area"`
  666. DeadTime string `json:"deadTime"`
  667. } `json:"neighbors"`
  668. }
  669. if err := json.Unmarshal(out, &parsed); err == nil && parsed.Neighbors != nil {
  670. for nbrID, entries := range parsed.Neighbors {
  671. for _, entry := range entries {
  672. neighbors = append(neighbors, OSPFNeighbor{
  673. NeighborID: nbrID,
  674. Address: entry.Address,
  675. Interface: entry.IfaceName,
  676. Area: entry.Area,
  677. State: entry.NbrState,
  678. DeadTimer: entry.DeadTime,
  679. })
  680. }
  681. }
  682. }
  683. return neighbors
  684. }
  685. func (m *Manager) GetOSPFRoutes() []OSPFRoute {
  686. var routes []OSPFRoute
  687. out, err := vtyshExec("show ip ospf route json")
  688. if err != nil {
  689. return routes
  690. }
  691. var parsed map[string]struct {
  692. RouteType string `json:"routeType"`
  693. Cost int `json:"cost"`
  694. Area string `json:"area"`
  695. Nexthops []struct {
  696. IP string `json:"ip"`
  697. DirectlyAttachedTo string `json:"directlyAttachedTo"`
  698. } `json:"nexthops"`
  699. }
  700. if err := json.Unmarshal(out, &parsed); err == nil {
  701. for prefix, r := range parsed {
  702. nh := ""
  703. dev := ""
  704. if len(r.Nexthops) > 0 {
  705. nh = strings.TrimSpace(r.Nexthops[0].IP)
  706. dev = r.Nexthops[0].DirectlyAttachedTo
  707. }
  708. routes = append(routes, OSPFRoute{
  709. Prefix: prefix,
  710. RouteType: r.RouteType,
  711. Cost: r.Cost,
  712. Area: r.Area,
  713. NextHop: nh,
  714. Interface: dev,
  715. })
  716. }
  717. }
  718. return routes
  719. }
  720. // -------------------------------------------------------------
  721. // Summary Telemetry
  722. // -------------------------------------------------------------
  723. func (m *Manager) GetSummary() DynamicRoutingSummary {
  724. m.mu.RLock()
  725. defer m.mu.RUnlock()
  726. summary := DynamicRoutingSummary{
  727. BGPEnabled: m.config.BGPInstance.Enabled,
  728. OSPFEnabled: m.config.OSPFInstance.Enabled,
  729. }
  730. // Check BGP status
  731. if summary.BGPEnabled {
  732. out, err := vtyshExec("show ip bgp summary json")
  733. if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
  734. summary.BGPRunning = true
  735. var bgpParsed struct {
  736. IPv4Unicast struct {
  737. PeerCount int `json:"peerCount"`
  738. Peers map[string]struct {
  739. State string `json:"state"`
  740. } `json:"peers"`
  741. } `json:"ipv4Unicast"`
  742. }
  743. if err := json.Unmarshal(out, &bgpParsed); err == nil {
  744. summary.BGPPeerCount = len(m.config.BGPPeers)
  745. for _, p := range bgpParsed.IPv4Unicast.Peers {
  746. if p.State == "Established" {
  747. summary.BGPEstablishedCount++
  748. }
  749. }
  750. }
  751. }
  752. }
  753. // Check OSPF status
  754. if summary.OSPFEnabled {
  755. out, err := vtyshExec("show ip ospf json")
  756. if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") {
  757. summary.OSPFRunning = true
  758. }
  759. nbrs := m.GetOSPFNeighbors()
  760. summary.OSPFNeighborCount = len(nbrs)
  761. for _, n := range nbrs {
  762. if strings.HasPrefix(n.State, "Full") {
  763. summary.OSPFAdjacentCount++
  764. }
  765. }
  766. }
  767. summary.RoutingFilterCount = len(m.config.RoutingFilters)
  768. return summary
  769. }
  770. func formatPrefixListClause(action, prefix, prefixLen string) string {
  771. act := "permit"
  772. if action == "discard" || action == "reject" {
  773. act = "deny"
  774. }
  775. clause := fmt.Sprintf("%s %s", act, prefix)
  776. prefixLen = strings.TrimSpace(prefixLen)
  777. if prefixLen != "" {
  778. if strings.Contains(prefixLen, "-") {
  779. parts := strings.Split(prefixLen, "-")
  780. if len(parts) == 2 {
  781. clause += fmt.Sprintf(" ge %s le %s", strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
  782. }
  783. } else if strings.HasPrefix(prefixLen, "ge ") || strings.HasPrefix(prefixLen, "le ") {
  784. clause += " " + prefixLen
  785. } else {
  786. clause += fmt.Sprintf(" ge %s le %s", prefixLen, prefixLen)
  787. }
  788. }
  789. return clause
  790. }
  791. // -------------------------------------------------------------
  792. // Routing Filter API & Operations (/routing filter)
  793. // -------------------------------------------------------------
  794. func (m *Manager) ListRoutingFilters() []RoutingFilterRule {
  795. m.mu.RLock()
  796. defer m.mu.RUnlock()
  797. filters := make([]RoutingFilterRule, len(m.config.RoutingFilters))
  798. copy(filters, m.config.RoutingFilters)
  799. return filters
  800. }
  801. func (m *Manager) SaveRoutingFilter(rule *RoutingFilterRule) error {
  802. m.mu.Lock()
  803. if rule.ID == "" {
  804. rule.ID = fmt.Sprintf("rf-%d", time.Now().UnixNano()/1e6)
  805. }
  806. if rule.RuleOrder <= 0 {
  807. rule.RuleOrder = (len(m.config.RoutingFilters) + 1) * 10
  808. }
  809. found := false
  810. for i, r := range m.config.RoutingFilters {
  811. if r.ID == rule.ID {
  812. m.config.RoutingFilters[i] = *rule
  813. found = true
  814. break
  815. }
  816. }
  817. if !found {
  818. m.config.RoutingFilters = append(m.config.RoutingFilters, *rule)
  819. }
  820. _ = m.save()
  821. m.mu.Unlock()
  822. return m.applyFRRConfig()
  823. }
  824. func (m *Manager) DeleteRoutingFilter(id string) error {
  825. m.mu.Lock()
  826. newRules := make([]RoutingFilterRule, 0, len(m.config.RoutingFilters))
  827. for _, r := range m.config.RoutingFilters {
  828. if r.ID != id {
  829. newRules = append(newRules, r)
  830. }
  831. }
  832. m.config.RoutingFilters = newRules
  833. _ = m.save()
  834. m.mu.Unlock()
  835. return m.applyFRRConfig()
  836. }
  837. func (m *Manager) SetRoutingFilterState(id string, disabled bool) error {
  838. m.mu.Lock()
  839. for i := range m.config.RoutingFilters {
  840. if m.config.RoutingFilters[i].ID == id {
  841. m.config.RoutingFilters[i].Disabled = disabled
  842. break
  843. }
  844. }
  845. _ = m.save()
  846. m.mu.Unlock()
  847. return m.applyFRRConfig()
  848. }