manager.go 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
  1. package routing
  2. import (
  3. "bufio"
  4. "encoding/json"
  5. "fmt"
  6. "log"
  7. "net"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "sort"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. "time"
  16. "github.com/vishvananda/netlink"
  17. )
  18. const (
  19. StaticRoutesStorePath = "/etc/debtik/routing/static_routes.json"
  20. PolicyRulesStorePath = "/etc/debtik/routing/policy_rules.json"
  21. CustomTablesStorePath = "/etc/debtik/routing/custom_tables.json"
  22. RTTableFilePath = "/etc/iproute2/rt_tables"
  23. )
  24. type RoutingManager struct {
  25. mu sync.Mutex
  26. staticRoutes []RouteItem
  27. policyRules []PolicyRuleItem
  28. customTables []RoutingTableItem
  29. stopChan chan struct{}
  30. }
  31. func NewRoutingManager() *RoutingManager {
  32. m := &RoutingManager{
  33. staticRoutes: make([]RouteItem, 0),
  34. policyRules: make([]PolicyRuleItem, 0),
  35. customTables: make([]RoutingTableItem, 0),
  36. stopChan: make(chan struct{}),
  37. }
  38. m.initRTTableFile()
  39. m.load()
  40. m.syncCustomTablesToSystem()
  41. m.applyAll()
  42. m.startHealthCheckWorker()
  43. m.startRouteSyncWorker()
  44. return m
  45. }
  46. func (m *RoutingManager) initRTTableFile() {
  47. _ = os.MkdirAll(filepath.Dir(RTTableFilePath), 0755)
  48. if _, err := os.Stat(RTTableFilePath); os.IsNotExist(err) {
  49. // Copy from /usr/share/iproute2/rt_tables or seed default
  50. content, err := os.ReadFile("/usr/share/iproute2/rt_tables")
  51. if err == nil {
  52. _ = os.WriteFile(RTTableFilePath, content, 0644)
  53. } else {
  54. defaultContent := "# reserved values\n255\tlocal\n254\tmain\n253\tdefault\n0\tunspec\n"
  55. _ = os.WriteFile(RTTableFilePath, []byte(defaultContent), 0644)
  56. }
  57. }
  58. }
  59. func (m *RoutingManager) load() {
  60. if data, err := os.ReadFile(StaticRoutesStorePath); err == nil {
  61. _ = json.Unmarshal(data, &m.staticRoutes)
  62. }
  63. if data, err := os.ReadFile(PolicyRulesStorePath); err == nil {
  64. _ = json.Unmarshal(data, &m.policyRules)
  65. }
  66. if data, err := os.ReadFile(CustomTablesStorePath); err == nil {
  67. _ = json.Unmarshal(data, &m.customTables)
  68. }
  69. for i := range m.staticRoutes {
  70. if !m.staticRoutes[i].Disabled && m.staticRoutes[i].HealthStatus != "offline" {
  71. m.staticRoutes[i].Active = true
  72. }
  73. }
  74. }
  75. func (m *RoutingManager) save() error {
  76. _ = os.MkdirAll("/etc/debtik/routing", 0755)
  77. if data, err := json.MarshalIndent(m.staticRoutes, "", " "); err != nil {
  78. return err
  79. } else if err = os.WriteFile(StaticRoutesStorePath, data, 0600); err != nil {
  80. return err
  81. }
  82. if data, err := json.MarshalIndent(m.policyRules, "", " "); err != nil {
  83. return err
  84. } else if err = os.WriteFile(PolicyRulesStorePath, data, 0600); err != nil {
  85. return err
  86. }
  87. if data, err := json.MarshalIndent(m.customTables, "", " "); err != nil {
  88. return err
  89. } else if err = os.WriteFile(CustomTablesStorePath, data, 0600); err != nil {
  90. return err
  91. }
  92. return nil
  93. }
  94. func (m *RoutingManager) syncCustomTablesToSystem() {
  95. content, _ := os.ReadFile(RTTableFilePath)
  96. str := string(content)
  97. var sb strings.Builder
  98. sb.WriteString(str)
  99. for _, t := range m.customTables {
  100. check := fmt.Sprintf("\t%s", t.Name)
  101. if !strings.Contains(str, check) {
  102. sb.WriteString(fmt.Sprintf("%d\t%s\n", t.TableID, t.Name))
  103. }
  104. }
  105. _ = os.WriteFile(RTTableFilePath, []byte(sb.String()), 0644)
  106. }
  107. func (m *RoutingManager) cleanRogueDefaultRoutes() {
  108. hasManagedDefault := false
  109. for _, r := range m.staticRoutes {
  110. if normalizeDst(r.DstAddress) == "default" && !r.Disabled {
  111. hasManagedDefault = true
  112. break
  113. }
  114. }
  115. if !hasManagedDefault {
  116. return
  117. }
  118. routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
  119. if err != nil {
  120. return
  121. }
  122. links, _ := netlink.LinkList()
  123. linkMap := make(map[int]string)
  124. for _, l := range links {
  125. linkMap[l.Attrs().Index] = l.Attrs().Name
  126. }
  127. for _, r := range routes {
  128. if (r.Table == 254 || r.Table == 0) && r.Dst == nil && len(r.MultiPath) == 0 {
  129. gw := ""
  130. if r.Gw != nil {
  131. gw = r.Gw.String()
  132. }
  133. ifName := linkMap[r.LinkIndex]
  134. // If priority is 0 (unmetered route from dhclient or boot) and protocol != RTPROT_STATIC:
  135. if r.Priority == 0 && int(r.Protocol) != 4 {
  136. log.Printf("[RouteManager] Removing rogue unmanaged default route: via %s dev %s proto %d", gw, ifName, r.Protocol)
  137. _ = netlink.RouteDel(&r)
  138. }
  139. }
  140. }
  141. }
  142. func (m *RoutingManager) getTableIDByName(name string) int {
  143. name = strings.TrimSpace(name)
  144. if name == "" || name == "main" {
  145. return 254
  146. }
  147. if name == "default" {
  148. return 253
  149. }
  150. if name == "local" {
  151. return 255
  152. }
  153. for id, n := range m.getTableMap() {
  154. if strings.EqualFold(n, name) {
  155. return id
  156. }
  157. }
  158. if id, err := strconv.Atoi(name); err == nil {
  159. return id
  160. }
  161. return 254
  162. }
  163. func (m *RoutingManager) startRouteSyncWorker() {
  164. ticker := time.NewTicker(3 * time.Second)
  165. go func() {
  166. for {
  167. select {
  168. case <-m.stopChan:
  169. ticker.Stop()
  170. return
  171. case <-ticker.C:
  172. m.syncAllRoutesWorker()
  173. }
  174. }
  175. }()
  176. }
  177. func (m *RoutingManager) syncAllRoutesWorker() {
  178. m.mu.Lock()
  179. defer m.mu.Unlock()
  180. // Unmanaged routes belong to DHCP, the host or other routing daemons.
  181. liveRoutes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
  182. if err != nil {
  183. return
  184. }
  185. type groupKey struct {
  186. dst string
  187. table int
  188. dist int
  189. }
  190. kernelRouteGroups := make(map[groupKey]bool)
  191. for _, lr := range liveRoutes {
  192. dst := "default"
  193. if lr.Dst != nil {
  194. dst = lr.Dst.String()
  195. }
  196. tbl := lr.Table
  197. if tbl == 0 {
  198. tbl = 254
  199. }
  200. k := groupKey{dst: normalizeDst(dst), table: tbl, dist: lr.Priority}
  201. kernelRouteGroups[k] = true
  202. }
  203. seen := make(map[groupKey]bool)
  204. for _, r := range m.staticRoutes {
  205. if r.Disabled || r.HealthStatus == "offline" {
  206. continue
  207. }
  208. normDst := normalizeDst(r.DstAddress)
  209. tableID := m.getTableIDByName(r.RoutingTable)
  210. k := groupKey{dst: normDst, table: tableID, dist: r.Distance}
  211. if !seen[k] {
  212. seen[k] = true
  213. if !kernelRouteGroups[k] {
  214. log.Printf("[RouteSync] Missing kernel route %s table %s (id %d) metric %d -> Restoring", normDst, r.RoutingTable, tableID, r.Distance)
  215. if err := m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance); err != nil {
  216. log.Printf("route synchronization failed: %v", err)
  217. }
  218. }
  219. }
  220. }
  221. }
  222. func (m *RoutingManager) applyAll() {
  223. // Unmanaged routes belong to DHCP, the host or other routing daemons.
  224. // Apply all static routes grouped by (dst, table, distance) for ECMP
  225. type groupKey struct {
  226. dst string
  227. table string
  228. dist int
  229. }
  230. seen := make(map[groupKey]bool)
  231. for _, r := range m.staticRoutes {
  232. normDst := normalizeDst(r.DstAddress)
  233. tbl := strings.TrimSpace(r.RoutingTable)
  234. if tbl == "" {
  235. tbl = "main"
  236. }
  237. k := groupKey{dst: normDst, table: tbl, dist: r.Distance}
  238. if !seen[k] {
  239. seen[k] = true
  240. _ = m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
  241. }
  242. }
  243. // Apply policy rules
  244. for _, pr := range m.policyRules {
  245. _ = m.applyRuleToKernel(pr)
  246. }
  247. }
  248. // ==================== Routes (/ip route) ====================
  249. func isDynamicProtocol(proto netlink.RouteProtocol) bool {
  250. switch int(proto) {
  251. case 11, // RTPROT_ZEBRA
  252. 12, // RTPROT_BIRD
  253. 16, // RTPROT_DHCP
  254. 42, // RTPROT_BABEL
  255. 186, // RTPROT_BGP
  256. 187, // RTPROT_ISIS
  257. 188, // RTPROT_OSPF
  258. 189, // RTPROT_RIP
  259. 192: // RTPROT_EIGRP
  260. return true
  261. default:
  262. return false
  263. }
  264. }
  265. func getRouteProtocolName(proto netlink.RouteProtocol, priority int, isDirect bool, isMulticast bool) string {
  266. if isMulticast {
  267. return "multicast"
  268. }
  269. switch int(proto) {
  270. case 16:
  271. return "dhcp"
  272. case 4:
  273. return "static"
  274. case 186:
  275. return "bgp"
  276. case 188:
  277. return "ospf"
  278. case 189:
  279. return "rip"
  280. case 11:
  281. return "zebra"
  282. case 42:
  283. return "babel"
  284. case 12: // RTPROT_BIRD
  285. if priority == 20 {
  286. return "bgp"
  287. } else if priority == 200 {
  288. return "ibgp"
  289. } else if priority == 110 {
  290. return "ospf"
  291. } else if priority == 120 {
  292. return "rip"
  293. }
  294. return "bgp"
  295. case 2:
  296. return "connected"
  297. case 3:
  298. return "boot"
  299. default:
  300. if isDirect {
  301. return "connected"
  302. }
  303. if priority == 20 {
  304. return "bgp"
  305. } else if priority == 200 {
  306. return "ibgp"
  307. } else if priority == 110 {
  308. return "ospf"
  309. } else if priority == 120 {
  310. return "rip"
  311. }
  312. return "static"
  313. }
  314. }
  315. func (m *RoutingManager) listLiveRoutesLocked() []RouteItem {
  316. tableNames := m.getTableMap()
  317. links, _ := netlink.LinkList()
  318. linkMap := make(map[int]string)
  319. for _, l := range links {
  320. linkMap[l.Attrs().Index] = l.Attrs().Name
  321. }
  322. var liveRoutes []RouteItem
  323. routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
  324. if err == nil {
  325. for _, r := range routes {
  326. dst := "0.0.0.0/0"
  327. if r.Dst != nil {
  328. dst = r.Dst.String()
  329. }
  330. gw := ""
  331. if r.Gw != nil {
  332. gw = r.Gw.String()
  333. }
  334. tableName := "main"
  335. if name, ok := tableNames[r.Table]; ok {
  336. tableName = name
  337. } else if r.Table != 254 && r.Table != 0 {
  338. tableName = strconv.Itoa(r.Table)
  339. }
  340. isDyn := isDynamicProtocol(r.Protocol)
  341. isDirect := (r.Scope == netlink.SCOPE_LINK || r.Protocol == netlink.RouteProtocol(2)) && !isDyn && r.Protocol != 4
  342. isStatic := !isDirect && !isDyn
  343. isMulticast := r.Type == 5 || strings.HasPrefix(dst, "224.") || strings.HasPrefix(dst, "ff") || dst == "224.0.0.0/4"
  344. protoName := getRouteProtocolName(r.Protocol, r.Priority, isDirect, isMulticast)
  345. if len(r.MultiPath) > 0 {
  346. for _, nh := range r.MultiPath {
  347. nhGw := ""
  348. if nh.Gw != nil {
  349. nhGw = nh.Gw.String()
  350. }
  351. nhIfName := linkMap[nh.LinkIndex]
  352. cleanDst := strings.ReplaceAll(dst, "/", "_")
  353. cleanGw := nhGw
  354. if cleanGw == "" {
  355. cleanGw = nhIfName
  356. }
  357. routeID := fmt.Sprintf("kr-%d-%s-%s", r.Table, cleanDst, cleanGw)
  358. liveRoutes = append(liveRoutes, RouteItem{
  359. ID: routeID,
  360. DstAddress: dst,
  361. Gateway: nhGw,
  362. Interface: nhIfName,
  363. Distance: r.Priority,
  364. Protocol: protoName,
  365. RoutingTable: tableName,
  366. TableID: r.Table,
  367. Active: true,
  368. Connect: isDirect,
  369. Dynamic: isDyn,
  370. Static: isStatic,
  371. ECMP: true,
  372. IsMultiPath: true,
  373. Multicast: isMulticast,
  374. Disabled: false,
  375. })
  376. }
  377. continue
  378. }
  379. ifName := linkMap[r.LinkIndex]
  380. act := "lookup"
  381. if r.Type == 6 { // RTN_BLACKHOLE
  382. gw = "blackhole"
  383. act = "blackhole"
  384. }
  385. cleanDst := strings.ReplaceAll(dst, "/", "_")
  386. cleanGw := gw
  387. if cleanGw == "" {
  388. cleanGw = ifName
  389. }
  390. routeID := fmt.Sprintf("kr-%d-%s-%s", r.Table, cleanDst, cleanGw)
  391. liveRoutes = append(liveRoutes, RouteItem{
  392. ID: routeID,
  393. DstAddress: dst,
  394. Gateway: gw,
  395. Action: act,
  396. Interface: ifName,
  397. Distance: r.Priority,
  398. Protocol: protoName,
  399. RoutingTable: tableName,
  400. TableID: r.Table,
  401. Active: true,
  402. Connect: isDirect,
  403. Dynamic: isDyn,
  404. Static: isStatic,
  405. Multicast: isMulticast,
  406. Disabled: false,
  407. })
  408. }
  409. }
  410. return liveRoutes
  411. }
  412. func (m *RoutingManager) ListRoutes() []RouteItem {
  413. m.mu.Lock()
  414. defer m.mu.Unlock()
  415. liveRoutes := m.listLiveRoutesLocked()
  416. // Merge static routes that might be disabled or have custom metadata
  417. for _, sr := range m.staticRoutes {
  418. found := false
  419. for i, lr := range liveRoutes {
  420. gwMatches := sr.Gateway == "" || lr.Gateway == sr.Gateway || lr.Interface == sr.Gateway || (sr.Interface != "" && lr.Interface == sr.Interface)
  421. if lr.DstAddress == sr.DstAddress && lr.RoutingTable == sr.RoutingTable && gwMatches {
  422. liveRoutes[i].Comment = sr.Comment
  423. liveRoutes[i].Static = true
  424. liveRoutes[i].Dynamic = false
  425. liveRoutes[i].Protocol = "static"
  426. liveRoutes[i].ID = sr.ID
  427. if liveRoutes[i].Gateway == "" && sr.Gateway != "" {
  428. liveRoutes[i].Gateway = sr.Gateway
  429. }
  430. if sr.Distance > 0 && liveRoutes[i].Distance == 0 {
  431. liveRoutes[i].Distance = sr.Distance
  432. }
  433. liveRoutes[i].CheckGateway = sr.CheckGateway
  434. liveRoutes[i].CheckTarget = sr.CheckTarget
  435. liveRoutes[i].CheckInterval = sr.CheckInterval
  436. liveRoutes[i].CheckThreshold = sr.CheckThreshold
  437. liveRoutes[i].HealthStatus = sr.HealthStatus
  438. liveRoutes[i].HealthRTT = sr.HealthRTT
  439. liveRoutes[i].HealthLossPct = sr.HealthLossPct
  440. liveRoutes[i].HealthFailCount = sr.HealthFailCount
  441. liveRoutes[i].HealthLastCheck = sr.HealthLastCheck
  442. found = true
  443. break
  444. }
  445. }
  446. if !found {
  447. // Inactive or disabled static route
  448. srCopy := sr
  449. srCopy.Active = false
  450. srCopy.Static = true
  451. srCopy.Dynamic = false
  452. srCopy.Protocol = "static"
  453. liveRoutes = append(liveRoutes, srCopy)
  454. }
  455. }
  456. // Detect Equal-Cost Multi-Path (ECMP) routes
  457. groupCounts := make(map[string]int)
  458. for _, r := range liveRoutes {
  459. if r.Active && !r.Disabled {
  460. key := fmt.Sprintf("%s|%s|%d", r.DstAddress, r.RoutingTable, r.Distance)
  461. groupCounts[key]++
  462. }
  463. }
  464. for i := range liveRoutes {
  465. key := fmt.Sprintf("%s|%s|%d", liveRoutes[i].DstAddress, liveRoutes[i].RoutingTable, liveRoutes[i].Distance)
  466. if liveRoutes[i].IsMultiPath || (liveRoutes[i].Active && !liveRoutes[i].Disabled && groupCounts[key] > 1) {
  467. liveRoutes[i].ECMP = true
  468. }
  469. if liveRoutes[i].DstAddress == "224.0.0.0/4" || strings.HasPrefix(liveRoutes[i].DstAddress, "224.") || strings.HasPrefix(liveRoutes[i].DstAddress, "ff") {
  470. liveRoutes[i].Multicast = true
  471. }
  472. }
  473. sort.Slice(liveRoutes, func(i, j int) bool {
  474. if liveRoutes[i].RoutingTable != liveRoutes[j].RoutingTable {
  475. return liveRoutes[i].RoutingTable < liveRoutes[j].RoutingTable
  476. }
  477. if liveRoutes[i].DstAddress != liveRoutes[j].DstAddress {
  478. return liveRoutes[i].DstAddress < liveRoutes[j].DstAddress
  479. }
  480. return liveRoutes[i].Gateway < liveRoutes[j].Gateway
  481. })
  482. return liveRoutes
  483. }
  484. func (m *RoutingManager) SaveRoute(r RouteItem) (err error) {
  485. m.mu.Lock()
  486. defer m.mu.Unlock()
  487. before := append([]RouteItem(nil), m.staticRoutes...)
  488. dirty, applied := false, false
  489. defer func() {
  490. if err != nil && dirty {
  491. m.staticRoutes = before
  492. if applied {
  493. _ = m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
  494. }
  495. m.applyAll()
  496. _ = m.save()
  497. }
  498. }()
  499. if r.DstAddress == "" {
  500. return fmt.Errorf("destination address is required")
  501. }
  502. if r.DstAddress == "default" {
  503. r.DstAddress = "0.0.0.0/0"
  504. }
  505. if r.RoutingTable == "" {
  506. r.RoutingTable = "main"
  507. }
  508. if _, _, parseErr := net.ParseCIDR(r.DstAddress); parseErr != nil && net.ParseIP(r.DstAddress) == nil {
  509. return fmt.Errorf("invalid destination: %s", r.DstAddress)
  510. }
  511. dirty = true
  512. r.Static = true
  513. // 1. If old route details are provided from edit modal, remove old route first
  514. if r.OldDstAddress != "" {
  515. oldDist := 0
  516. if r.OldDistance != nil {
  517. oldDist = *r.OldDistance
  518. }
  519. oldRoute := RouteItem{
  520. DstAddress: r.OldDstAddress,
  521. Gateway: r.OldGateway,
  522. RoutingTable: r.OldRoutingTable,
  523. Distance: oldDist,
  524. Interface: r.OldInterface,
  525. }
  526. _ = m.removeRouteFromKernel(oldRoute)
  527. }
  528. // 2. If existing entry in m.staticRoutes exists, remove its old route from kernel
  529. foundIndex := -1
  530. for i, existing := range m.staticRoutes {
  531. matchByID := r.ID != "" && existing.ID == r.ID
  532. matchByExactParams := r.ID == "" &&
  533. normalizeDst(existing.DstAddress) == normalizeDst(r.DstAddress) &&
  534. existing.RoutingTable == r.RoutingTable &&
  535. existing.Gateway == r.Gateway &&
  536. existing.Interface == r.Interface &&
  537. existing.Distance == r.Distance
  538. if matchByID || matchByExactParams {
  539. foundIndex = i
  540. _ = m.removeRouteFromKernel(existing)
  541. break
  542. }
  543. }
  544. // 3. If r.ID starts with kr- (kernel static route being edited/adopted), remove it from kernel
  545. if strings.HasPrefix(r.ID, "kr-") {
  546. for _, lr := range m.listLiveRoutesLocked() {
  547. if lr.ID == r.ID {
  548. _ = m.removeRouteFromKernel(lr)
  549. break
  550. }
  551. }
  552. r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
  553. }
  554. if r.ID == "" {
  555. r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
  556. }
  557. if r.CheckInterval <= 0 {
  558. r.CheckInterval = 5
  559. }
  560. if r.CheckThreshold <= 0 {
  561. r.CheckThreshold = 3
  562. }
  563. if r.CheckGateway == "internet" && strings.TrimSpace(r.CheckTarget) == "" {
  564. r.CheckTarget = "1.1.1.1"
  565. }
  566. if r.CheckGateway != "" && r.CheckGateway != "none" {
  567. if r.HealthStatus == "" {
  568. r.HealthStatus = "checking"
  569. }
  570. } else {
  571. r.CheckGateway = "none"
  572. r.HealthStatus = "none"
  573. }
  574. if foundIndex >= 0 {
  575. if r.HealthStatus == "checking" && m.staticRoutes[foundIndex].HealthStatus != "" && m.staticRoutes[foundIndex].CheckGateway == r.CheckGateway {
  576. r.HealthStatus = m.staticRoutes[foundIndex].HealthStatus
  577. r.HealthRTT = m.staticRoutes[foundIndex].HealthRTT
  578. r.HealthLossPct = m.staticRoutes[foundIndex].HealthLossPct
  579. r.HealthLastCheck = m.staticRoutes[foundIndex].HealthLastCheck
  580. }
  581. m.staticRoutes[foundIndex] = r
  582. } else {
  583. m.staticRoutes = append(m.staticRoutes, r)
  584. }
  585. for i := range m.staticRoutes {
  586. if m.staticRoutes[i].ID == r.ID {
  587. if !m.staticRoutes[i].Disabled && m.staticRoutes[i].HealthStatus != "offline" {
  588. m.staticRoutes[i].Active = true
  589. } else {
  590. m.staticRoutes[i].Active = false
  591. }
  592. break
  593. }
  594. }
  595. if saveErr := m.save(); saveErr != nil {
  596. return saveErr
  597. }
  598. if err := m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance); err != nil {
  599. return err
  600. }
  601. applied = true
  602. if r.OldDstAddress != "" {
  603. oldDist := 0
  604. if r.OldDistance != nil {
  605. oldDist = *r.OldDistance
  606. }
  607. if r.OldDstAddress != r.DstAddress || r.OldRoutingTable != r.RoutingTable || oldDist != r.Distance {
  608. if err := m.syncRouteGroupToKernel(r.OldDstAddress, r.OldRoutingTable, oldDist); err != nil {
  609. return err
  610. }
  611. }
  612. }
  613. return nil
  614. }
  615. func (m *RoutingManager) DeleteRoute(id string) error {
  616. m.mu.Lock()
  617. defer m.mu.Unlock()
  618. var updated []RouteItem
  619. var target *RouteItem
  620. for _, r := range m.staticRoutes {
  621. if r.ID == id {
  622. target = &r
  623. } else {
  624. updated = append(updated, r)
  625. }
  626. }
  627. if target != nil {
  628. m.staticRoutes = updated
  629. _ = m.save()
  630. _ = m.syncRouteGroupToKernel(target.DstAddress, target.RoutingTable, target.Distance)
  631. return nil
  632. }
  633. // If not in staticRoutes, could be a live route (e.g. kr-...)
  634. for _, lr := range m.listLiveRoutesLocked() {
  635. if lr.ID == id {
  636. _ = m.removeRouteFromKernel(lr)
  637. return nil
  638. }
  639. }
  640. return fmt.Errorf("route not found")
  641. }
  642. func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
  643. m.mu.Lock()
  644. defer m.mu.Unlock()
  645. for i := range m.staticRoutes {
  646. if m.staticRoutes[i].ID == id {
  647. m.staticRoutes[i].Disabled = disabled
  648. if !disabled && m.staticRoutes[i].HealthStatus != "offline" {
  649. m.staticRoutes[i].Active = true
  650. } else if disabled {
  651. m.staticRoutes[i].Active = false
  652. }
  653. _ = m.save()
  654. _ = m.syncRouteGroupToKernel(m.staticRoutes[i].DstAddress, m.staticRoutes[i].RoutingTable, m.staticRoutes[i].Distance)
  655. return nil
  656. }
  657. }
  658. // If not in staticRoutes, could be a kernel static route to disable
  659. if disabled {
  660. for _, lr := range m.listLiveRoutesLocked() {
  661. if lr.ID == id {
  662. lr.Disabled = true
  663. lr.Static = true
  664. lr.Active = false
  665. lr.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
  666. m.staticRoutes = append(m.staticRoutes, lr)
  667. _ = m.save()
  668. _ = m.syncRouteGroupToKernel(lr.DstAddress, lr.RoutingTable, lr.Distance)
  669. return nil
  670. }
  671. }
  672. }
  673. return fmt.Errorf("route not found")
  674. }
  675. func normalizeDst(dst string) string {
  676. dst = strings.TrimSpace(dst)
  677. if dst == "" || dst == "default" || dst == "0.0.0.0/0" {
  678. return "default"
  679. }
  680. if !strings.Contains(dst, "/") && net.ParseIP(dst) != nil {
  681. return dst + "/32"
  682. }
  683. return dst
  684. }
  685. func (m *RoutingManager) syncRouteGroupToKernel(dstAddress, routingTable string, distance int) error {
  686. normDst := normalizeDst(dstAddress)
  687. tbl := strings.TrimSpace(routingTable)
  688. if tbl == "" {
  689. tbl = "main"
  690. }
  691. var activeRoutes []RouteItem
  692. for _, r := range m.staticRoutes {
  693. rDst := normalizeDst(r.DstAddress)
  694. rTbl := strings.TrimSpace(r.RoutingTable)
  695. if rTbl == "" {
  696. rTbl = "main"
  697. }
  698. if rDst == normDst && rTbl == tbl && r.Distance == distance {
  699. if !r.Disabled && r.Active && r.HealthStatus != "offline" {
  700. activeRoutes = append(activeRoutes, r)
  701. }
  702. }
  703. }
  704. if len(activeRoutes) == 0 {
  705. // Safety guard: if all default routes are marked offline, do NOT delete the default route into a blackout!
  706. // Fallback to keeping all enabled static routes in the group active so recovery traffic and emergency pings can flow.
  707. if normDst == "default" {
  708. var fallbackRoutes []RouteItem
  709. for _, r := range m.staticRoutes {
  710. rDst := normalizeDst(r.DstAddress)
  711. rTbl := strings.TrimSpace(r.RoutingTable)
  712. if rTbl == "" {
  713. rTbl = "main"
  714. }
  715. if rDst == normDst && rTbl == tbl && r.Distance == distance && !r.Disabled {
  716. fallbackRoutes = append(fallbackRoutes, r)
  717. }
  718. }
  719. if len(fallbackRoutes) > 0 {
  720. log.Printf("[RouteManager] All default routes offline in group! Keeping fallback routes active to prevent blackout")
  721. activeRoutes = fallbackRoutes
  722. }
  723. }
  724. }
  725. if len(activeRoutes) == 0 {
  726. args := []string{"route", "del", normDst}
  727. if distance > 0 {
  728. args = append(args, "metric", strconv.Itoa(distance))
  729. }
  730. if tbl != "main" {
  731. args = append(args, "table", tbl)
  732. }
  733. args = append(args, "proto", "static")
  734. _ = exec.Command("ip", args...).Run()
  735. // Also delete blackhole route variant if it existed
  736. bhArgs := []string{"route", "del", "blackhole", normDst}
  737. if distance > 0 {
  738. bhArgs = append(bhArgs, "metric", strconv.Itoa(distance))
  739. }
  740. if tbl != "main" {
  741. bhArgs = append(bhArgs, "table", tbl)
  742. }
  743. _ = exec.Command("ip", bhArgs...).Run()
  744. return nil
  745. }
  746. if len(activeRoutes) == 1 {
  747. r := activeRoutes[0]
  748. gw := strings.TrimSpace(r.Gateway)
  749. isBlackhole := strings.EqualFold(gw, "blackhole") || strings.EqualFold(r.Action, "blackhole")
  750. var args []string
  751. if isBlackhole {
  752. args = []string{"route", "replace", "blackhole", normDst}
  753. } else {
  754. args = []string{"route", "replace", normDst}
  755. if gw != "" {
  756. if net.ParseIP(gw) != nil {
  757. args = append(args, "via", gw)
  758. } else {
  759. args = append(args, "dev", gw)
  760. }
  761. }
  762. iface := strings.TrimSpace(r.Interface)
  763. if iface != "" && gw != iface && (gw == "" || net.ParseIP(gw) != nil) {
  764. args = append(args, "dev", iface)
  765. }
  766. if gw != "" && iface != "" && net.ParseIP(gw) != nil {
  767. args = append(args, "onlink")
  768. }
  769. }
  770. if distance > 0 {
  771. args = append(args, "metric", strconv.Itoa(distance))
  772. }
  773. if tbl != "main" {
  774. args = append(args, "table", tbl)
  775. }
  776. args = append(args, "proto", "static")
  777. cmd := exec.Command("ip", args...)
  778. if out, err := cmd.CombinedOutput(); err != nil {
  779. return fmt.Errorf("failed to apply route (%s): %s: %w", strings.Join(args, " "), string(out), err)
  780. }
  781. return nil
  782. }
  783. // Multiple routes: ECMP multipath route
  784. args := []string{"route", "replace", normDst, "scope", "global"}
  785. if distance > 0 {
  786. args = append(args, "metric", strconv.Itoa(distance))
  787. }
  788. if tbl != "main" {
  789. args = append(args, "table", tbl)
  790. }
  791. args = append(args, "proto", "static")
  792. for _, r := range activeRoutes {
  793. args = append(args, "nexthop")
  794. gw := strings.TrimSpace(r.Gateway)
  795. if gw != "" && net.ParseIP(gw) != nil {
  796. args = append(args, "via", gw)
  797. }
  798. iface := strings.TrimSpace(r.Interface)
  799. if iface == "" && gw != "" && net.ParseIP(gw) == nil {
  800. iface = gw
  801. }
  802. if iface != "" {
  803. args = append(args, "dev", iface)
  804. }
  805. if gw != "" && iface != "" && net.ParseIP(gw) != nil {
  806. args = append(args, "onlink")
  807. }
  808. args = append(args, "weight", "1")
  809. }
  810. cmd := exec.Command("ip", args...)
  811. if out, err := cmd.CombinedOutput(); err != nil {
  812. return fmt.Errorf("failed to apply multipath route (%s): %s: %w", strings.Join(args, " "), string(out), err)
  813. }
  814. return nil
  815. }
  816. func (m *RoutingManager) applyRouteToKernel(r RouteItem) error {
  817. return m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
  818. }
  819. func (m *RoutingManager) removeRouteFromKernel(r RouteItem) error {
  820. return m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
  821. }
  822. // ==================== Policy Rules (/ip route rule) ====================
  823. func (m *RoutingManager) ListPolicyRules() []PolicyRuleItem {
  824. m.mu.Lock()
  825. defer m.mu.Unlock()
  826. // Query live rules from kernel
  827. rules, err := netlink.RuleList(netlink.FAMILY_V4)
  828. if err != nil {
  829. return m.policyRules
  830. }
  831. tableNames := m.getTableMap()
  832. var res []PolicyRuleItem
  833. userRulesByPrio := make(map[int]PolicyRuleItem)
  834. for _, pr := range m.policyRules {
  835. userRulesByPrio[pr.Priority] = pr
  836. }
  837. for _, r := range rules {
  838. tableName := "main"
  839. if name, ok := tableNames[r.Table]; ok {
  840. tableName = name
  841. } else if r.Table != 254 && r.Table != 0 {
  842. tableName = strconv.Itoa(r.Table)
  843. }
  844. src := ""
  845. if r.Src != nil {
  846. src = r.Src.String()
  847. }
  848. dst := ""
  849. if r.Dst != nil {
  850. dst = r.Dst.String()
  851. }
  852. markStr := ""
  853. if r.Mark > 0 {
  854. markStr = fmt.Sprintf("0x%x (%d)", r.Mark, r.Mark)
  855. }
  856. id := fmt.Sprintf("rule-%d-%d", r.Priority, r.Table)
  857. comment := ""
  858. isDefault := false
  859. isSystem := false
  860. if r.Priority == 0 || r.Priority == 32766 || r.Priority == 32767 {
  861. isDefault = true
  862. isSystem = true
  863. } else if userRule, ok := userRulesByPrio[r.Priority]; ok {
  864. if userRule.ID != "" {
  865. id = userRule.ID
  866. }
  867. comment = userRule.Comment
  868. isDefault = false
  869. isSystem = false
  870. } else {
  871. // System/daemon rule not added by user in debtik (e.g. WireGuard, IPsec)
  872. isSystem = true
  873. }
  874. res = append(res, PolicyRuleItem{
  875. ID: id,
  876. Priority: r.Priority,
  877. SrcAddress: src,
  878. DstAddress: dst,
  879. RoutingMark: markStr,
  880. Table: tableName,
  881. TableID: r.Table,
  882. Action: "lookup",
  883. Comment: comment,
  884. IsDefault: isDefault,
  885. IsSystem: isSystem,
  886. })
  887. }
  888. return res
  889. }
  890. func (m *RoutingManager) SavePolicyRule(pr PolicyRuleItem) error {
  891. m.mu.Lock()
  892. defer m.mu.Unlock()
  893. if pr.Table == "" {
  894. pr.Table = "main"
  895. }
  896. if pr.ID == "" {
  897. pr.ID = fmt.Sprintf("prule-%d", time.Now().UnixNano())
  898. }
  899. if pr.Priority == 0 {
  900. pr.Priority = 1000 + len(m.policyRules)*10
  901. }
  902. if err := m.applyRuleToKernel(pr); err != nil {
  903. return err
  904. }
  905. m.policyRules = append(m.policyRules, pr)
  906. return m.save()
  907. }
  908. func (m *RoutingManager) DeletePolicyRule(id string) error {
  909. m.mu.Lock()
  910. defer m.mu.Unlock()
  911. var updated []PolicyRuleItem
  912. var target *PolicyRuleItem
  913. for _, pr := range m.policyRules {
  914. if pr.ID == id || fmt.Sprintf("rule-%d-%d", pr.Priority, pr.TableID) == id {
  915. target = &pr
  916. } else {
  917. updated = append(updated, pr)
  918. }
  919. }
  920. if target != nil {
  921. _ = m.removeRuleFromKernel(*target)
  922. } else if strings.HasPrefix(id, "rule-") {
  923. parts := strings.Split(id, "-")
  924. if len(parts) >= 2 {
  925. if prio, err := strconv.Atoi(parts[1]); err == nil && prio > 0 {
  926. _ = exec.Command("ip", "rule", "del", "pref", strconv.Itoa(prio)).Run()
  927. }
  928. }
  929. }
  930. m.policyRules = updated
  931. return m.save()
  932. }
  933. func (m *RoutingManager) applyRuleToKernel(pr PolicyRuleItem) error {
  934. args := []string{"rule", "add"}
  935. if pr.Priority > 0 {
  936. args = append(args, "pref", strconv.Itoa(pr.Priority))
  937. }
  938. if pr.SrcAddress != "" {
  939. args = append(args, "from", pr.SrcAddress)
  940. }
  941. if pr.DstAddress != "" {
  942. args = append(args, "to", pr.DstAddress)
  943. }
  944. if pr.RoutingMark != "" {
  945. // Clean mark string if it has (hex)
  946. markClean := pr.RoutingMark
  947. if strings.Contains(markClean, "(") {
  948. markClean = strings.TrimSpace(markClean[:strings.Index(markClean, "(")])
  949. }
  950. args = append(args, "fwmark", markClean)
  951. }
  952. if pr.Table != "" {
  953. args = append(args, "lookup", pr.Table)
  954. }
  955. cmd := exec.Command("ip", args...)
  956. out, err := cmd.CombinedOutput()
  957. if err != nil {
  958. return fmt.Errorf("failed to add rule (%s): %s: %w", strings.Join(args, " "), string(out), err)
  959. }
  960. return nil
  961. }
  962. func (m *RoutingManager) removeRuleFromKernel(pr PolicyRuleItem) error {
  963. if pr.Priority > 0 {
  964. _ = exec.Command("ip", "rule", "del", "pref", strconv.Itoa(pr.Priority)).Run()
  965. return nil
  966. }
  967. args := []string{"rule", "del"}
  968. if pr.RoutingMark != "" {
  969. markClean := pr.RoutingMark
  970. if strings.Contains(markClean, "(") {
  971. markClean = strings.TrimSpace(markClean[:strings.Index(markClean, "(")])
  972. }
  973. args = append(args, "fwmark", markClean)
  974. }
  975. if pr.Table != "" {
  976. args = append(args, "lookup", pr.Table)
  977. }
  978. _ = exec.Command("ip", args...).Run()
  979. return nil
  980. }
  981. // ==================== Routing Tables ====================
  982. func (m *RoutingManager) ListTables() []RoutingTableItem {
  983. m.mu.Lock()
  984. defer m.mu.Unlock()
  985. tables := []RoutingTableItem{
  986. {Name: "main", TableID: 254, Comment: "Default system routing table"},
  987. {Name: "default", TableID: 253, Comment: "Default fallback table"},
  988. {Name: "local", TableID: 255, Comment: "Local kernel device table"},
  989. }
  990. tables = append(tables, m.customTables...)
  991. return tables
  992. }
  993. func (m *RoutingManager) SaveTable(t RoutingTableItem) error {
  994. m.mu.Lock()
  995. defer m.mu.Unlock()
  996. if t.Name == "" {
  997. return fmt.Errorf("table name is required")
  998. }
  999. if t.TableID == 0 {
  1000. // Auto assign FIB ID starting at 100
  1001. t.TableID = 100 + len(m.customTables)
  1002. }
  1003. for _, existing := range m.customTables {
  1004. if existing.Name == t.Name || existing.TableID == t.TableID {
  1005. return fmt.Errorf("table with name %q or ID %d already exists", t.Name, t.TableID)
  1006. }
  1007. }
  1008. m.customTables = append(m.customTables, t)
  1009. _ = m.save()
  1010. m.syncCustomTablesToSystem()
  1011. return nil
  1012. }
  1013. func (m *RoutingManager) getTableMap() map[int]string {
  1014. tableMap := map[int]string{
  1015. 254: "main",
  1016. 253: "default",
  1017. 255: "local",
  1018. }
  1019. // Read /etc/iproute2/rt_tables
  1020. if f, err := os.Open(RTTableFilePath); err == nil {
  1021. defer f.Close()
  1022. scanner := bufio.NewScanner(f)
  1023. for scanner.Scan() {
  1024. line := strings.TrimSpace(scanner.Text())
  1025. if line == "" || strings.HasPrefix(line, "#") {
  1026. continue
  1027. }
  1028. parts := strings.Fields(line)
  1029. if len(parts) >= 2 {
  1030. if id, err := strconv.Atoi(parts[0]); err == nil {
  1031. tableMap[id] = parts[1]
  1032. }
  1033. }
  1034. }
  1035. }
  1036. for _, t := range m.customTables {
  1037. tableMap[t.TableID] = t.Name
  1038. }
  1039. return tableMap
  1040. }
  1041. func (m *RoutingManager) Stop() {
  1042. if m.stopChan != nil {
  1043. select {
  1044. case <-m.stopChan:
  1045. default:
  1046. close(m.stopChan)
  1047. }
  1048. }
  1049. }
  1050. // RenameInterface updates static routes and policy rules when an interface is renamed.
  1051. func (m *RoutingManager) RenameInterface(oldName, newName string) {
  1052. m.mu.Lock()
  1053. defer m.mu.Unlock()
  1054. changed := false
  1055. for i := range m.staticRoutes {
  1056. if m.staticRoutes[i].Interface == oldName {
  1057. m.staticRoutes[i].Interface = newName
  1058. changed = true
  1059. }
  1060. if m.staticRoutes[i].Gateway == oldName {
  1061. m.staticRoutes[i].Gateway = newName
  1062. changed = true
  1063. }
  1064. }
  1065. if changed {
  1066. _ = m.save()
  1067. }
  1068. }