manager.go 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178
  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. // Per-peer Output Networks (RouterOS v7 output.network)
  242. var peerNets []string
  243. for _, on := range peer.OutputNetworks {
  244. on = strings.TrimSpace(on)
  245. if on != "" {
  246. peerNets = append(peerNets, on)
  247. }
  248. }
  249. if peer.DefaultOriginate {
  250. peerNets = append(peerNets, "0.0.0.0/0")
  251. }
  252. peerNetsProto := ""
  253. if len(peerNets) > 0 {
  254. peerNetsProto = fmt.Sprintf("bgp_nets_%s", protoName)
  255. sb.WriteString(fmt.Sprintf("protocol static %s {\n", peerNetsProto))
  256. sb.WriteString(" ipv4;\n")
  257. for _, pn := range peerNets {
  258. sb.WriteString(fmt.Sprintf(" route %s blackhole;\n", pn))
  259. }
  260. sb.WriteString("}\n\n")
  261. }
  262. sb.WriteString(fmt.Sprintf("protocol bgp %s {\n", protoName))
  263. if peer.Name != "" {
  264. sb.WriteString(fmt.Sprintf(" description \"%s\";\n", peer.Name))
  265. }
  266. if peer.Disabled {
  267. sb.WriteString(" disabled;\n")
  268. }
  269. if peer.LocalAddress != "" {
  270. if peer.LocalPort > 0 {
  271. sb.WriteString(fmt.Sprintf(" local %s port %d as %d;\n", peer.LocalAddress, peer.LocalPort, localAS))
  272. } else {
  273. sb.WriteString(fmt.Sprintf(" local %s as %d;\n", peer.LocalAddress, localAS))
  274. }
  275. } else {
  276. if peer.LocalPort > 0 {
  277. sb.WriteString(fmt.Sprintf(" local port %d as %d;\n", peer.LocalPort, localAS))
  278. } else {
  279. sb.WriteString(fmt.Sprintf(" local as %d;\n", localAS))
  280. }
  281. }
  282. if peer.RemotePort > 0 {
  283. sb.WriteString(fmt.Sprintf(" neighbor %s port %d as %d;\n", peer.RemoteAddress, peer.RemotePort, peer.RemoteAS))
  284. } else {
  285. sb.WriteString(fmt.Sprintf(" neighbor %s as %d;\n", peer.RemoteAddress, peer.RemoteAS))
  286. }
  287. if peer.Multihop {
  288. sb.WriteString(" multihop;\n")
  289. }
  290. if peer.Passive || (!peer.Connect && peer.Listen) {
  291. sb.WriteString(" passive;\n")
  292. }
  293. if peer.Password != "" {
  294. sb.WriteString(fmt.Sprintf(" password \"%s\";\n", peer.Password))
  295. }
  296. holdTime := 180
  297. if peer.HoldTime > 0 {
  298. holdTime = peer.HoldTime
  299. } else if m.config.BGPInstance.HoldTime > 0 {
  300. holdTime = m.config.BGPInstance.HoldTime
  301. }
  302. sb.WriteString(fmt.Sprintf(" hold time %d;\n", holdTime))
  303. if peer.KeepaliveTime > 0 {
  304. sb.WriteString(fmt.Sprintf(" keepalive time %d;\n", peer.KeepaliveTime))
  305. }
  306. // IPv4 Channel
  307. sb.WriteString(" ipv4 {\n")
  308. if peer.NexthopSelf || peer.NexthopChoice == "force-self" {
  309. sb.WriteString(" next hop self;\n")
  310. }
  311. // Import Filter
  312. if peer.InFilter != "" && len(chainRules[peer.InFilter]) > 0 {
  313. sb.WriteString(fmt.Sprintf(" import filter %s;\n", sanitizeIdentifier("filter", peer.InFilter)))
  314. } else {
  315. sb.WriteString(" import all;\n")
  316. }
  317. // Export Filter
  318. if peer.OutFilter != "" && len(chainRules[peer.OutFilter]) > 0 {
  319. sb.WriteString(fmt.Sprintf(" export filter %s;\n", sanitizeIdentifier("filter", peer.OutFilter)))
  320. } else {
  321. var exportSources []string
  322. if peerNetsProto != "" {
  323. exportSources = append(exportSources, fmt.Sprintf(`proto = "%s"`, peerNetsProto))
  324. }
  325. if len(activeNets) > 0 {
  326. exportSources = append(exportSources, `proto = "bgp_static_nets"`)
  327. }
  328. exportSources = append(exportSources, `source = RTS_BGP`)
  329. if peer.RedistConnected || m.config.BGPInstance.RedistributeConnected {
  330. exportSources = append(exportSources, `source = RTS_DEVICE`)
  331. }
  332. if peer.RedistStatic || m.config.BGPInstance.RedistributeStatic {
  333. exportSources = append(exportSources, `source = RTS_STATIC`)
  334. }
  335. if peer.RedistOSPF || m.config.BGPInstance.RedistributeOSPF {
  336. exportSources = append(exportSources, `source = RTS_OSPF`)
  337. }
  338. if len(exportSources) > 0 {
  339. sb.WriteString(fmt.Sprintf(" export where (%s);\n", strings.Join(exportSources, ") || (")))
  340. } else {
  341. sb.WriteString(" export none;\n")
  342. }
  343. }
  344. sb.WriteString(" };\n")
  345. sb.WriteString("}\n\n")
  346. }
  347. }
  348. // OSPFv2 Protocol
  349. if m.config.OSPFInstance.Enabled {
  350. ospfRouterID := routerID
  351. if m.config.OSPFInstance.RouterID != "" {
  352. ospfRouterID = m.config.OSPFInstance.RouterID
  353. }
  354. sb.WriteString("protocol ospf v2 ospf_main {\n")
  355. sb.WriteString(fmt.Sprintf(" router id %s;\n", ospfRouterID))
  356. sb.WriteString(" ipv4 {\n")
  357. sb.WriteString(" import all;\n")
  358. var ospfExports []string
  359. ospfExports = append(ospfExports, "RTS_STATIC")
  360. if m.config.OSPFInstance.RedistributeConnected {
  361. ospfExports = append(ospfExports, "RTS_DEVICE")
  362. }
  363. if m.config.OSPFInstance.RedistributeBGP {
  364. ospfExports = append(ospfExports, "RTS_BGP")
  365. }
  366. if len(ospfExports) > 0 {
  367. sb.WriteString(fmt.Sprintf(" export where source ~ [ %s ];\n", strings.Join(ospfExports, ", ")))
  368. } else {
  369. sb.WriteString(" export none;\n")
  370. }
  371. sb.WriteString(" };\n\n")
  372. // Group interfaces by area
  373. areaIfaces := make(map[string][]OSPFInterface)
  374. for _, iface := range m.config.OSPFInterfaces {
  375. if iface.Disabled {
  376. continue
  377. }
  378. area := strings.TrimSpace(iface.Area)
  379. if area == "" {
  380. area = "0.0.0.0"
  381. }
  382. areaIfaces[area] = append(areaIfaces[area], iface)
  383. }
  384. // Ensure backbone area always exists
  385. if _, ok := areaIfaces["0.0.0.0"]; !ok {
  386. areaIfaces["0.0.0.0"] = []OSPFInterface{}
  387. }
  388. areas := m.config.OSPFAreas
  389. if len(areas) == 0 {
  390. areas = []OSPFArea{
  391. {
  392. Name: "backbone",
  393. AreaID: "0.0.0.0",
  394. AreaType: "default",
  395. },
  396. }
  397. }
  398. for _, area := range areas {
  399. areaID := strings.TrimSpace(area.AreaID)
  400. if areaID == "" {
  401. areaID = "0.0.0.0"
  402. }
  403. ifaces := areaIfaces[areaID]
  404. sb.WriteString(fmt.Sprintf(" area %s {\n", areaID))
  405. if area.AreaType == "stub" {
  406. sb.WriteString(" stub;\n")
  407. } else if area.AreaType == "nssa" {
  408. sb.WriteString(" nssa;\n")
  409. }
  410. for _, iface := range ifaces {
  411. target := iface.Interface
  412. if target == "" {
  413. target = iface.Network
  414. }
  415. if target == "" {
  416. continue
  417. }
  418. sb.WriteString(fmt.Sprintf(" interface \"%s\" {\n", target))
  419. if iface.Cost > 0 {
  420. sb.WriteString(fmt.Sprintf(" cost %d;\n", iface.Cost))
  421. }
  422. if iface.Priority > 0 {
  423. sb.WriteString(fmt.Sprintf(" priority %d;\n", iface.Priority))
  424. }
  425. if iface.HelloInterval > 0 {
  426. sb.WriteString(fmt.Sprintf(" hello %d;\n", iface.HelloInterval))
  427. }
  428. if iface.DeadInterval > 0 {
  429. sb.WriteString(fmt.Sprintf(" dead %d;\n", iface.DeadInterval))
  430. }
  431. if iface.Passive {
  432. sb.WriteString(" stub;\n")
  433. } else {
  434. sb.WriteString(" type broadcast;\n")
  435. }
  436. if iface.AuthType == "md5" && iface.AuthKey != "" {
  437. sb.WriteString(" authentication cryptographic;\n")
  438. sb.WriteString(fmt.Sprintf(" password \"%s\";\n", iface.AuthKey))
  439. } else if iface.AuthType == "simple" && iface.AuthKey != "" {
  440. sb.WriteString(" authentication simple;\n")
  441. sb.WriteString(fmt.Sprintf(" password \"%s\";\n", iface.AuthKey))
  442. }
  443. sb.WriteString(" };\n")
  444. }
  445. sb.WriteString(" };\n")
  446. }
  447. sb.WriteString("}\n\n")
  448. }
  449. // Write candidate configuration to temporary file
  450. tmpPath := birdConfigPath + ".new"
  451. if err := os.WriteFile(tmpPath, []byte(sb.String()), 0644); err != nil {
  452. return fmt.Errorf("failed to write %s: %w", tmpPath, err)
  453. }
  454. // Validate syntax with bird -p
  455. out, err := exec.Command("bird", "-p", "-c", tmpPath).CombinedOutput()
  456. if err != nil {
  457. _ = os.Remove(tmpPath)
  458. return fmt.Errorf("bird syntax check failed: %s: %w", string(out), err)
  459. }
  460. // Replace active config
  461. if err := os.Rename(tmpPath, birdConfigPath); err != nil {
  462. return fmt.Errorf("failed to replace %s: %w", birdConfigPath, err)
  463. }
  464. // Ensure BIRD systemd service is active
  465. _ = exec.Command("systemctl", "start", "bird").Run()
  466. // Live reload without packet loss
  467. reloadOut, reloadErr := birdcExec("configure")
  468. if reloadErr != nil {
  469. log.Printf("[DynamicRouting] birdc configure notice: %s", string(reloadOut))
  470. }
  471. return nil
  472. }
  473. func birdcExec(cmd string) ([]byte, error) {
  474. return exec.Command("birdc", cmd).CombinedOutput()
  475. }
  476. // -------------------------------------------------------------
  477. // BGP API & Operations (Multi-Instance BGP)
  478. // -------------------------------------------------------------
  479. func (m *Manager) GetBGPInstance() BGPInstance {
  480. m.mu.RLock()
  481. defer m.mu.RUnlock()
  482. inst := m.config.BGPInstance
  483. out, err := birdcExec("show status")
  484. if err == nil && strings.Contains(string(out), "Daemon is up and running") && inst.Enabled {
  485. inst.Running = true
  486. }
  487. return inst
  488. }
  489. func (m *Manager) UpdateBGPInstance(inst BGPInstance) error {
  490. m.mu.Lock()
  491. m.config.BGPInstance = inst
  492. _ = m.save()
  493. m.mu.Unlock()
  494. return m.applyBirdConfig()
  495. }
  496. func (m *Manager) ListBGPPeers() []BGPPeer {
  497. m.mu.RLock()
  498. defer m.mu.RUnlock()
  499. peers := make([]BGPPeer, len(m.config.BGPPeers))
  500. copy(peers, m.config.BGPPeers)
  501. // Fetch live peer telemetry from birdc
  502. out, err := birdcExec("show protocols all")
  503. if err == nil {
  504. type livePeerData struct {
  505. State string
  506. RemoteAddr string
  507. RemoteAS uint32
  508. LocalAS uint32
  509. PfxRcd int
  510. PfxSnt int
  511. Uptime string
  512. }
  513. liveMap := make(map[string]*livePeerData)
  514. lines := strings.Split(string(out), "\n")
  515. var cur *livePeerData
  516. for _, line := range lines {
  517. trimmed := strings.TrimSpace(line)
  518. if strings.Contains(line, " BGP ") {
  519. cur = &livePeerData{}
  520. fields := strings.Fields(line)
  521. if len(fields) >= 5 {
  522. cur.Uptime = fields[4]
  523. }
  524. }
  525. if cur == nil {
  526. continue
  527. }
  528. if strings.HasPrefix(trimmed, "BGP state:") {
  529. cur.State = strings.TrimSpace(strings.TrimPrefix(trimmed, "BGP state:"))
  530. } else if strings.HasPrefix(trimmed, "Neighbor address:") {
  531. cur.RemoteAddr = strings.TrimSpace(strings.TrimPrefix(trimmed, "Neighbor address:"))
  532. liveMap[cur.RemoteAddr] = cur
  533. } else if strings.HasPrefix(trimmed, "Neighbor AS:") {
  534. v, _ := strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(trimmed, "Neighbor AS:")), 10, 32)
  535. cur.RemoteAS = uint32(v)
  536. } else if strings.HasPrefix(trimmed, "Local AS:") {
  537. v, _ := strconv.ParseUint(strings.TrimSpace(strings.TrimPrefix(trimmed, "Local AS:")), 10, 32)
  538. cur.LocalAS = uint32(v)
  539. } else if strings.HasPrefix(trimmed, "Routes:") {
  540. parts := strings.Split(trimmed, ",")
  541. for _, p := range parts {
  542. p = strings.TrimSpace(p)
  543. if strings.Contains(p, "imported") {
  544. f := strings.Fields(p)
  545. if len(f) >= 2 {
  546. cur.PfxRcd, _ = strconv.Atoi(f[len(f)-2])
  547. }
  548. } else if strings.Contains(p, "exported") {
  549. f := strings.Fields(p)
  550. if len(f) >= 2 {
  551. cur.PfxSnt, _ = strconv.Atoi(f[0])
  552. }
  553. }
  554. }
  555. }
  556. }
  557. for i := range peers {
  558. if live, ok := liveMap[peers[i].RemoteAddress]; ok {
  559. peers[i].State = live.State
  560. peers[i].Uptime = live.Uptime
  561. peers[i].PrefixReceived = live.PfxRcd
  562. peers[i].PrefixSent = live.PfxSnt
  563. if peers[i].LocalAS == 0 && live.LocalAS > 0 {
  564. peers[i].LocalAS = live.LocalAS
  565. }
  566. } else if peers[i].Disabled {
  567. peers[i].State = "Disabled"
  568. } else {
  569. peers[i].State = "Idle"
  570. }
  571. }
  572. }
  573. return peers
  574. }
  575. func (m *Manager) SaveBGPPeer(peer *BGPPeer) error {
  576. m.mu.Lock()
  577. defer m.mu.Unlock()
  578. if peer.ID == "" {
  579. peer.ID = fmt.Sprintf("bgp-peer-%d", time.Now().UnixNano())
  580. }
  581. found := false
  582. for i, p := range m.config.BGPPeers {
  583. if p.ID == peer.ID {
  584. m.config.BGPPeers[i] = *peer
  585. found = true
  586. break
  587. }
  588. }
  589. if !found {
  590. m.config.BGPPeers = append(m.config.BGPPeers, *peer)
  591. }
  592. if err := m.save(); err != nil {
  593. return err
  594. }
  595. return m.applyBirdConfig()
  596. }
  597. func (m *Manager) DeleteBGPPeer(id string) error {
  598. m.mu.Lock()
  599. defer m.mu.Unlock()
  600. newPeers := make([]BGPPeer, 0, len(m.config.BGPPeers))
  601. for _, p := range m.config.BGPPeers {
  602. if p.ID != id {
  603. newPeers = append(newPeers, p)
  604. }
  605. }
  606. m.config.BGPPeers = newPeers
  607. if err := m.save(); err != nil {
  608. return err
  609. }
  610. return m.applyBirdConfig()
  611. }
  612. func (m *Manager) SetBGPPeerState(id string, disabled bool) error {
  613. m.mu.Lock()
  614. defer m.mu.Unlock()
  615. for i := range m.config.BGPPeers {
  616. if m.config.BGPPeers[i].ID == id {
  617. m.config.BGPPeers[i].Disabled = disabled
  618. break
  619. }
  620. }
  621. if err := m.save(); err != nil {
  622. return err
  623. }
  624. return m.applyBirdConfig()
  625. }
  626. func (m *Manager) ListBGPNetworks() []BGPNetwork {
  627. m.mu.RLock()
  628. defer m.mu.RUnlock()
  629. return m.config.BGPNetworks
  630. }
  631. func (m *Manager) SaveBGPNetwork(net BGPNetwork) error {
  632. m.mu.Lock()
  633. defer m.mu.Unlock()
  634. if net.ID == "" {
  635. net.ID = fmt.Sprintf("bgp-net-%d", time.Now().UnixNano())
  636. }
  637. found := false
  638. for i, n := range m.config.BGPNetworks {
  639. if n.ID == net.ID {
  640. m.config.BGPNetworks[i] = net
  641. found = true
  642. break
  643. }
  644. }
  645. if !found {
  646. m.config.BGPNetworks = append(m.config.BGPNetworks, net)
  647. }
  648. if err := m.save(); err != nil {
  649. return err
  650. }
  651. return m.applyBirdConfig()
  652. }
  653. func (m *Manager) DeleteBGPNetwork(id string) error {
  654. m.mu.Lock()
  655. defer m.mu.Unlock()
  656. newNets := make([]BGPNetwork, 0, len(m.config.BGPNetworks))
  657. for _, n := range m.config.BGPNetworks {
  658. if n.ID != id {
  659. newNets = append(newNets, n)
  660. }
  661. }
  662. m.config.BGPNetworks = newNets
  663. if err := m.save(); err != nil {
  664. return err
  665. }
  666. return m.applyBirdConfig()
  667. }
  668. func (m *Manager) GetBGPRoutes() []BGPRoute {
  669. var routes []BGPRoute
  670. out, err := birdcExec("show route where source = RTS_BGP all")
  671. if err != nil {
  672. return routes
  673. }
  674. lines := strings.Split(string(out), "\n")
  675. var cur *BGPRoute
  676. for _, line := range lines {
  677. trimmed := strings.TrimSpace(line)
  678. if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "Table") {
  679. continue
  680. }
  681. if !strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, " ") {
  682. fields := strings.Fields(trimmed)
  683. if len(fields) >= 2 {
  684. routes = append(routes, BGPRoute{
  685. Prefix: fields[0],
  686. Best: strings.Contains(line, "*"),
  687. Origin: "IGP",
  688. })
  689. cur = &routes[len(routes)-1]
  690. }
  691. continue
  692. }
  693. if cur == nil {
  694. continue
  695. }
  696. if strings.HasPrefix(trimmed, "via ") {
  697. parts := strings.Fields(trimmed)
  698. if len(parts) >= 2 {
  699. cur.NextHop = parts[1]
  700. }
  701. } else if strings.HasPrefix(trimmed, "BGP.as_path:") {
  702. cur.Path = strings.TrimSpace(strings.TrimPrefix(trimmed, "BGP.as_path:"))
  703. } else if strings.HasPrefix(trimmed, "BGP.origin:") {
  704. cur.Origin = strings.TrimSpace(strings.TrimPrefix(trimmed, "BGP.origin:"))
  705. } else if strings.HasPrefix(trimmed, "BGP.next_hop:") {
  706. cur.NextHop = strings.TrimSpace(strings.TrimPrefix(trimmed, "BGP.next_hop:"))
  707. } else if strings.HasPrefix(trimmed, "BGP.med:") {
  708. cur.Metric, _ = strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(trimmed, "BGP.med:")))
  709. }
  710. }
  711. return routes
  712. }
  713. // -------------------------------------------------------------
  714. // OSPF API & Operations
  715. // -------------------------------------------------------------
  716. func (m *Manager) GetOSPFInstance() OSPFInstance {
  717. m.mu.RLock()
  718. defer m.mu.RUnlock()
  719. inst := m.config.OSPFInstance
  720. out, err := birdcExec("show protocols")
  721. if err == nil && strings.Contains(string(out), "OSPF") && strings.Contains(string(out), "up") && inst.Enabled {
  722. inst.Running = true
  723. }
  724. return inst
  725. }
  726. func (m *Manager) UpdateOSPFInstance(inst OSPFInstance) error {
  727. m.mu.Lock()
  728. m.config.OSPFInstance = inst
  729. _ = m.save()
  730. m.mu.Unlock()
  731. return m.applyBirdConfig()
  732. }
  733. func (m *Manager) ListOSPFAreas() []OSPFArea {
  734. m.mu.RLock()
  735. defer m.mu.RUnlock()
  736. return m.config.OSPFAreas
  737. }
  738. func (m *Manager) SaveOSPFArea(area OSPFArea) error {
  739. m.mu.Lock()
  740. defer m.mu.Unlock()
  741. if area.ID == "" {
  742. area.ID = fmt.Sprintf("area-%d", time.Now().UnixNano())
  743. }
  744. found := false
  745. for i, a := range m.config.OSPFAreas {
  746. if a.ID == area.ID {
  747. m.config.OSPFAreas[i] = area
  748. found = true
  749. break
  750. }
  751. }
  752. if !found {
  753. m.config.OSPFAreas = append(m.config.OSPFAreas, area)
  754. }
  755. if err := m.save(); err != nil {
  756. return err
  757. }
  758. return m.applyBirdConfig()
  759. }
  760. func (m *Manager) DeleteOSPFArea(id string) error {
  761. m.mu.Lock()
  762. defer m.mu.Unlock()
  763. newAreas := make([]OSPFArea, 0, len(m.config.OSPFAreas))
  764. for _, a := range m.config.OSPFAreas {
  765. if a.ID != id {
  766. newAreas = append(newAreas, a)
  767. }
  768. }
  769. m.config.OSPFAreas = newAreas
  770. if err := m.save(); err != nil {
  771. return err
  772. }
  773. return m.applyBirdConfig()
  774. }
  775. func (m *Manager) ListOSPFInterfaces() []OSPFInterface {
  776. m.mu.RLock()
  777. defer m.mu.RUnlock()
  778. return m.config.OSPFInterfaces
  779. }
  780. func (m *Manager) SaveOSPFInterface(iface OSPFInterface) error {
  781. m.mu.Lock()
  782. defer m.mu.Unlock()
  783. if iface.ID == "" {
  784. iface.ID = fmt.Sprintf("ospf-iface-%d", time.Now().UnixNano())
  785. }
  786. if iface.Cost <= 0 {
  787. iface.Cost = 10
  788. }
  789. if iface.Priority <= 0 {
  790. iface.Priority = 1
  791. }
  792. if iface.HelloInterval <= 0 {
  793. iface.HelloInterval = 10
  794. }
  795. if iface.DeadInterval <= 0 {
  796. iface.DeadInterval = 40
  797. }
  798. found := false
  799. for i, item := range m.config.OSPFInterfaces {
  800. if item.ID == iface.ID {
  801. m.config.OSPFInterfaces[i] = iface
  802. found = true
  803. break
  804. }
  805. }
  806. if !found {
  807. m.config.OSPFInterfaces = append(m.config.OSPFInterfaces, iface)
  808. }
  809. if err := m.save(); err != nil {
  810. return err
  811. }
  812. return m.applyBirdConfig()
  813. }
  814. func (m *Manager) DeleteOSPFInterface(id string) error {
  815. m.mu.Lock()
  816. defer m.mu.Unlock()
  817. newInterfaces := make([]OSPFInterface, 0, len(m.config.OSPFInterfaces))
  818. for _, item := range m.config.OSPFInterfaces {
  819. if item.ID != id {
  820. newInterfaces = append(newInterfaces, item)
  821. }
  822. }
  823. m.config.OSPFInterfaces = newInterfaces
  824. if err := m.save(); err != nil {
  825. return err
  826. }
  827. return m.applyBirdConfig()
  828. }
  829. func (m *Manager) SetOSPFInterfaceState(id string, disabled bool) error {
  830. m.mu.Lock()
  831. defer m.mu.Unlock()
  832. for i := range m.config.OSPFInterfaces {
  833. if m.config.OSPFInterfaces[i].ID == id {
  834. m.config.OSPFInterfaces[i].Disabled = disabled
  835. break
  836. }
  837. }
  838. if err := m.save(); err != nil {
  839. return err
  840. }
  841. return m.applyBirdConfig()
  842. }
  843. func (m *Manager) GetOSPFNeighbors() []OSPFNeighbor {
  844. var neighbors []OSPFNeighbor
  845. out, err := birdcExec("show ospf neighbors")
  846. if err != nil {
  847. return neighbors
  848. }
  849. lines := strings.Split(string(out), "\n")
  850. for _, line := range lines {
  851. trimmed := strings.TrimSpace(line)
  852. if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "Router ID") || strings.HasSuffix(trimmed, ":") {
  853. continue
  854. }
  855. fields := strings.Fields(trimmed)
  856. if len(fields) >= 5 {
  857. nbr := OSPFNeighbor{
  858. NeighborID: fields[0],
  859. State: fields[2],
  860. DeadTimer: fields[3],
  861. Interface: fields[4],
  862. }
  863. if len(fields) >= 6 {
  864. nbr.Address = fields[5]
  865. } else {
  866. nbr.Address = fields[0]
  867. }
  868. neighbors = append(neighbors, nbr)
  869. }
  870. }
  871. return neighbors
  872. }
  873. func (m *Manager) GetOSPFRoutes() []OSPFRoute {
  874. var routes []OSPFRoute
  875. out, err := birdcExec("show route where source = RTS_OSPF all")
  876. if err != nil {
  877. return routes
  878. }
  879. lines := strings.Split(string(out), "\n")
  880. var cur *OSPFRoute
  881. for _, line := range lines {
  882. trimmed := strings.TrimSpace(line)
  883. if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "Table") {
  884. continue
  885. }
  886. if !strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, " ") {
  887. fields := strings.Fields(trimmed)
  888. if len(fields) >= 2 {
  889. cost := 10
  890. if idx := strings.Index(line, "/"); idx != -1 {
  891. sub := line[idx+1:]
  892. if end := strings.Index(sub, ")"); end != -1 {
  893. cost, _ = strconv.Atoi(sub[:end])
  894. }
  895. }
  896. routes = append(routes, OSPFRoute{
  897. Prefix: fields[0],
  898. Cost: cost,
  899. RouteType: "Intra-Area",
  900. })
  901. cur = &routes[len(routes)-1]
  902. }
  903. continue
  904. }
  905. if cur == nil {
  906. continue
  907. }
  908. if strings.HasPrefix(trimmed, "via ") {
  909. parts := strings.Fields(trimmed)
  910. if len(parts) >= 2 {
  911. cur.NextHop = parts[1]
  912. }
  913. if len(parts) >= 4 && parts[2] == "on" {
  914. cur.Interface = parts[3]
  915. }
  916. } else if strings.HasPrefix(trimmed, "Type:") {
  917. cur.RouteType = strings.TrimSpace(strings.TrimPrefix(trimmed, "Type:"))
  918. }
  919. }
  920. return routes
  921. }
  922. // -------------------------------------------------------------
  923. // Routing Filter API & Operations (/routing filter)
  924. // -------------------------------------------------------------
  925. func (m *Manager) ListRoutingFilters() []RoutingFilterRule {
  926. m.mu.RLock()
  927. defer m.mu.RUnlock()
  928. filters := make([]RoutingFilterRule, len(m.config.RoutingFilters))
  929. copy(filters, m.config.RoutingFilters)
  930. return filters
  931. }
  932. func (m *Manager) SaveRoutingFilter(rule *RoutingFilterRule) error {
  933. m.mu.Lock()
  934. if rule.ID == "" {
  935. rule.ID = fmt.Sprintf("rf-%d", time.Now().UnixNano()/1e6)
  936. }
  937. if rule.RuleOrder <= 0 {
  938. rule.RuleOrder = (len(m.config.RoutingFilters) + 1) * 10
  939. }
  940. found := false
  941. for i, r := range m.config.RoutingFilters {
  942. if r.ID == rule.ID {
  943. m.config.RoutingFilters[i] = *rule
  944. found = true
  945. break
  946. }
  947. }
  948. if !found {
  949. m.config.RoutingFilters = append(m.config.RoutingFilters, *rule)
  950. }
  951. _ = m.save()
  952. m.mu.Unlock()
  953. return m.applyBirdConfig()
  954. }
  955. func (m *Manager) DeleteRoutingFilter(id string) error {
  956. m.mu.Lock()
  957. newRules := make([]RoutingFilterRule, 0, len(m.config.RoutingFilters))
  958. for _, r := range m.config.RoutingFilters {
  959. if r.ID != id {
  960. newRules = append(newRules, r)
  961. }
  962. }
  963. m.config.RoutingFilters = newRules
  964. _ = m.save()
  965. m.mu.Unlock()
  966. return m.applyBirdConfig()
  967. }
  968. func (m *Manager) SetRoutingFilterState(id string, disabled bool) error {
  969. m.mu.Lock()
  970. for i := range m.config.RoutingFilters {
  971. if m.config.RoutingFilters[i].ID == id {
  972. m.config.RoutingFilters[i].Disabled = disabled
  973. break
  974. }
  975. }
  976. _ = m.save()
  977. m.mu.Unlock()
  978. return m.applyBirdConfig()
  979. }
  980. // -------------------------------------------------------------
  981. // Summary Telemetry
  982. // -------------------------------------------------------------
  983. func (m *Manager) GetSummary() DynamicRoutingSummary {
  984. m.mu.RLock()
  985. defer m.mu.RUnlock()
  986. summary := DynamicRoutingSummary{
  987. BGPEnabled: m.config.BGPInstance.Enabled,
  988. OSPFEnabled: m.config.OSPFInstance.Enabled,
  989. BGPPeerCount: len(m.config.BGPPeers),
  990. RoutingFilterCount: len(m.config.RoutingFilters),
  991. }
  992. out, err := birdcExec("show status")
  993. birdRunning := err == nil && strings.Contains(string(out), "Daemon is up and running")
  994. if birdRunning && summary.BGPEnabled {
  995. summary.BGPRunning = true
  996. peers := m.ListBGPPeers()
  997. est := 0
  998. for _, p := range peers {
  999. if strings.EqualFold(p.State, "Established") || strings.EqualFold(p.State, "Up") {
  1000. est++
  1001. }
  1002. }
  1003. summary.BGPEstablishedCount = est
  1004. }
  1005. if birdRunning && summary.OSPFEnabled {
  1006. summary.OSPFRunning = true
  1007. nbrs := m.GetOSPFNeighbors()
  1008. summary.OSPFNeighborCount = len(nbrs)
  1009. adj := 0
  1010. for _, n := range nbrs {
  1011. if strings.Contains(strings.ToLower(n.State), "full") {
  1012. adj++
  1013. }
  1014. }
  1015. summary.OSPFAdjacentCount = adj
  1016. }
  1017. return summary
  1018. }