manager.go 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133
  1. package dynamicrouting
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "regexp"
  10. "sort"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "time"
  15. )
  16. const (
  17. defaultConfigPath = "/etc/proxmox-router/routing/dynamic.json"
  18. birdConfigPath = "/etc/bird/bird.conf"
  19. )
  20. type Manager struct {
  21. mu sync.RWMutex
  22. configPath string
  23. config DynamicConfig
  24. }
  25. func NewManager(configPath string) *Manager {
  26. if configPath == "" {
  27. configPath = defaultConfigPath
  28. }
  29. m := &Manager{
  30. configPath: configPath,
  31. }
  32. if err := m.load(); err != nil {
  33. log.Printf("[DynamicRouting] Config load warning: %v, initializing defaults", err)
  34. m.initDefaults()
  35. _ = m.save()
  36. }
  37. // Apply initial configuration to BIRD 2
  38. if err := m.applyBirdConfig(); err != nil {
  39. log.Printf("[DynamicRouting] Initial BIRD apply warning: %v", err)
  40. }
  41. return m
  42. }
  43. func (m *Manager) initDefaults() {
  44. m.config = DynamicConfig{
  45. BGPInstance: BGPInstance{
  46. Enabled: false,
  47. AS: 65001,
  48. RouterID: "192.168.131.56",
  49. HoldTime: 180,
  50. KeepaliveTime: 60,
  51. RedistributeConnected: false,
  52. RedistributeStatic: false,
  53. RedistributeOSPF: false,
  54. },
  55. BGPPeers: []BGPPeer{},
  56. BGPNetworks: []BGPNetwork{},
  57. OSPFInstance: OSPFInstance{
  58. Enabled: false,
  59. RouterID: "192.168.131.56",
  60. RedistributeConnected: false,
  61. RedistributeStatic: false,
  62. RedistributeBGP: false,
  63. DefaultMetric: 10,
  64. },
  65. OSPFAreas: []OSPFArea{
  66. {
  67. ID: "area-backbone",
  68. Name: "backbone",
  69. AreaID: "0.0.0.0",
  70. AreaType: "default",
  71. Comment: "Default OSPF Backbone Area",
  72. },
  73. },
  74. OSPFInterfaces: []OSPFInterface{},
  75. RoutingFilters: []RoutingFilterRule{},
  76. }
  77. }
  78. func (m *Manager) load() error {
  79. m.mu.Lock()
  80. defer m.mu.Unlock()
  81. data, err := os.ReadFile(m.configPath)
  82. if err != nil {
  83. return err
  84. }
  85. return json.Unmarshal(data, &m.config)
  86. }
  87. func (m *Manager) save() error {
  88. dir := filepath.Dir(m.configPath)
  89. if err := os.MkdirAll(dir, 0755); err != nil {
  90. return err
  91. }
  92. data, err := json.MarshalIndent(m.config, "", " ")
  93. if err != nil {
  94. return err
  95. }
  96. return os.WriteFile(m.configPath, data, 0644)
  97. }
  98. // sanitizeIdentifier creates a valid BIRD protocol/function identifier: [a-zA-Z][a-zA-Z0-9_]*
  99. func sanitizeIdentifier(prefix, raw string) string {
  100. reg := regexp.MustCompile("[^a-zA-Z0-9_]+")
  101. cleaned := reg.ReplaceAllString(raw, "_")
  102. cleaned = strings.Trim(cleaned, "_")
  103. if cleaned == "" {
  104. cleaned = "inst"
  105. }
  106. res := prefix + "_" + cleaned
  107. if len(res) > 32 {
  108. res = res[:32]
  109. }
  110. return res
  111. }
  112. // applyBirdConfig generates /etc/bird/bird.conf and live reloads BIRD daemon
  113. func (m *Manager) applyBirdConfig() error {
  114. var sb strings.Builder
  115. routerID := "192.168.131.56"
  116. if m.config.BGPInstance.RouterID != "" {
  117. routerID = m.config.BGPInstance.RouterID
  118. } else if m.config.OSPFInstance.RouterID != "" {
  119. routerID = m.config.OSPFInstance.RouterID
  120. }
  121. sb.WriteString("# /etc/bird/bird.conf\n")
  122. sb.WriteString("# Generated dynamically by Proxmox Router (BIRD 2 Engine)\n\n")
  123. sb.WriteString("log syslog all;\n")
  124. sb.WriteString(fmt.Sprintf("router id %s;\n\n", routerID))
  125. // Base system protocols
  126. sb.WriteString("protocol device {\n")
  127. sb.WriteString(" scan time 10;\n")
  128. sb.WriteString("}\n\n")
  129. sb.WriteString("protocol direct {\n")
  130. sb.WriteString(" ipv4;\n")
  131. sb.WriteString("}\n\n")
  132. sb.WriteString("protocol kernel {\n")
  133. sb.WriteString(" ipv4 {\n")
  134. sb.WriteString(" export all;\n")
  135. sb.WriteString(" };\n")
  136. sb.WriteString(" learn;\n")
  137. sb.WriteString("}\n\n")
  138. // Static BGP Networks announcements
  139. var activeNets []string
  140. if m.config.BGPInstance.Enabled {
  141. for _, net := range m.config.BGPNetworks {
  142. if !net.Disabled && strings.TrimSpace(net.Network) != "" {
  143. activeNets = append(activeNets, strings.TrimSpace(net.Network))
  144. }
  145. }
  146. }
  147. if len(activeNets) > 0 {
  148. sb.WriteString("protocol static bgp_static_nets {\n")
  149. sb.WriteString(" ipv4;\n")
  150. for _, n := range activeNets {
  151. sb.WriteString(fmt.Sprintf(" route %s blackhole;\n", n))
  152. }
  153. sb.WriteString("}\n\n")
  154. }
  155. // Routing Filters (/routing filter)
  156. chainRules := make(map[string][]RoutingFilterRule)
  157. for _, rule := range m.config.RoutingFilters {
  158. if rule.Disabled || strings.TrimSpace(rule.Chain) == "" {
  159. continue
  160. }
  161. chain := strings.TrimSpace(rule.Chain)
  162. chainRules[chain] = append(chainRules[chain], rule)
  163. }
  164. for chain, rules := range chainRules {
  165. sort.Slice(rules, func(i, j int) bool {
  166. return rules[i].RuleOrder < rules[j].RuleOrder
  167. })
  168. filterName := sanitizeIdentifier("filter", chain)
  169. sb.WriteString(fmt.Sprintf("filter %s {\n", filterName))
  170. for _, r := range rules {
  171. var conditions []string
  172. if r.Prefix != "" {
  173. pfx := strings.TrimSpace(r.Prefix)
  174. pfxLen := strings.TrimSpace(r.PrefixLen)
  175. if pfxLen != "" {
  176. if strings.Contains(pfxLen, "-") {
  177. parts := strings.Split(pfxLen, "-")
  178. if len(parts) == 2 {
  179. conditions = append(conditions, fmt.Sprintf("net ~ [ %s{%s,%s} ]", pfx, strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])))
  180. }
  181. } else {
  182. conditions = append(conditions, fmt.Sprintf("net ~ [ %s{%s,32} ]", pfx, pfxLen))
  183. }
  184. } else {
  185. conditions = append(conditions, fmt.Sprintf("net ~ [ %s+ ]", pfx))
  186. }
  187. }
  188. if r.MatchASPath != "" {
  189. conditions = append(conditions, fmt.Sprintf("bgp_path ~ [= %s =]", strings.TrimSpace(r.MatchASPath)))
  190. }
  191. if r.MatchCommunity != "" {
  192. comm := strings.TrimSpace(r.MatchCommunity)
  193. if strings.Contains(comm, ":") {
  194. parts := strings.Split(comm, ":")
  195. conditions = append(conditions, fmt.Sprintf("(%s, %s) ~ bgp_community", parts[0], parts[1]))
  196. }
  197. }
  198. condStr := "true"
  199. if len(conditions) > 0 {
  200. condStr = strings.Join(conditions, " && ")
  201. }
  202. sb.WriteString(fmt.Sprintf(" if %s then {\n", condStr))
  203. if r.SetLocalPref > 0 {
  204. sb.WriteString(fmt.Sprintf(" bgp_local_pref = %d;\n", r.SetLocalPref))
  205. }
  206. if r.SetMetric > 0 {
  207. sb.WriteString(fmt.Sprintf(" bgp_med = %d;\n", r.SetMetric))
  208. }
  209. if r.SetCommunity != "" {
  210. comm := strings.TrimSpace(r.SetCommunity)
  211. if strings.Contains(comm, ":") {
  212. parts := strings.Split(comm, ":")
  213. sb.WriteString(fmt.Sprintf(" bgp_community.add((%s, %s));\n", parts[0], parts[1]))
  214. }
  215. }
  216. if r.SetNextHop != "" {
  217. sb.WriteString(fmt.Sprintf(" bgp_next_hop = %s;\n", strings.TrimSpace(r.SetNextHop)))
  218. }
  219. if r.Action == "discard" || r.Action == "reject" {
  220. sb.WriteString(" reject;\n")
  221. } else {
  222. sb.WriteString(" accept;\n")
  223. }
  224. sb.WriteString(" }\n")
  225. }
  226. sb.WriteString(" accept;\n")
  227. sb.WriteString("}\n\n")
  228. }
  229. // BGP Protocols (True Multi-Instance / Multi-AS support!)
  230. if m.config.BGPInstance.Enabled {
  231. for i, peer := range m.config.BGPPeers {
  232. if peer.RemoteAddress == "" || peer.RemoteAS == 0 {
  233. continue
  234. }
  235. // Local AS determination: peer.LocalAS if set, otherwise global AS
  236. localAS := m.config.BGPInstance.AS
  237. if peer.LocalAS > 0 {
  238. localAS = peer.LocalAS
  239. }
  240. protoName := sanitizeIdentifier("bgp", fmt.Sprintf("%s_%d", peer.ID, i))
  241. sb.WriteString(fmt.Sprintf("protocol bgp %s {\n", protoName))
  242. if peer.Name != "" {
  243. sb.WriteString(fmt.Sprintf(" description \"%s\";\n", peer.Name))
  244. }
  245. if peer.Disabled {
  246. sb.WriteString(" disabled;\n")
  247. }
  248. if peer.LocalAddress != "" {
  249. sb.WriteString(fmt.Sprintf(" local %s as %d;\n", peer.LocalAddress, localAS))
  250. } else {
  251. sb.WriteString(fmt.Sprintf(" local as %d;\n", localAS))
  252. }
  253. sb.WriteString(fmt.Sprintf(" neighbor %s as %d;\n", peer.RemoteAddress, peer.RemoteAS))
  254. if peer.Multihop {
  255. sb.WriteString(" multihop;\n")
  256. }
  257. if peer.Passive {
  258. sb.WriteString(" passive;\n")
  259. }
  260. if peer.Password != "" {
  261. sb.WriteString(fmt.Sprintf(" password \"%s\";\n", peer.Password))
  262. }
  263. holdTime := 180
  264. if m.config.BGPInstance.HoldTime > 0 {
  265. holdTime = m.config.BGPInstance.HoldTime
  266. }
  267. sb.WriteString(fmt.Sprintf(" hold time %d;\n\n", holdTime))
  268. // IPv4 Channel
  269. sb.WriteString(" ipv4 {\n")
  270. if peer.NexthopSelf {
  271. sb.WriteString(" next hop self;\n")
  272. }
  273. // Import Filter
  274. if peer.InFilter != "" && len(chainRules[peer.InFilter]) > 0 {
  275. sb.WriteString(fmt.Sprintf(" import filter %s;\n", sanitizeIdentifier("filter", peer.InFilter)))
  276. } else {
  277. sb.WriteString(" import all;\n")
  278. }
  279. // Export Filter
  280. if peer.OutFilter != "" && len(chainRules[peer.OutFilter]) > 0 {
  281. sb.WriteString(fmt.Sprintf(" export filter %s;\n", sanitizeIdentifier("filter", peer.OutFilter)))
  282. } else {
  283. var exportSources []string
  284. if len(activeNets) > 0 {
  285. exportSources = append(exportSources, `proto = "bgp_static_nets"`)
  286. }
  287. exportSources = append(exportSources, `source = RTS_BGP`)
  288. if m.config.BGPInstance.RedistributeConnected {
  289. exportSources = append(exportSources, `source = RTS_DEVICE`)
  290. }
  291. if m.config.BGPInstance.RedistributeStatic {
  292. exportSources = append(exportSources, `source = RTS_STATIC`)
  293. }
  294. if m.config.BGPInstance.RedistributeOSPF {
  295. exportSources = append(exportSources, `source = RTS_OSPF`)
  296. }
  297. if len(exportSources) > 0 {
  298. sb.WriteString(fmt.Sprintf(" export where (%s);\n", strings.Join(exportSources, ") || (")))
  299. } else {
  300. sb.WriteString(" export none;\n")
  301. }
  302. }
  303. sb.WriteString(" };\n")
  304. sb.WriteString("}\n\n")
  305. }
  306. }
  307. // OSPFv2 Protocol
  308. if m.config.OSPFInstance.Enabled {
  309. ospfRouterID := routerID
  310. if m.config.OSPFInstance.RouterID != "" {
  311. ospfRouterID = m.config.OSPFInstance.RouterID
  312. }
  313. sb.WriteString("protocol ospf v2 ospf_main {\n")
  314. sb.WriteString(fmt.Sprintf(" router id %s;\n", ospfRouterID))
  315. sb.WriteString(" ipv4 {\n")
  316. sb.WriteString(" import all;\n")
  317. var ospfExports []string
  318. ospfExports = append(ospfExports, "RTS_STATIC")
  319. if m.config.OSPFInstance.RedistributeConnected {
  320. ospfExports = append(ospfExports, "RTS_DEVICE")
  321. }
  322. if m.config.OSPFInstance.RedistributeBGP {
  323. ospfExports = append(ospfExports, "RTS_BGP")
  324. }
  325. if len(ospfExports) > 0 {
  326. sb.WriteString(fmt.Sprintf(" export where source ~ [ %s ];\n", strings.Join(ospfExports, ", ")))
  327. } else {
  328. sb.WriteString(" export none;\n")
  329. }
  330. sb.WriteString(" };\n\n")
  331. // Group interfaces by area
  332. areaIfaces := make(map[string][]OSPFInterface)
  333. for _, iface := range m.config.OSPFInterfaces {
  334. if iface.Disabled {
  335. continue
  336. }
  337. area := strings.TrimSpace(iface.Area)
  338. if area == "" {
  339. area = "0.0.0.0"
  340. }
  341. areaIfaces[area] = append(areaIfaces[area], iface)
  342. }
  343. // Ensure backbone area always exists
  344. if _, ok := areaIfaces["0.0.0.0"]; !ok {
  345. areaIfaces["0.0.0.0"] = []OSPFInterface{}
  346. }
  347. areas := m.config.OSPFAreas
  348. if len(areas) == 0 {
  349. areas = []OSPFArea{
  350. {
  351. Name: "backbone",
  352. AreaID: "0.0.0.0",
  353. AreaType: "default",
  354. },
  355. }
  356. }
  357. for _, area := range areas {
  358. areaID := strings.TrimSpace(area.AreaID)
  359. if areaID == "" {
  360. areaID = "0.0.0.0"
  361. }
  362. ifaces := areaIfaces[areaID]
  363. sb.WriteString(fmt.Sprintf(" area %s {\n", areaID))
  364. if area.AreaType == "stub" {
  365. sb.WriteString(" stub;\n")
  366. } else if area.AreaType == "nssa" {
  367. sb.WriteString(" nssa;\n")
  368. }
  369. for _, iface := range ifaces {
  370. target := iface.Interface
  371. if target == "" {
  372. target = iface.Network
  373. }
  374. if target == "" {
  375. continue
  376. }
  377. sb.WriteString(fmt.Sprintf(" interface \"%s\" {\n", target))
  378. if iface.Cost > 0 {
  379. sb.WriteString(fmt.Sprintf(" cost %d;\n", iface.Cost))
  380. }
  381. if iface.Priority > 0 {
  382. sb.WriteString(fmt.Sprintf(" priority %d;\n", iface.Priority))
  383. }
  384. if iface.HelloInterval > 0 {
  385. sb.WriteString(fmt.Sprintf(" hello %d;\n", iface.HelloInterval))
  386. }
  387. if iface.DeadInterval > 0 {
  388. sb.WriteString(fmt.Sprintf(" dead %d;\n", iface.DeadInterval))
  389. }
  390. if iface.Passive {
  391. sb.WriteString(" stub;\n")
  392. } else {
  393. sb.WriteString(" type broadcast;\n")
  394. }
  395. if iface.AuthType == "md5" && iface.AuthKey != "" {
  396. sb.WriteString(" authentication cryptographic;\n")
  397. sb.WriteString(fmt.Sprintf(" password \"%s\";\n", iface.AuthKey))
  398. } else if iface.AuthType == "simple" && iface.AuthKey != "" {
  399. sb.WriteString(" authentication simple;\n")
  400. sb.WriteString(fmt.Sprintf(" password \"%s\";\n", iface.AuthKey))
  401. }
  402. sb.WriteString(" };\n")
  403. }
  404. sb.WriteString(" };\n")
  405. }
  406. sb.WriteString("}\n\n")
  407. }
  408. // Write candidate configuration to temporary file
  409. tmpPath := birdConfigPath + ".new"
  410. if err := os.WriteFile(tmpPath, []byte(sb.String()), 0644); err != nil {
  411. return fmt.Errorf("failed to write %s: %w", tmpPath, err)
  412. }
  413. // Validate syntax with bird -p
  414. out, err := exec.Command("bird", "-p", "-c", tmpPath).CombinedOutput()
  415. if err != nil {
  416. _ = os.Remove(tmpPath)
  417. return fmt.Errorf("bird syntax check failed: %s: %w", string(out), err)
  418. }
  419. // Replace active config
  420. if err := os.Rename(tmpPath, birdConfigPath); err != nil {
  421. return fmt.Errorf("failed to replace %s: %w", birdConfigPath, err)
  422. }
  423. // Ensure BIRD systemd service is active
  424. _ = exec.Command("systemctl", "start", "bird").Run()
  425. // Live reload without packet loss
  426. reloadOut, reloadErr := birdcExec("configure")
  427. if reloadErr != nil {
  428. log.Printf("[DynamicRouting] birdc configure notice: %s", string(reloadOut))
  429. }
  430. return nil
  431. }
  432. func birdcExec(cmd string) ([]byte, error) {
  433. return exec.Command("birdc", cmd).CombinedOutput()
  434. }
  435. // -------------------------------------------------------------
  436. // BGP API & Operations (Multi-Instance BGP)
  437. // -------------------------------------------------------------
  438. func (m *Manager) GetBGPInstance() BGPInstance {
  439. m.mu.RLock()
  440. defer m.mu.RUnlock()
  441. inst := m.config.BGPInstance
  442. out, err := birdcExec("show status")
  443. if err == nil && strings.Contains(string(out), "Daemon is up and running") && inst.Enabled {
  444. inst.Running = true
  445. }
  446. return inst
  447. }
  448. func (m *Manager) UpdateBGPInstance(inst BGPInstance) error {
  449. m.mu.Lock()
  450. m.config.BGPInstance = inst
  451. _ = m.save()
  452. m.mu.Unlock()
  453. return m.applyBirdConfig()
  454. }
  455. func (m *Manager) ListBGPPeers() []BGPPeer {
  456. m.mu.RLock()
  457. defer m.mu.RUnlock()
  458. peers := make([]BGPPeer, len(m.config.BGPPeers))
  459. copy(peers, m.config.BGPPeers)
  460. // Fetch live peer telemetry from birdc
  461. out, err := birdcExec("show protocols all")
  462. if err == nil {
  463. type livePeerData struct {
  464. State string
  465. RemoteAddr string
  466. RemoteAS uint32
  467. LocalAS uint32
  468. PfxRcd int
  469. PfxSnt int
  470. Uptime string
  471. }
  472. liveMap := make(map[string]*livePeerData)
  473. lines := strings.Split(string(out), "\n")
  474. var cur *livePeerData
  475. for _, line := range lines {
  476. trimmed := strings.TrimSpace(line)
  477. if strings.Contains(line, " BGP ") {
  478. cur = &livePeerData{}
  479. fields := strings.Fields(line)
  480. if len(fields) >= 5 {
  481. cur.Uptime = fields[4]
  482. }
  483. }
  484. if cur == nil {
  485. continue
  486. }
  487. if strings.HasPrefix(trimmed, "BGP state:") {
  488. cur.State = strings.TrimSpace(strings.TrimPrefix(trimmed, "BGP state:"))
  489. } else if strings.HasPrefix(trimmed, "Neighbor address:") {
  490. cur.RemoteAddr = strings.TrimSpace(strings.TrimPrefix(trimmed, "Neighbor address:"))
  491. liveMap[cur.RemoteAddr] = cur
  492. } else if strings.HasPrefix(trimmed, "Neighbor AS:") {
  493. v, _ := strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(trimmed, "Neighbor AS:")), 10, 32)
  494. cur.RemoteAS = uint32(v)
  495. } else if strings.HasPrefix(trimmed, "Local AS:") {
  496. v, _ := strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(trimmed, "Local AS:")), 10, 32)
  497. cur.LocalAS = uint32(v)
  498. } else if strings.HasPrefix(trimmed, "Routes:") {
  499. parts := strings.Split(trimmed, ",")
  500. for _, p := range parts {
  501. p = strings.TrimSpace(p)
  502. if strings.Contains(p, "imported") {
  503. f := strings.Fields(p)
  504. if len(f) >= 2 {
  505. cur.PfxRcd, _ = strconv.Atoi(f[len(f)-2])
  506. }
  507. } else if strings.Contains(p, "exported") {
  508. f := strings.Fields(p)
  509. if len(f) >= 2 {
  510. cur.PfxSnt, _ = strconv.Atoi(f[0])
  511. }
  512. }
  513. }
  514. }
  515. }
  516. for i := range peers {
  517. if live, ok := liveMap[peers[i].RemoteAddress]; ok {
  518. peers[i].State = live.State
  519. peers[i].Uptime = live.Uptime
  520. peers[i].PrefixReceived = live.PfxRcd
  521. peers[i].PrefixSent = live.PfxSnt
  522. if peers[i].LocalAS == 0 && live.LocalAS > 0 {
  523. peers[i].LocalAS = live.LocalAS
  524. }
  525. } else if peers[i].Disabled {
  526. peers[i].State = "Disabled"
  527. } else {
  528. peers[i].State = "Idle"
  529. }
  530. }
  531. }
  532. return peers
  533. }
  534. func (m *Manager) SaveBGPPeer(peer BGPPeer) error {
  535. m.mu.Lock()
  536. defer m.mu.Unlock()
  537. if peer.ID == "" {
  538. peer.ID = fmt.Sprintf("bgp-peer-%d", time.Now().UnixNano())
  539. }
  540. found := false
  541. for i, p := range m.config.BGPPeers {
  542. if p.ID == peer.ID {
  543. m.config.BGPPeers[i] = peer
  544. found = true
  545. break
  546. }
  547. }
  548. if !found {
  549. m.config.BGPPeers = append(m.config.BGPPeers, peer)
  550. }
  551. if err := m.save(); err != nil {
  552. return err
  553. }
  554. return m.applyBirdConfig()
  555. }
  556. func (m *Manager) DeleteBGPPeer(id string) error {
  557. m.mu.Lock()
  558. defer m.mu.Unlock()
  559. newPeers := make([]BGPPeer, 0, len(m.config.BGPPeers))
  560. for _, p := range m.config.BGPPeers {
  561. if p.ID != id {
  562. newPeers = append(newPeers, p)
  563. }
  564. }
  565. m.config.BGPPeers = newPeers
  566. if err := m.save(); err != nil {
  567. return err
  568. }
  569. return m.applyBirdConfig()
  570. }
  571. func (m *Manager) SetBGPPeerState(id string, disabled bool) error {
  572. m.mu.Lock()
  573. defer m.mu.Unlock()
  574. for i := range m.config.BGPPeers {
  575. if m.config.BGPPeers[i].ID == id {
  576. m.config.BGPPeers[i].Disabled = disabled
  577. break
  578. }
  579. }
  580. if err := m.save(); err != nil {
  581. return err
  582. }
  583. return m.applyBirdConfig()
  584. }
  585. func (m *Manager) ListBGPNetworks() []BGPNetwork {
  586. m.mu.RLock()
  587. defer m.mu.RUnlock()
  588. return m.config.BGPNetworks
  589. }
  590. func (m *Manager) SaveBGPNetwork(net BGPNetwork) error {
  591. m.mu.Lock()
  592. defer m.mu.Unlock()
  593. if net.ID == "" {
  594. net.ID = fmt.Sprintf("bgp-net-%d", time.Now().UnixNano())
  595. }
  596. found := false
  597. for i, n := range m.config.BGPNetworks {
  598. if n.ID == net.ID {
  599. m.config.BGPNetworks[i] = net
  600. found = true
  601. break
  602. }
  603. }
  604. if !found {
  605. m.config.BGPNetworks = append(m.config.BGPNetworks, net)
  606. }
  607. if err := m.save(); err != nil {
  608. return err
  609. }
  610. return m.applyBirdConfig()
  611. }
  612. func (m *Manager) DeleteBGPNetwork(id string) error {
  613. m.mu.Lock()
  614. defer m.mu.Unlock()
  615. newNets := make([]BGPNetwork, 0, len(m.config.BGPNetworks))
  616. for _, n := range m.config.BGPNetworks {
  617. if n.ID != id {
  618. newNets = append(newNets, n)
  619. }
  620. }
  621. m.config.BGPNetworks = newNets
  622. if err := m.save(); err != nil {
  623. return err
  624. }
  625. return m.applyBirdConfig()
  626. }
  627. func (m *Manager) GetBGPRoutes() []BGPRoute {
  628. var routes []BGPRoute
  629. out, err := birdcExec("show route where source = RTS_BGP all")
  630. if err != nil {
  631. return routes
  632. }
  633. lines := strings.Split(string(out), "\n")
  634. var cur *BGPRoute
  635. for _, line := range lines {
  636. trimmed := strings.TrimSpace(line)
  637. if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "Table") {
  638. continue
  639. }
  640. if !strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, " ") {
  641. fields := strings.Fields(trimmed)
  642. if len(fields) >= 2 {
  643. routes = append(routes, BGPRoute{
  644. Prefix: fields[0],
  645. Best: strings.Contains(line, "*"),
  646. Origin: "IGP",
  647. })
  648. cur = &routes[len(routes)-1]
  649. }
  650. continue
  651. }
  652. if cur == nil {
  653. continue
  654. }
  655. if strings.HasPrefix(trimmed, "via ") {
  656. parts := strings.Fields(trimmed)
  657. if len(parts) >= 2 {
  658. cur.NextHop = parts[1]
  659. }
  660. } else if strings.HasPrefix(trimmed, "BGP.as_path:") {
  661. cur.Path = strings.TrimSpace(strings.TrimPrefix(trimmed, "BGP.as_path:"))
  662. } else if strings.HasPrefix(trimmed, "BGP.origin:") {
  663. cur.Origin = strings.TrimSpace(strings.TrimPrefix(trimmed, "BGP.origin:"))
  664. } else if strings.HasPrefix(trimmed, "BGP.next_hop:") {
  665. cur.NextHop = strings.TrimSpace(strings.TrimPrefix(trimmed, "BGP.next_hop:"))
  666. } else if strings.HasPrefix(trimmed, "BGP.med:") {
  667. cur.Metric, _ = strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(trimmed, "BGP.med:")))
  668. }
  669. }
  670. return routes
  671. }
  672. // -------------------------------------------------------------
  673. // OSPF API & Operations
  674. // -------------------------------------------------------------
  675. func (m *Manager) GetOSPFInstance() OSPFInstance {
  676. m.mu.RLock()
  677. defer m.mu.RUnlock()
  678. inst := m.config.OSPFInstance
  679. out, err := birdcExec("show protocols")
  680. if err == nil && strings.Contains(string(out), "OSPF") && strings.Contains(string(out), "up") && inst.Enabled {
  681. inst.Running = true
  682. }
  683. return inst
  684. }
  685. func (m *Manager) UpdateOSPFInstance(inst OSPFInstance) error {
  686. m.mu.Lock()
  687. m.config.OSPFInstance = inst
  688. _ = m.save()
  689. m.mu.Unlock()
  690. return m.applyBirdConfig()
  691. }
  692. func (m *Manager) ListOSPFAreas() []OSPFArea {
  693. m.mu.RLock()
  694. defer m.mu.RUnlock()
  695. return m.config.OSPFAreas
  696. }
  697. func (m *Manager) SaveOSPFArea(area OSPFArea) error {
  698. m.mu.Lock()
  699. defer m.mu.Unlock()
  700. if area.ID == "" {
  701. area.ID = fmt.Sprintf("area-%d", time.Now().UnixNano())
  702. }
  703. found := false
  704. for i, a := range m.config.OSPFAreas {
  705. if a.ID == area.ID {
  706. m.config.OSPFAreas[i] = area
  707. found = true
  708. break
  709. }
  710. }
  711. if !found {
  712. m.config.OSPFAreas = append(m.config.OSPFAreas, area)
  713. }
  714. if err := m.save(); err != nil {
  715. return err
  716. }
  717. return m.applyBirdConfig()
  718. }
  719. func (m *Manager) DeleteOSPFArea(id string) error {
  720. m.mu.Lock()
  721. defer m.mu.Unlock()
  722. newAreas := make([]OSPFArea, 0, len(m.config.OSPFAreas))
  723. for _, a := range m.config.OSPFAreas {
  724. if a.ID != id {
  725. newAreas = append(newAreas, a)
  726. }
  727. }
  728. m.config.OSPFAreas = newAreas
  729. if err := m.save(); err != nil {
  730. return err
  731. }
  732. return m.applyBirdConfig()
  733. }
  734. func (m *Manager) ListOSPFInterfaces() []OSPFInterface {
  735. m.mu.RLock()
  736. defer m.mu.RUnlock()
  737. return m.config.OSPFInterfaces
  738. }
  739. func (m *Manager) SaveOSPFInterface(iface OSPFInterface) error {
  740. m.mu.Lock()
  741. defer m.mu.Unlock()
  742. if iface.ID == "" {
  743. iface.ID = fmt.Sprintf("ospf-iface-%d", time.Now().UnixNano())
  744. }
  745. if iface.Cost <= 0 {
  746. iface.Cost = 10
  747. }
  748. if iface.Priority <= 0 {
  749. iface.Priority = 1
  750. }
  751. if iface.HelloInterval <= 0 {
  752. iface.HelloInterval = 10
  753. }
  754. if iface.DeadInterval <= 0 {
  755. iface.DeadInterval = 40
  756. }
  757. found := false
  758. for i, item := range m.config.OSPFInterfaces {
  759. if item.ID == iface.ID {
  760. m.config.OSPFInterfaces[i] = iface
  761. found = true
  762. break
  763. }
  764. }
  765. if !found {
  766. m.config.OSPFInterfaces = append(m.config.OSPFInterfaces, iface)
  767. }
  768. if err := m.save(); err != nil {
  769. return err
  770. }
  771. return m.applyBirdConfig()
  772. }
  773. func (m *Manager) DeleteOSPFInterface(id string) error {
  774. m.mu.Lock()
  775. defer m.mu.Unlock()
  776. newInterfaces := make([]OSPFInterface, 0, len(m.config.OSPFInterfaces))
  777. for _, item := range m.config.OSPFInterfaces {
  778. if item.ID != id {
  779. newInterfaces = append(newInterfaces, item)
  780. }
  781. }
  782. m.config.OSPFInterfaces = newInterfaces
  783. if err := m.save(); err != nil {
  784. return err
  785. }
  786. return m.applyBirdConfig()
  787. }
  788. func (m *Manager) SetOSPFInterfaceState(id string, disabled bool) error {
  789. m.mu.Lock()
  790. defer m.mu.Unlock()
  791. for i := range m.config.OSPFInterfaces {
  792. if m.config.OSPFInterfaces[i].ID == id {
  793. m.config.OSPFInterfaces[i].Disabled = disabled
  794. break
  795. }
  796. }
  797. if err := m.save(); err != nil {
  798. return err
  799. }
  800. return m.applyBirdConfig()
  801. }
  802. func (m *Manager) GetOSPFNeighbors() []OSPFNeighbor {
  803. var neighbors []OSPFNeighbor
  804. out, err := birdcExec("show ospf neighbors")
  805. if err != nil {
  806. return neighbors
  807. }
  808. lines := strings.Split(string(out), "\n")
  809. for _, line := range lines {
  810. trimmed := strings.TrimSpace(line)
  811. if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "Router ID") || strings.HasSuffix(trimmed, ":") {
  812. continue
  813. }
  814. fields := strings.Fields(trimmed)
  815. if len(fields) >= 5 {
  816. nbr := OSPFNeighbor{
  817. NeighborID: fields[0],
  818. State: fields[2],
  819. DeadTimer: fields[3],
  820. Interface: fields[4],
  821. }
  822. if len(fields) >= 6 {
  823. nbr.Address = fields[5]
  824. } else {
  825. nbr.Address = fields[0]
  826. }
  827. neighbors = append(neighbors, nbr)
  828. }
  829. }
  830. return neighbors
  831. }
  832. func (m *Manager) GetOSPFRoutes() []OSPFRoute {
  833. var routes []OSPFRoute
  834. out, err := birdcExec("show route where source = RTS_OSPF all")
  835. if err != nil {
  836. return routes
  837. }
  838. lines := strings.Split(string(out), "\n")
  839. var cur *OSPFRoute
  840. for _, line := range lines {
  841. trimmed := strings.TrimSpace(line)
  842. if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "Table") {
  843. continue
  844. }
  845. if !strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, " ") {
  846. fields := strings.Fields(trimmed)
  847. if len(fields) >= 2 {
  848. cost := 10
  849. if idx := strings.Index(line, "/"); idx != -1 {
  850. sub := line[idx+1:]
  851. if end := strings.Index(sub, ")"); end != -1 {
  852. cost, _ = strconv.Atoi(sub[:end])
  853. }
  854. }
  855. routes = append(routes, OSPFRoute{
  856. Prefix: fields[0],
  857. Cost: cost,
  858. RouteType: "Intra-Area",
  859. })
  860. cur = &routes[len(routes)-1]
  861. }
  862. continue
  863. }
  864. if cur == nil {
  865. continue
  866. }
  867. if strings.HasPrefix(trimmed, "via ") {
  868. parts := strings.Fields(trimmed)
  869. if len(parts) >= 2 {
  870. cur.NextHop = parts[1]
  871. }
  872. if len(parts) >= 4 && parts[2] == "on" {
  873. cur.Interface = parts[3]
  874. }
  875. } else if strings.HasPrefix(trimmed, "Type:") {
  876. cur.RouteType = strings.TrimSpace(strings.TrimPrefix(trimmed, "Type:"))
  877. }
  878. }
  879. return routes
  880. }
  881. // -------------------------------------------------------------
  882. // Routing Filter API & Operations (/routing filter)
  883. // -------------------------------------------------------------
  884. func (m *Manager) ListRoutingFilters() []RoutingFilterRule {
  885. m.mu.RLock()
  886. defer m.mu.RUnlock()
  887. filters := make([]RoutingFilterRule, len(m.config.RoutingFilters))
  888. copy(filters, m.config.RoutingFilters)
  889. return filters
  890. }
  891. func (m *Manager) SaveRoutingFilter(rule *RoutingFilterRule) error {
  892. m.mu.Lock()
  893. if rule.ID == "" {
  894. rule.ID = fmt.Sprintf("rf-%d", time.Now().UnixNano()/1e6)
  895. }
  896. if rule.RuleOrder <= 0 {
  897. rule.RuleOrder = (len(m.config.RoutingFilters) + 1) * 10
  898. }
  899. found := false
  900. for i, r := range m.config.RoutingFilters {
  901. if r.ID == rule.ID {
  902. m.config.RoutingFilters[i] = *rule
  903. found = true
  904. break
  905. }
  906. }
  907. if !found {
  908. m.config.RoutingFilters = append(m.config.RoutingFilters, *rule)
  909. }
  910. _ = m.save()
  911. m.mu.Unlock()
  912. return m.applyBirdConfig()
  913. }
  914. func (m *Manager) DeleteRoutingFilter(id string) error {
  915. m.mu.Lock()
  916. newRules := make([]RoutingFilterRule, 0, len(m.config.RoutingFilters))
  917. for _, r := range m.config.RoutingFilters {
  918. if r.ID != id {
  919. newRules = append(newRules, r)
  920. }
  921. }
  922. m.config.RoutingFilters = newRules
  923. _ = m.save()
  924. m.mu.Unlock()
  925. return m.applyBirdConfig()
  926. }
  927. func (m *Manager) SetRoutingFilterState(id string, disabled bool) error {
  928. m.mu.Lock()
  929. for i := range m.config.RoutingFilters {
  930. if m.config.RoutingFilters[i].ID == id {
  931. m.config.RoutingFilters[i].Disabled = disabled
  932. break
  933. }
  934. }
  935. _ = m.save()
  936. m.mu.Unlock()
  937. return m.applyBirdConfig()
  938. }
  939. // -------------------------------------------------------------
  940. // Summary Telemetry
  941. // -------------------------------------------------------------
  942. func (m *Manager) GetSummary() DynamicRoutingSummary {
  943. m.mu.RLock()
  944. defer m.mu.RUnlock()
  945. summary := DynamicRoutingSummary{
  946. BGPEnabled: m.config.BGPInstance.Enabled,
  947. OSPFEnabled: m.config.OSPFInstance.Enabled,
  948. BGPPeerCount: len(m.config.BGPPeers),
  949. RoutingFilterCount: len(m.config.RoutingFilters),
  950. }
  951. out, err := birdcExec("show status")
  952. birdRunning := err == nil && strings.Contains(string(out), "Daemon is up and running")
  953. if birdRunning && summary.BGPEnabled {
  954. summary.BGPRunning = true
  955. peers := m.ListBGPPeers()
  956. est := 0
  957. for _, p := range peers {
  958. if strings.EqualFold(p.State, "Established") || strings.EqualFold(p.State, "Up") {
  959. est++
  960. }
  961. }
  962. summary.BGPEstablishedCount = est
  963. }
  964. if birdRunning && summary.OSPFEnabled {
  965. summary.OSPFRunning = true
  966. nbrs := m.GetOSPFNeighbors()
  967. summary.OSPFNeighborCount = len(nbrs)
  968. adj := 0
  969. for _, n := range nbrs {
  970. if strings.Contains(strings.ToLower(n.State), "full") {
  971. adj++
  972. }
  973. }
  974. summary.OSPFAdjacentCount = adj
  975. }
  976. return summary
  977. }