manager.go 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643
  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. "debtik/internal/sdwan"
  17. "github.com/vishvananda/netlink"
  18. )
  19. var (
  20. StaticRoutesStorePath = "/etc/debtik/routing/static_routes.json"
  21. PolicyRulesStorePath = "/etc/debtik/routing/policy_rules.json"
  22. CustomTablesStorePath = "/etc/debtik/routing/custom_tables.json"
  23. RTTableFilePath = "/etc/iproute2/rt_tables"
  24. )
  25. type RoutingManager struct {
  26. mu sync.Mutex
  27. staticRoutes []RouteItem
  28. policyRules []PolicyRuleItem
  29. customTables []RoutingTableItem
  30. stopChan chan struct{}
  31. }
  32. func NewRoutingManager() *RoutingManager {
  33. m := &RoutingManager{
  34. staticRoutes: make([]RouteItem, 0),
  35. policyRules: make([]PolicyRuleItem, 0),
  36. customTables: make([]RoutingTableItem, 0),
  37. stopChan: make(chan struct{}),
  38. }
  39. m.initRTTableFile()
  40. m.load()
  41. m.syncCustomTablesToSystem()
  42. m.applyAll()
  43. m.startHealthCheckWorker()
  44. m.startRouteSyncWorker()
  45. sdwan.RegisterStateChangeCallback(func() {
  46. m.SyncSDWANRoutes()
  47. })
  48. return m
  49. }
  50. func (m *RoutingManager) initRTTableFile() {
  51. _ = os.MkdirAll(filepath.Dir(RTTableFilePath), 0755)
  52. if _, err := os.Stat(RTTableFilePath); os.IsNotExist(err) {
  53. // Copy from /usr/share/iproute2/rt_tables or seed default
  54. content, err := os.ReadFile("/usr/share/iproute2/rt_tables")
  55. if err == nil {
  56. _ = os.WriteFile(RTTableFilePath, content, 0644)
  57. } else {
  58. defaultContent := "# reserved values\n255\tlocal\n254\tmain\n253\tdefault\n0\tunspec\n"
  59. _ = os.WriteFile(RTTableFilePath, []byte(defaultContent), 0644)
  60. }
  61. }
  62. }
  63. func (m *RoutingManager) load() {
  64. if data, err := os.ReadFile(StaticRoutesStorePath); err == nil {
  65. _ = json.Unmarshal(data, &m.staticRoutes)
  66. }
  67. if data, err := os.ReadFile(PolicyRulesStorePath); err == nil {
  68. _ = json.Unmarshal(data, &m.policyRules)
  69. }
  70. if data, err := os.ReadFile(CustomTablesStorePath); err == nil {
  71. _ = json.Unmarshal(data, &m.customTables)
  72. }
  73. // Deduplicate static routes: if duplicates exist, keep the latest entry
  74. seenIDs := make(map[string]bool)
  75. seenKeys := make(map[string]bool)
  76. var dedupedRev []RouteItem
  77. hasDups := false
  78. // Traverse from end to start so latest updates take precedence
  79. for i := len(m.staticRoutes) - 1; i >= 0; i-- {
  80. r := m.staticRoutes[i]
  81. if r.ID == "" {
  82. r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
  83. }
  84. normDst := normalizeDst(r.DstAddress)
  85. tbl := r.RoutingTable
  86. if tbl == "" {
  87. tbl = "main"
  88. }
  89. gw := strings.TrimSpace(r.Gateway)
  90. iface := strings.TrimSpace(r.Interface)
  91. routeKey := fmt.Sprintf("%s|%s|%s|%s|%d", normDst, tbl, gw, iface, r.Distance)
  92. if seenIDs[r.ID] || seenKeys[routeKey] {
  93. log.Printf("[RouteManager] Pruning duplicate static route on load: ID=%s dst=%s gw=%s iface=%s", r.ID, r.DstAddress, r.Gateway, r.Interface)
  94. hasDups = true
  95. continue
  96. }
  97. seenIDs[r.ID] = true
  98. seenKeys[routeKey] = true
  99. dedupedRev = append(dedupedRev, r)
  100. }
  101. // Reverse back to original order
  102. var deduped []RouteItem
  103. for i := len(dedupedRev) - 1; i >= 0; i-- {
  104. deduped = append(deduped, dedupedRev[i])
  105. }
  106. if hasDups {
  107. m.staticRoutes = deduped
  108. _ = m.save()
  109. }
  110. for i := range m.staticRoutes {
  111. if !m.staticRoutes[i].Disabled && m.staticRoutes[i].HealthStatus != "offline" {
  112. m.staticRoutes[i].Active = true
  113. }
  114. }
  115. }
  116. func (m *RoutingManager) save() error {
  117. _ = os.MkdirAll("/etc/debtik/routing", 0755)
  118. if data, err := json.MarshalIndent(m.staticRoutes, "", " "); err != nil {
  119. return err
  120. } else if err = os.WriteFile(StaticRoutesStorePath, data, 0600); err != nil {
  121. return err
  122. }
  123. if data, err := json.MarshalIndent(m.policyRules, "", " "); err != nil {
  124. return err
  125. } else if err = os.WriteFile(PolicyRulesStorePath, data, 0600); err != nil {
  126. return err
  127. }
  128. if data, err := json.MarshalIndent(m.customTables, "", " "); err != nil {
  129. return err
  130. } else if err = os.WriteFile(CustomTablesStorePath, data, 0600); err != nil {
  131. return err
  132. }
  133. return nil
  134. }
  135. func (m *RoutingManager) syncCustomTablesToSystem() {
  136. content, _ := os.ReadFile(RTTableFilePath)
  137. str := string(content)
  138. var sb strings.Builder
  139. sb.WriteString(str)
  140. for _, t := range m.customTables {
  141. check := fmt.Sprintf("\t%s", t.Name)
  142. if !strings.Contains(str, check) {
  143. sb.WriteString(fmt.Sprintf("%d\t%s\n", t.TableID, t.Name))
  144. }
  145. }
  146. _ = os.WriteFile(RTTableFilePath, []byte(sb.String()), 0644)
  147. }
  148. func (m *RoutingManager) cleanRogueDefaultRoutes() {
  149. hasManagedDefault := false
  150. for _, r := range m.staticRoutes {
  151. if normalizeDst(r.DstAddress) == "default" && !r.Disabled {
  152. hasManagedDefault = true
  153. break
  154. }
  155. }
  156. if !hasManagedDefault {
  157. return
  158. }
  159. routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
  160. if err != nil {
  161. return
  162. }
  163. links, _ := netlink.LinkList()
  164. linkMap := make(map[int]string)
  165. for _, l := range links {
  166. linkMap[l.Attrs().Index] = l.Attrs().Name
  167. }
  168. for _, r := range routes {
  169. if (r.Table == 254 || r.Table == 0) && r.Dst == nil && len(r.MultiPath) == 0 {
  170. gw := ""
  171. if r.Gw != nil {
  172. gw = r.Gw.String()
  173. }
  174. ifName := linkMap[r.LinkIndex]
  175. // If priority is 0 (unmetered route from dhclient or boot) and protocol != RTPROT_STATIC:
  176. if r.Priority == 0 && int(r.Protocol) != 4 {
  177. log.Printf("[RouteManager] Removing rogue unmanaged default route: via %s dev %s proto %d", gw, ifName, r.Protocol)
  178. _ = netlink.RouteDel(&r)
  179. }
  180. }
  181. }
  182. }
  183. func (m *RoutingManager) getTableIDByName(name string) int {
  184. name = strings.TrimSpace(name)
  185. if name == "" || name == "main" {
  186. return 254
  187. }
  188. if name == "default" {
  189. return 253
  190. }
  191. if name == "local" {
  192. return 255
  193. }
  194. for id, n := range m.getTableMap() {
  195. if strings.EqualFold(n, name) {
  196. return id
  197. }
  198. }
  199. if id, err := strconv.Atoi(name); err == nil {
  200. return id
  201. }
  202. return 254
  203. }
  204. func (m *RoutingManager) startRouteSyncWorker() {
  205. ticker := time.NewTicker(3 * time.Second)
  206. go func() {
  207. for {
  208. select {
  209. case <-m.stopChan:
  210. ticker.Stop()
  211. return
  212. case <-ticker.C:
  213. m.syncAllRoutesWorker()
  214. }
  215. }
  216. }()
  217. }
  218. func (m *RoutingManager) syncAllRoutesWorker() {
  219. m.mu.Lock()
  220. defer m.mu.Unlock()
  221. // Unmanaged routes belong to DHCP, the host or other routing daemons.
  222. liveRoutes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
  223. if err != nil {
  224. return
  225. }
  226. type groupKey struct {
  227. dst string
  228. table int
  229. dist int
  230. }
  231. kernelRouteGroups := make(map[groupKey]bool)
  232. for _, lr := range liveRoutes {
  233. dst := "default"
  234. if lr.Dst != nil {
  235. dst = lr.Dst.String()
  236. }
  237. tbl := lr.Table
  238. if tbl == 0 {
  239. tbl = 254
  240. }
  241. k := groupKey{dst: normalizeDst(dst), table: tbl, dist: lr.Priority}
  242. kernelRouteGroups[k] = true
  243. }
  244. seen := make(map[groupKey]bool)
  245. for _, r := range m.staticRoutes {
  246. if r.Disabled || r.HealthStatus == "offline" {
  247. continue
  248. }
  249. normDst := normalizeDst(r.DstAddress)
  250. tableID := m.getTableIDByName(r.RoutingTable)
  251. k := groupKey{dst: normDst, table: tableID, dist: r.Distance}
  252. if !seen[k] {
  253. seen[k] = true
  254. if !kernelRouteGroups[k] {
  255. log.Printf("[RouteSync] Missing kernel route %s table %s (id %d) metric %d -> Restoring", normDst, r.RoutingTable, tableID, r.Distance)
  256. if err := m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance); err != nil {
  257. log.Printf("route synchronization failed: %v", err)
  258. }
  259. }
  260. }
  261. }
  262. }
  263. func (m *RoutingManager) applyAll() {
  264. // Unmanaged routes belong to DHCP, the host or other routing daemons.
  265. // Apply all static routes grouped by (dst, table, distance) for ECMP
  266. type groupKey struct {
  267. dst string
  268. table string
  269. dist int
  270. }
  271. seen := make(map[groupKey]bool)
  272. for _, r := range m.staticRoutes {
  273. normDst := normalizeDst(r.DstAddress)
  274. tbl := strings.TrimSpace(r.RoutingTable)
  275. if tbl == "" {
  276. tbl = "main"
  277. }
  278. k := groupKey{dst: normDst, table: tbl, dist: r.Distance}
  279. if !seen[k] {
  280. seen[k] = true
  281. _ = m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
  282. }
  283. }
  284. // Apply policy rules
  285. for _, pr := range m.policyRules {
  286. _ = m.applyRuleToKernel(pr)
  287. }
  288. }
  289. // ==================== Routes (/ip route) ====================
  290. func isDynamicProtocol(proto netlink.RouteProtocol) bool {
  291. switch int(proto) {
  292. case 11, // RTPROT_ZEBRA
  293. 12, // RTPROT_BIRD
  294. 16, // RTPROT_DHCP
  295. 42, // RTPROT_BABEL
  296. 186, // RTPROT_BGP
  297. 187, // RTPROT_ISIS
  298. 188, // RTPROT_OSPF
  299. 189, // RTPROT_RIP
  300. 192: // RTPROT_EIGRP
  301. return true
  302. default:
  303. return false
  304. }
  305. }
  306. func getRouteProtocolName(proto netlink.RouteProtocol, priority int, isDirect bool, isMulticast bool) string {
  307. if isMulticast {
  308. return "multicast"
  309. }
  310. switch int(proto) {
  311. case 16:
  312. return "dhcp"
  313. case 4:
  314. return "static"
  315. case 186:
  316. return "bgp"
  317. case 188:
  318. return "ospf"
  319. case 189:
  320. return "rip"
  321. case 11:
  322. return "zebra"
  323. case 42:
  324. return "babel"
  325. case 12: // RTPROT_BIRD
  326. if priority == 20 {
  327. return "bgp"
  328. } else if priority == 200 {
  329. return "ibgp"
  330. } else if priority == 110 {
  331. return "ospf"
  332. } else if priority == 120 {
  333. return "rip"
  334. }
  335. return "bgp"
  336. case 2:
  337. return "connected"
  338. case 3:
  339. return "boot"
  340. default:
  341. if isDirect {
  342. return "connected"
  343. }
  344. if priority == 20 {
  345. return "bgp"
  346. } else if priority == 200 {
  347. return "ibgp"
  348. } else if priority == 110 {
  349. return "ospf"
  350. } else if priority == 120 {
  351. return "rip"
  352. }
  353. return "static"
  354. }
  355. }
  356. func (m *RoutingManager) listLiveRoutesLocked() []RouteItem {
  357. tableNames := m.getTableMap()
  358. links, _ := netlink.LinkList()
  359. linkMap := make(map[int]string)
  360. for _, l := range links {
  361. linkMap[l.Attrs().Index] = l.Attrs().Name
  362. }
  363. var liveRoutes []RouteItem
  364. routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
  365. if err == nil {
  366. for _, r := range routes {
  367. dst := "0.0.0.0/0"
  368. if r.Dst != nil {
  369. dst = r.Dst.String()
  370. }
  371. gw := ""
  372. if r.Gw != nil {
  373. gw = r.Gw.String()
  374. }
  375. tableName := "main"
  376. if name, ok := tableNames[r.Table]; ok {
  377. tableName = name
  378. } else if r.Table != 254 && r.Table != 0 {
  379. tableName = strconv.Itoa(r.Table)
  380. }
  381. isDyn := isDynamicProtocol(r.Protocol)
  382. isDirect := (r.Scope == netlink.SCOPE_LINK || r.Protocol == netlink.RouteProtocol(2)) && !isDyn && r.Protocol != 4
  383. isStatic := !isDirect && !isDyn
  384. isMulticast := r.Type == 5 || strings.HasPrefix(dst, "224.") || strings.HasPrefix(dst, "ff") || dst == "224.0.0.0/4"
  385. protoName := getRouteProtocolName(r.Protocol, r.Priority, isDirect, isMulticast)
  386. if len(r.MultiPath) > 0 {
  387. for _, nh := range r.MultiPath {
  388. nhGw := ""
  389. if nh.Gw != nil {
  390. nhGw = nh.Gw.String()
  391. }
  392. nhIfName := linkMap[nh.LinkIndex]
  393. cleanDst := strings.ReplaceAll(dst, "/", "_")
  394. cleanGw := nhGw
  395. if cleanGw == "" {
  396. cleanGw = nhIfName
  397. }
  398. routeID := fmt.Sprintf("kr-%d-%s-%s", r.Table, cleanDst, cleanGw)
  399. liveRoutes = append(liveRoutes, RouteItem{
  400. ID: routeID,
  401. DstAddress: dst,
  402. Gateway: nhGw,
  403. Interface: nhIfName,
  404. Distance: r.Priority,
  405. Protocol: protoName,
  406. RoutingTable: tableName,
  407. TableID: r.Table,
  408. Active: true,
  409. Connect: isDirect,
  410. Dynamic: isDyn,
  411. Static: isStatic,
  412. ECMP: true,
  413. IsMultiPath: true,
  414. Multicast: isMulticast,
  415. Disabled: false,
  416. })
  417. }
  418. continue
  419. }
  420. ifName := linkMap[r.LinkIndex]
  421. act := "lookup"
  422. if r.Type == 6 { // RTN_BLACKHOLE
  423. gw = "blackhole"
  424. act = "blackhole"
  425. }
  426. cleanDst := strings.ReplaceAll(dst, "/", "_")
  427. cleanGw := gw
  428. if cleanGw == "" {
  429. cleanGw = ifName
  430. }
  431. routeID := fmt.Sprintf("kr-%d-%s-%s", r.Table, cleanDst, cleanGw)
  432. liveRoutes = append(liveRoutes, RouteItem{
  433. ID: routeID,
  434. DstAddress: dst,
  435. Gateway: gw,
  436. Action: act,
  437. Interface: ifName,
  438. Distance: r.Priority,
  439. Protocol: protoName,
  440. RoutingTable: tableName,
  441. TableID: r.Table,
  442. Active: true,
  443. Connect: isDirect,
  444. Dynamic: isDyn,
  445. Static: isStatic,
  446. Multicast: isMulticast,
  447. Disabled: false,
  448. })
  449. }
  450. }
  451. return liveRoutes
  452. }
  453. func (m *RoutingManager) ListRoutes() []RouteItem {
  454. m.mu.Lock()
  455. defer m.mu.Unlock()
  456. liveRoutes := m.listLiveRoutesLocked()
  457. // Merge static routes that might be disabled or have custom metadata
  458. matchedLive := make(map[int]bool)
  459. for _, sr := range m.staticRoutes {
  460. normSrDst := normalizeDst(sr.DstAddress)
  461. found := false
  462. for i, lr := range liveRoutes {
  463. if matchedLive[i] {
  464. continue
  465. }
  466. if normalizeDst(lr.DstAddress) != normSrDst {
  467. continue
  468. }
  469. if lr.RoutingTable != sr.RoutingTable {
  470. continue
  471. }
  472. gwMatches := sr.Gateway == "" || lr.Gateway == sr.Gateway || lr.Interface == sr.Gateway || (sr.Interface != "" && lr.Interface == sr.Interface)
  473. if !gwMatches && (sdwan.IsSDWANGroup(sr.Interface) || sdwan.IsSDWANGroup(sr.Gateway)) {
  474. targetSDWAN := sr.Interface
  475. if !sdwan.IsSDWANGroup(targetSDWAN) {
  476. targetSDWAN = sr.Gateway
  477. }
  478. if resolved, ok := sdwan.ResolveGroupForRouting(targetSDWAN); ok {
  479. for _, nh := range resolved {
  480. if nh.Interface == lr.Interface || (nh.Gateway != "" && nh.Gateway == lr.Gateway) {
  481. gwMatches = true
  482. break
  483. }
  484. }
  485. }
  486. }
  487. if gwMatches {
  488. matchedLive[i] = true
  489. liveRoutes[i].Comment = sr.Comment
  490. liveRoutes[i].Static = true
  491. liveRoutes[i].Dynamic = false
  492. liveRoutes[i].Protocol = "static"
  493. liveRoutes[i].ID = sr.ID
  494. if sr.Interface != "" {
  495. liveRoutes[i].Interface = sr.Interface
  496. }
  497. if liveRoutes[i].Gateway == "" && sr.Gateway != "" {
  498. liveRoutes[i].Gateway = sr.Gateway
  499. }
  500. if sr.Distance > 0 && liveRoutes[i].Distance == 0 {
  501. liveRoutes[i].Distance = sr.Distance
  502. }
  503. liveRoutes[i].CheckGateway = sr.CheckGateway
  504. liveRoutes[i].CheckTarget = sr.CheckTarget
  505. liveRoutes[i].CheckInterval = sr.CheckInterval
  506. liveRoutes[i].CheckThreshold = sr.CheckThreshold
  507. liveRoutes[i].HealthStatus = sr.HealthStatus
  508. liveRoutes[i].HealthRTT = sr.HealthRTT
  509. liveRoutes[i].HealthLossPct = sr.HealthLossPct
  510. liveRoutes[i].HealthFailCount = sr.HealthFailCount
  511. liveRoutes[i].HealthLastCheck = sr.HealthLastCheck
  512. found = true
  513. break
  514. }
  515. }
  516. if !found {
  517. // Inactive or disabled static route
  518. srCopy := sr
  519. srCopy.Active = false
  520. srCopy.Static = true
  521. srCopy.Dynamic = false
  522. srCopy.Protocol = "static"
  523. liveRoutes = append(liveRoutes, srCopy)
  524. }
  525. }
  526. // Detect Equal-Cost Multi-Path (ECMP) routes
  527. groupCounts := make(map[string]int)
  528. for _, r := range liveRoutes {
  529. if r.Active && !r.Disabled {
  530. key := fmt.Sprintf("%s|%s|%d", normalizeDst(r.DstAddress), r.RoutingTable, r.Distance)
  531. groupCounts[key]++
  532. }
  533. }
  534. for i := range liveRoutes {
  535. key := fmt.Sprintf("%s|%s|%d", normalizeDst(liveRoutes[i].DstAddress), liveRoutes[i].RoutingTable, liveRoutes[i].Distance)
  536. if liveRoutes[i].IsMultiPath || (liveRoutes[i].Active && !liveRoutes[i].Disabled && groupCounts[key] > 1) {
  537. liveRoutes[i].ECMP = true
  538. }
  539. if liveRoutes[i].DstAddress == "224.0.0.0/4" || strings.HasPrefix(liveRoutes[i].DstAddress, "224.") || strings.HasPrefix(liveRoutes[i].DstAddress, "ff") {
  540. liveRoutes[i].Multicast = true
  541. }
  542. }
  543. sort.Slice(liveRoutes, func(i, j int) bool {
  544. if liveRoutes[i].RoutingTable != liveRoutes[j].RoutingTable {
  545. return liveRoutes[i].RoutingTable < liveRoutes[j].RoutingTable
  546. }
  547. if liveRoutes[i].DstAddress != liveRoutes[j].DstAddress {
  548. return liveRoutes[i].DstAddress < liveRoutes[j].DstAddress
  549. }
  550. return liveRoutes[i].Gateway < liveRoutes[j].Gateway
  551. })
  552. return liveRoutes
  553. }
  554. func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
  555. if r == nil {
  556. return fmt.Errorf("route cannot be nil")
  557. }
  558. m.mu.Lock()
  559. defer m.mu.Unlock()
  560. before := append([]RouteItem(nil), m.staticRoutes...)
  561. dirty, applied := false, false
  562. defer func() {
  563. if err != nil && dirty {
  564. m.staticRoutes = before
  565. if applied {
  566. _ = m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
  567. }
  568. m.applyAll()
  569. _ = m.save()
  570. }
  571. }()
  572. if r.DstAddress == "" {
  573. return fmt.Errorf("destination address is required")
  574. }
  575. if r.DstAddress == "default" {
  576. r.DstAddress = "0.0.0.0/0"
  577. }
  578. if r.RoutingTable == "" {
  579. r.RoutingTable = "main"
  580. }
  581. if _, _, parseErr := net.ParseCIDR(r.DstAddress); parseErr != nil && net.ParseIP(r.DstAddress) == nil {
  582. return fmt.Errorf("invalid destination: %s", r.DstAddress)
  583. }
  584. dirty = true
  585. r.Static = true
  586. origID := r.ID
  587. foundIndex := -1
  588. // 1. Direct ID match in static routes (if not a temporary/kernel route ID)
  589. if r.ID != "" && !strings.HasPrefix(r.ID, "kr-") {
  590. for i, existing := range m.staticRoutes {
  591. if existing.ID == r.ID {
  592. foundIndex = i
  593. break
  594. }
  595. }
  596. }
  597. // 2. Match by OldDstAddress / OldGateway passed from the edit modal
  598. if foundIndex == -1 && r.OldDstAddress != "" {
  599. normOldDst := normalizeDst(r.OldDstAddress)
  600. oldTable := r.OldRoutingTable
  601. if oldTable == "" {
  602. oldTable = "main"
  603. }
  604. for i, existing := range m.staticRoutes {
  605. exTbl := existing.RoutingTable
  606. if exTbl == "" {
  607. exTbl = "main"
  608. }
  609. if normalizeDst(existing.DstAddress) == normOldDst && exTbl == oldTable {
  610. oldGwMatches := r.OldGateway == "" ||
  611. existing.Gateway == r.OldGateway ||
  612. existing.Interface == r.OldGateway ||
  613. (r.OldInterface != "" && existing.Interface == r.OldInterface)
  614. if oldGwMatches {
  615. foundIndex = i
  616. break
  617. }
  618. }
  619. }
  620. }
  621. // 3. Match by target DstAddress + Table + Gateway/Interface (prevent duplicates on re-save)
  622. if foundIndex == -1 {
  623. normNewDst := normalizeDst(r.DstAddress)
  624. for i, existing := range m.staticRoutes {
  625. exTbl := existing.RoutingTable
  626. if exTbl == "" {
  627. exTbl = "main"
  628. }
  629. if normalizeDst(existing.DstAddress) == normNewDst && exTbl == r.RoutingTable {
  630. gwMatches := (existing.Gateway != "" && existing.Gateway == r.Gateway) ||
  631. (existing.Interface != "" && existing.Interface == r.Interface) ||
  632. (existing.Gateway == "" && r.Gateway == "")
  633. if gwMatches {
  634. foundIndex = i
  635. break
  636. }
  637. }
  638. }
  639. }
  640. // 4. Fallback match: if editing with OldDstAddress but gateway was changed and didn't match oldGw
  641. if foundIndex == -1 && r.OldDstAddress != "" {
  642. normOldDst := normalizeDst(r.OldDstAddress)
  643. oldTable := r.OldRoutingTable
  644. if oldTable == "" {
  645. oldTable = "main"
  646. }
  647. for i, existing := range m.staticRoutes {
  648. exTbl := existing.RoutingTable
  649. if exTbl == "" {
  650. exTbl = "main"
  651. }
  652. if normalizeDst(existing.DstAddress) == normOldDst && exTbl == oldTable {
  653. foundIndex = i
  654. break
  655. }
  656. }
  657. }
  658. if r.CheckInterval <= 0 {
  659. r.CheckInterval = 5
  660. }
  661. if r.CheckThreshold <= 0 {
  662. r.CheckThreshold = 3
  663. }
  664. if r.CheckGateway == "internet" && strings.TrimSpace(r.CheckTarget) == "" {
  665. r.CheckTarget = "1.1.1.1"
  666. }
  667. if r.CheckGateway != "" && r.CheckGateway != "none" {
  668. if r.HealthStatus == "" {
  669. r.HealthStatus = "checking"
  670. }
  671. } else {
  672. r.CheckGateway = "none"
  673. r.HealthStatus = "none"
  674. }
  675. var oldRouteToClean *RouteItem
  676. if foundIndex >= 0 {
  677. oldExisting := m.staticRoutes[foundIndex]
  678. r.ID = oldExisting.ID // Preserve persistent ID!
  679. if normalizeDst(oldExisting.DstAddress) != normalizeDst(r.DstAddress) ||
  680. oldExisting.RoutingTable != r.RoutingTable ||
  681. oldExisting.Distance != r.Distance ||
  682. oldExisting.Gateway != r.Gateway ||
  683. oldExisting.Interface != r.Interface {
  684. oldCopy := oldExisting
  685. oldRouteToClean = &oldCopy
  686. }
  687. if r.HealthStatus == "checking" && oldExisting.HealthStatus != "" && oldExisting.CheckGateway == r.CheckGateway {
  688. r.HealthStatus = oldExisting.HealthStatus
  689. r.HealthRTT = oldExisting.HealthRTT
  690. r.HealthLossPct = oldExisting.HealthLossPct
  691. r.HealthLastCheck = oldExisting.HealthLastCheck
  692. }
  693. m.staticRoutes[foundIndex] = *r
  694. } else {
  695. if r.ID == "" || strings.HasPrefix(r.ID, "kr-") {
  696. r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
  697. }
  698. // If adopting a live kernel route not in staticRoutes, remove it from direct kernel routes
  699. if strings.HasPrefix(origID, "kr-") {
  700. for _, lr := range m.listLiveRoutesLocked() {
  701. if lr.ID == origID {
  702. _ = m.deleteDirectKernelRoute(lr)
  703. break
  704. }
  705. }
  706. }
  707. m.staticRoutes = append(m.staticRoutes, *r)
  708. }
  709. if r.OldDstAddress != "" {
  710. oldDist := 0
  711. if r.OldDistance != nil {
  712. oldDist = *r.OldDistance
  713. }
  714. if normalizeDst(r.OldDstAddress) != normalizeDst(r.DstAddress) ||
  715. r.OldRoutingTable != r.RoutingTable ||
  716. oldDist != r.Distance ||
  717. r.OldGateway != r.Gateway ||
  718. r.OldInterface != r.Interface {
  719. oldRouteToClean = &RouteItem{
  720. DstAddress: r.OldDstAddress,
  721. Gateway: r.OldGateway,
  722. RoutingTable: r.OldRoutingTable,
  723. Distance: oldDist,
  724. Interface: r.OldInterface,
  725. }
  726. }
  727. }
  728. for i := range m.staticRoutes {
  729. if m.staticRoutes[i].ID == r.ID {
  730. if !m.staticRoutes[i].Disabled && m.staticRoutes[i].HealthStatus != "offline" {
  731. m.staticRoutes[i].Active = true
  732. } else {
  733. m.staticRoutes[i].Active = false
  734. }
  735. break
  736. }
  737. }
  738. // In-memory deduplication pass on static routes
  739. seenKeys := make(map[string]bool)
  740. var deduped []RouteItem
  741. for _, sr := range m.staticRoutes {
  742. k := fmt.Sprintf("%s|%s|%s|%s|%d", normalizeDst(sr.DstAddress), sr.RoutingTable, sr.Gateway, sr.Interface, sr.Distance)
  743. if !seenKeys[k] {
  744. seenKeys[k] = true
  745. deduped = append(deduped, sr)
  746. }
  747. }
  748. m.staticRoutes = deduped
  749. if saveErr := m.save(); saveErr != nil {
  750. return saveErr
  751. }
  752. // Clean up old route group if destination, routing table, distance, gateway, or interface changed
  753. if oldRouteToClean != nil {
  754. _ = m.syncRouteGroupToKernel(oldRouteToClean.DstAddress, oldRouteToClean.RoutingTable, oldRouteToClean.Distance)
  755. _ = m.deleteDirectKernelRoute(*oldRouteToClean)
  756. }
  757. // Apply updated/new route group to kernel
  758. if err := m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance); err != nil {
  759. return err
  760. }
  761. applied = true
  762. return nil
  763. }
  764. func (m *RoutingManager) DeleteRoute(id string) error {
  765. m.mu.Lock()
  766. defer m.mu.Unlock()
  767. var updated []RouteItem
  768. var target *RouteItem
  769. // 1. Direct ID match in static routes
  770. for _, r := range m.staticRoutes {
  771. if r.ID == id {
  772. targetCopy := r
  773. target = &targetCopy
  774. } else {
  775. updated = append(updated, r)
  776. }
  777. }
  778. // 2. If not found by direct ID, check if it's a live kernel route ID (e.g. kr-...)
  779. if target == nil {
  780. for _, lr := range m.listLiveRoutesLocked() {
  781. if lr.ID == id {
  782. // Check if any static route corresponds to this live route
  783. matchIdx := -1
  784. normLrDst := normalizeDst(lr.DstAddress)
  785. for i, sr := range m.staticRoutes {
  786. gwMatches := sr.Gateway == "" || lr.Gateway == sr.Gateway || lr.Interface == sr.Gateway || (sr.Interface != "" && lr.Interface == sr.Interface)
  787. if normalizeDst(sr.DstAddress) == normLrDst && sr.RoutingTable == lr.RoutingTable && gwMatches {
  788. matchIdx = i
  789. break
  790. }
  791. }
  792. if matchIdx >= 0 {
  793. targetCopy := m.staticRoutes[matchIdx]
  794. target = &targetCopy
  795. for idx, r := range m.staticRoutes {
  796. if idx != matchIdx {
  797. updated = append(updated, r)
  798. }
  799. }
  800. _ = m.deleteDirectKernelRoute(lr)
  801. break
  802. } else {
  803. // Pure kernel route, not managed in static_routes.json
  804. return m.deleteDirectKernelRoute(lr)
  805. }
  806. }
  807. }
  808. }
  809. if target != nil {
  810. m.staticRoutes = updated
  811. _ = m.save()
  812. _ = m.syncRouteGroupToKernel(target.DstAddress, target.RoutingTable, target.Distance)
  813. _ = m.deleteDirectKernelRoute(*target)
  814. return nil
  815. }
  816. return fmt.Errorf("route not found")
  817. }
  818. func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
  819. m.mu.Lock()
  820. defer m.mu.Unlock()
  821. for i := range m.staticRoutes {
  822. if m.staticRoutes[i].ID == id {
  823. m.staticRoutes[i].Disabled = disabled
  824. if !disabled && m.staticRoutes[i].HealthStatus != "offline" {
  825. m.staticRoutes[i].Active = true
  826. } else if disabled {
  827. m.staticRoutes[i].Active = false
  828. }
  829. _ = m.save()
  830. _ = m.syncRouteGroupToKernel(m.staticRoutes[i].DstAddress, m.staticRoutes[i].RoutingTable, m.staticRoutes[i].Distance)
  831. return nil
  832. }
  833. }
  834. // If not in staticRoutes, could be a live route ID (kr-...)
  835. for _, lr := range m.listLiveRoutesLocked() {
  836. if lr.ID == id {
  837. // Check if any static route corresponds to this live route
  838. normLrDst := normalizeDst(lr.DstAddress)
  839. for i := range m.staticRoutes {
  840. gwMatches := m.staticRoutes[i].Gateway == "" || lr.Gateway == m.staticRoutes[i].Gateway || lr.Interface == m.staticRoutes[i].Gateway || (m.staticRoutes[i].Interface != "" && lr.Interface == m.staticRoutes[i].Interface)
  841. if normalizeDst(m.staticRoutes[i].DstAddress) == normLrDst && m.staticRoutes[i].RoutingTable == lr.RoutingTable && gwMatches {
  842. m.staticRoutes[i].Disabled = disabled
  843. if !disabled && m.staticRoutes[i].HealthStatus != "offline" {
  844. m.staticRoutes[i].Active = true
  845. } else if disabled {
  846. m.staticRoutes[i].Active = false
  847. }
  848. _ = m.save()
  849. _ = m.syncRouteGroupToKernel(m.staticRoutes[i].DstAddress, m.staticRoutes[i].RoutingTable, m.staticRoutes[i].Distance)
  850. return nil
  851. }
  852. }
  853. // If pure kernel static route, adopt into staticRoutes as disabled
  854. if disabled {
  855. lr.Disabled = true
  856. lr.Static = true
  857. lr.Active = false
  858. lr.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
  859. m.staticRoutes = append(m.staticRoutes, lr)
  860. _ = m.save()
  861. _ = m.syncRouteGroupToKernel(lr.DstAddress, lr.RoutingTable, lr.Distance)
  862. return nil
  863. }
  864. }
  865. }
  866. return fmt.Errorf("route not found")
  867. }
  868. func sliceContains(list []string, item string) bool {
  869. for _, s := range list {
  870. if s == item {
  871. return true
  872. }
  873. }
  874. return false
  875. }
  876. func normalizeDst(dst string) string {
  877. dst = strings.TrimSpace(dst)
  878. if dst == "" || dst == "default" || dst == "0.0.0.0/0" {
  879. return "default"
  880. }
  881. if !strings.Contains(dst, "/") && net.ParseIP(dst) != nil {
  882. return dst + "/32"
  883. }
  884. return dst
  885. }
  886. func detectGatewayForInterface(ifaceName string) string {
  887. link, err := netlink.LinkByName(ifaceName)
  888. if err == nil {
  889. routes, err := netlink.RouteList(link, netlink.FAMILY_V4)
  890. if err == nil {
  891. for _, r := range routes {
  892. if (r.Dst == nil || r.Dst.IP.IsUnspecified()) && r.Gw != nil && !r.Gw.IsUnspecified() {
  893. return r.Gw.String()
  894. }
  895. }
  896. for _, r := range routes {
  897. if r.Gw != nil && !r.Gw.IsUnspecified() {
  898. return r.Gw.String()
  899. }
  900. }
  901. }
  902. }
  903. out, err := exec.Command("ip", "route", "show", "dev", ifaceName).CombinedOutput()
  904. if err == nil {
  905. lines := strings.Split(string(out), "\n")
  906. for _, l := range lines {
  907. if strings.Contains(l, "via ") {
  908. fields := strings.Fields(l)
  909. for i, f := range fields {
  910. if f == "via" && i+1 < len(fields) {
  911. gw := fields[i+1]
  912. if net.ParseIP(gw) != nil {
  913. return gw
  914. }
  915. }
  916. }
  917. }
  918. }
  919. }
  920. return ""
  921. }
  922. func (m *RoutingManager) syncRouteGroupToKernel(dstAddress, routingTable string, distance int) error {
  923. normDst := normalizeDst(dstAddress)
  924. tbl := strings.TrimSpace(routingTable)
  925. if tbl == "" {
  926. tbl = "main"
  927. }
  928. var activeRoutes []RouteItem
  929. for _, r := range m.staticRoutes {
  930. rDst := normalizeDst(r.DstAddress)
  931. rTbl := strings.TrimSpace(r.RoutingTable)
  932. if rTbl == "" {
  933. rTbl = "main"
  934. }
  935. if rDst == normDst && rTbl == tbl && r.Distance == distance {
  936. if !r.Disabled && r.Active && r.HealthStatus != "offline" {
  937. activeRoutes = append(activeRoutes, r)
  938. }
  939. }
  940. }
  941. if len(activeRoutes) == 0 {
  942. // Safety guard: if all default routes are marked offline, do NOT delete the default route into a blackout!
  943. // Fallback to keeping all enabled static routes in the group active so recovery traffic and emergency pings can flow.
  944. if normDst == "default" {
  945. var fallbackRoutes []RouteItem
  946. for _, r := range m.staticRoutes {
  947. rDst := normalizeDst(r.DstAddress)
  948. rTbl := strings.TrimSpace(r.RoutingTable)
  949. if rTbl == "" {
  950. rTbl = "main"
  951. }
  952. if rDst == normDst && rTbl == tbl && r.Distance == distance && !r.Disabled {
  953. fallbackRoutes = append(fallbackRoutes, r)
  954. }
  955. }
  956. if len(fallbackRoutes) > 0 {
  957. log.Printf("[RouteManager] All default routes offline in group! Keeping fallback routes active to prevent blackout")
  958. activeRoutes = fallbackRoutes
  959. }
  960. }
  961. }
  962. if len(activeRoutes) == 0 {
  963. args := []string{"route", "del", normDst}
  964. if distance > 0 {
  965. args = append(args, "metric", strconv.Itoa(distance))
  966. }
  967. if tbl != "main" {
  968. args = append(args, "table", tbl)
  969. }
  970. args = append(args, "proto", "static")
  971. _ = exec.Command("ip", args...).Run()
  972. // Also delete without proto static in case route protocol differed
  973. fbArgs := []string{"route", "del", normDst}
  974. if distance > 0 {
  975. fbArgs = append(fbArgs, "metric", strconv.Itoa(distance))
  976. }
  977. if tbl != "main" {
  978. fbArgs = append(fbArgs, "table", tbl)
  979. }
  980. _ = exec.Command("ip", fbArgs...).Run()
  981. // Bare fallback
  982. _ = exec.Command("ip", "route", "del", normDst).Run()
  983. // Also delete blackhole route variant if it existed
  984. bhArgs := []string{"route", "del", "blackhole", normDst}
  985. if distance > 0 {
  986. bhArgs = append(bhArgs, "metric", strconv.Itoa(distance))
  987. }
  988. if tbl != "main" {
  989. bhArgs = append(bhArgs, "table", tbl)
  990. }
  991. _ = exec.Command("ip", bhArgs...).Run()
  992. return nil
  993. }
  994. type expandedNexthop struct {
  995. Gateway string
  996. Interface string
  997. Weight int
  998. }
  999. var allNexthops []expandedNexthop
  1000. isBlackhole := false
  1001. for _, r := range activeRoutes {
  1002. gw := strings.TrimSpace(r.Gateway)
  1003. iface := strings.TrimSpace(r.Interface)
  1004. if strings.EqualFold(gw, "blackhole") || strings.EqualFold(r.Action, "blackhole") {
  1005. isBlackhole = true
  1006. break
  1007. }
  1008. // Check if interface or gateway specifies an SD-WAN virtual group
  1009. targetSDWAN := ""
  1010. if sdwan.IsSDWANGroup(iface) {
  1011. targetSDWAN = iface
  1012. } else if sdwan.IsSDWANGroup(gw) {
  1013. targetSDWAN = gw
  1014. }
  1015. if targetSDWAN != "" {
  1016. resolved, ok := sdwan.ResolveGroupForRouting(targetSDWAN)
  1017. if ok && len(resolved) > 0 {
  1018. for _, nh := range resolved {
  1019. allNexthops = append(allNexthops, expandedNexthop{
  1020. Gateway: nh.Gateway,
  1021. Interface: nh.Interface,
  1022. Weight: nh.Weight,
  1023. })
  1024. }
  1025. continue
  1026. }
  1027. }
  1028. // Standard static route
  1029. if gw != "" && net.ParseIP(gw) == nil && iface == "" {
  1030. iface = gw
  1031. gw = ""
  1032. }
  1033. if gw == "" && iface != "" {
  1034. gw = detectGatewayForInterface(iface)
  1035. }
  1036. allNexthops = append(allNexthops, expandedNexthop{
  1037. Gateway: gw,
  1038. Interface: iface,
  1039. Weight: 1,
  1040. })
  1041. }
  1042. if isBlackhole {
  1043. args := []string{"route", "replace", "blackhole", normDst}
  1044. if distance > 0 {
  1045. args = append(args, "metric", strconv.Itoa(distance))
  1046. }
  1047. if tbl != "main" {
  1048. args = append(args, "table", tbl)
  1049. }
  1050. args = append(args, "proto", "static")
  1051. if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil {
  1052. return fmt.Errorf("failed to apply blackhole route: %s: %w", string(out), err)
  1053. }
  1054. return nil
  1055. }
  1056. if len(allNexthops) == 0 {
  1057. return nil
  1058. }
  1059. if len(allNexthops) == 1 {
  1060. nh := allNexthops[0]
  1061. args := []string{"route", "replace", normDst}
  1062. hasVia := false
  1063. if nh.Gateway != "" && net.ParseIP(nh.Gateway) != nil {
  1064. args = append(args, "via", nh.Gateway)
  1065. hasVia = true
  1066. }
  1067. if nh.Interface != "" {
  1068. args = append(args, "dev", nh.Interface)
  1069. }
  1070. if hasVia && nh.Interface != "" {
  1071. args = append(args, "onlink")
  1072. }
  1073. if distance > 0 {
  1074. args = append(args, "metric", strconv.Itoa(distance))
  1075. }
  1076. if tbl != "main" {
  1077. args = append(args, "table", tbl)
  1078. }
  1079. args = append(args, "proto", "static")
  1080. cmd := exec.Command("ip", args...)
  1081. if out, err := cmd.CombinedOutput(); err != nil {
  1082. if sliceContains(args, "onlink") {
  1083. var noOnlinkArgs []string
  1084. for _, a := range args {
  1085. if a != "onlink" {
  1086. noOnlinkArgs = append(noOnlinkArgs, a)
  1087. }
  1088. }
  1089. if _, fbErr := exec.Command("ip", noOnlinkArgs...).CombinedOutput(); fbErr == nil {
  1090. return nil
  1091. }
  1092. }
  1093. return fmt.Errorf("failed to apply route (%s): %s: %w", strings.Join(args, " "), string(out), err)
  1094. }
  1095. return nil
  1096. }
  1097. // Multiple nexthops: ECMP multipath route
  1098. args := []string{"route", "replace", normDst, "scope", "global"}
  1099. if distance > 0 {
  1100. args = append(args, "metric", strconv.Itoa(distance))
  1101. }
  1102. if tbl != "main" {
  1103. args = append(args, "table", tbl)
  1104. }
  1105. args = append(args, "proto", "static")
  1106. for _, nh := range allNexthops {
  1107. args = append(args, "nexthop")
  1108. hasVia := false
  1109. if nh.Gateway != "" && net.ParseIP(nh.Gateway) != nil {
  1110. args = append(args, "via", nh.Gateway)
  1111. hasVia = true
  1112. }
  1113. if nh.Interface != "" {
  1114. args = append(args, "dev", nh.Interface)
  1115. }
  1116. if hasVia && nh.Interface != "" {
  1117. args = append(args, "onlink")
  1118. }
  1119. w := nh.Weight
  1120. if w <= 0 {
  1121. w = 1
  1122. }
  1123. args = append(args, "weight", strconv.Itoa(w))
  1124. }
  1125. cmd := exec.Command("ip", args...)
  1126. if out, err := cmd.CombinedOutput(); err != nil {
  1127. if sliceContains(args, "onlink") {
  1128. var noOnlinkArgs []string
  1129. for _, a := range args {
  1130. if a != "onlink" {
  1131. noOnlinkArgs = append(noOnlinkArgs, a)
  1132. }
  1133. }
  1134. if _, fbErr := exec.Command("ip", noOnlinkArgs...).CombinedOutput(); fbErr == nil {
  1135. return nil
  1136. }
  1137. }
  1138. return fmt.Errorf("failed to apply multipath route (%s): %s: %w", strings.Join(args, " "), string(out), err)
  1139. }
  1140. return nil
  1141. }
  1142. // SyncSDWANRoutes reapplies kernel routes for static routes referencing SD-WAN groups when group state changes.
  1143. func (m *RoutingManager) SyncSDWANRoutes() {
  1144. m.mu.Lock()
  1145. defer m.mu.Unlock()
  1146. type groupKey struct {
  1147. dst string
  1148. table string
  1149. dist int
  1150. }
  1151. seen := make(map[groupKey]bool)
  1152. var groupsToSync []groupKey
  1153. for _, r := range m.staticRoutes {
  1154. if r.Disabled {
  1155. continue
  1156. }
  1157. if sdwan.IsSDWANGroup(r.Interface) || sdwan.IsSDWANGroup(r.Gateway) {
  1158. tbl := strings.TrimSpace(r.RoutingTable)
  1159. if tbl == "" {
  1160. tbl = "main"
  1161. }
  1162. normDst := normalizeDst(r.DstAddress)
  1163. k := groupKey{dst: normDst, table: tbl, dist: r.Distance}
  1164. if !seen[k] {
  1165. seen[k] = true
  1166. groupsToSync = append(groupsToSync, groupKey{dst: r.DstAddress, table: tbl, dist: r.Distance})
  1167. }
  1168. }
  1169. }
  1170. for _, g := range groupsToSync {
  1171. if err := m.syncRouteGroupToKernel(g.dst, g.table, g.dist); err != nil {
  1172. log.Printf("[RouteManager] SD-WAN failover route sync failed for %s (%s metric %d): %v", g.dst, g.table, g.dist, err)
  1173. } else {
  1174. log.Printf("[RouteManager] SD-WAN failover synced route %s (%s metric %d) successfully", g.dst, g.table, g.dist)
  1175. }
  1176. }
  1177. }
  1178. func (m *RoutingManager) applyRouteToKernel(r RouteItem) error {
  1179. return m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
  1180. }
  1181. func (m *RoutingManager) removeRouteFromKernel(r RouteItem) error {
  1182. return m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
  1183. }
  1184. func (m *RoutingManager) deleteDirectKernelRoute(r RouteItem) error {
  1185. normDst := normalizeDst(r.DstAddress)
  1186. gw := strings.TrimSpace(r.Gateway)
  1187. isBlackhole := strings.EqualFold(gw, "blackhole") || strings.EqualFold(r.Action, "blackhole")
  1188. var args []string
  1189. if isBlackhole {
  1190. args = []string{"route", "del", "blackhole", normDst}
  1191. } else {
  1192. args = []string{"route", "del", normDst}
  1193. if gw != "" && net.ParseIP(gw) != nil {
  1194. args = append(args, "via", gw)
  1195. }
  1196. iface := strings.TrimSpace(r.Interface)
  1197. if iface != "" {
  1198. args = append(args, "dev", iface)
  1199. }
  1200. }
  1201. if r.RoutingTable != "" && r.RoutingTable != "main" {
  1202. args = append(args, "table", r.RoutingTable)
  1203. }
  1204. if r.Distance > 0 {
  1205. args = append(args, "metric", strconv.Itoa(r.Distance))
  1206. }
  1207. cmd := exec.Command("ip", args...)
  1208. if out, err := cmd.CombinedOutput(); err == nil {
  1209. return nil
  1210. } else {
  1211. log.Printf("[RouteManager] direct ip route del (%s): %s (%v), attempting fallback", strings.Join(args, " "), string(out), err)
  1212. }
  1213. if isBlackhole {
  1214. fbBhArgs := []string{"route", "del", "blackhole", normDst}
  1215. if r.RoutingTable != "" && r.RoutingTable != "main" {
  1216. fbBhArgs = append(fbBhArgs, "table", r.RoutingTable)
  1217. }
  1218. if r.Distance > 0 {
  1219. fbBhArgs = append(fbBhArgs, "metric", strconv.Itoa(r.Distance))
  1220. }
  1221. if err := exec.Command("ip", fbBhArgs...).Run(); err == nil {
  1222. return nil
  1223. }
  1224. return exec.Command("ip", "route", "del", "blackhole", normDst).Run()
  1225. }
  1226. fbArgs := []string{"route", "del", normDst}
  1227. if r.RoutingTable != "" && r.RoutingTable != "main" {
  1228. fbArgs = append(fbArgs, "table", r.RoutingTable)
  1229. }
  1230. if r.Distance > 0 {
  1231. fbArgs = append(fbArgs, "metric", strconv.Itoa(r.Distance))
  1232. }
  1233. if err := exec.Command("ip", fbArgs...).Run(); err == nil {
  1234. return nil
  1235. }
  1236. return exec.Command("ip", "route", "del", normDst).Run()
  1237. }
  1238. // ==================== Policy Rules (/ip route rule) ====================
  1239. func (m *RoutingManager) ListPolicyRules() []PolicyRuleItem {
  1240. m.mu.Lock()
  1241. defer m.mu.Unlock()
  1242. // Query live rules from kernel
  1243. rules, err := netlink.RuleList(netlink.FAMILY_V4)
  1244. if err != nil {
  1245. return m.policyRules
  1246. }
  1247. tableNames := m.getTableMap()
  1248. var res []PolicyRuleItem
  1249. userRulesByPrio := make(map[int]PolicyRuleItem)
  1250. for _, pr := range m.policyRules {
  1251. userRulesByPrio[pr.Priority] = pr
  1252. }
  1253. for _, r := range rules {
  1254. tableName := "main"
  1255. if name, ok := tableNames[r.Table]; ok {
  1256. tableName = name
  1257. } else if r.Table != 254 && r.Table != 0 {
  1258. tableName = strconv.Itoa(r.Table)
  1259. }
  1260. src := ""
  1261. if r.Src != nil {
  1262. src = r.Src.String()
  1263. }
  1264. dst := ""
  1265. if r.Dst != nil {
  1266. dst = r.Dst.String()
  1267. }
  1268. markStr := ""
  1269. if r.Mark > 0 {
  1270. markStr = fmt.Sprintf("0x%x (%d)", r.Mark, r.Mark)
  1271. }
  1272. id := fmt.Sprintf("rule-%d-%d", r.Priority, r.Table)
  1273. comment := ""
  1274. isDefault := false
  1275. isSystem := false
  1276. if r.Priority == 0 || r.Priority == 32766 || r.Priority == 32767 {
  1277. isDefault = true
  1278. isSystem = true
  1279. } else if userRule, ok := userRulesByPrio[r.Priority]; ok {
  1280. if userRule.ID != "" {
  1281. id = userRule.ID
  1282. }
  1283. comment = userRule.Comment
  1284. isDefault = false
  1285. isSystem = false
  1286. } else {
  1287. // System/daemon rule not added by user in debtik (e.g. WireGuard, IPsec)
  1288. isSystem = true
  1289. }
  1290. res = append(res, PolicyRuleItem{
  1291. ID: id,
  1292. Priority: r.Priority,
  1293. SrcAddress: src,
  1294. DstAddress: dst,
  1295. RoutingMark: markStr,
  1296. Table: tableName,
  1297. TableID: r.Table,
  1298. Action: "lookup",
  1299. Comment: comment,
  1300. IsDefault: isDefault,
  1301. IsSystem: isSystem,
  1302. })
  1303. }
  1304. return res
  1305. }
  1306. func (m *RoutingManager) SavePolicyRule(pr PolicyRuleItem) error {
  1307. m.mu.Lock()
  1308. defer m.mu.Unlock()
  1309. if pr.Table == "" {
  1310. pr.Table = "main"
  1311. }
  1312. if pr.ID == "" {
  1313. pr.ID = fmt.Sprintf("prule-%d", time.Now().UnixNano())
  1314. }
  1315. if pr.Priority == 0 {
  1316. pr.Priority = 1000 + len(m.policyRules)*10
  1317. }
  1318. if err := m.applyRuleToKernel(pr); err != nil {
  1319. return err
  1320. }
  1321. m.policyRules = append(m.policyRules, pr)
  1322. return m.save()
  1323. }
  1324. func (m *RoutingManager) DeletePolicyRule(id string) error {
  1325. m.mu.Lock()
  1326. defer m.mu.Unlock()
  1327. var updated []PolicyRuleItem
  1328. var target *PolicyRuleItem
  1329. for _, pr := range m.policyRules {
  1330. if pr.ID == id || fmt.Sprintf("rule-%d-%d", pr.Priority, pr.TableID) == id {
  1331. target = &pr
  1332. } else {
  1333. updated = append(updated, pr)
  1334. }
  1335. }
  1336. if target != nil {
  1337. _ = m.removeRuleFromKernel(*target)
  1338. } else if strings.HasPrefix(id, "rule-") {
  1339. parts := strings.Split(id, "-")
  1340. if len(parts) >= 2 {
  1341. if prio, err := strconv.Atoi(parts[1]); err == nil && prio > 0 {
  1342. _ = exec.Command("ip", "rule", "del", "pref", strconv.Itoa(prio)).Run()
  1343. }
  1344. }
  1345. }
  1346. m.policyRules = updated
  1347. return m.save()
  1348. }
  1349. func (m *RoutingManager) applyRuleToKernel(pr PolicyRuleItem) error {
  1350. args := []string{"rule", "add"}
  1351. if pr.Priority > 0 {
  1352. args = append(args, "pref", strconv.Itoa(pr.Priority))
  1353. }
  1354. if pr.SrcAddress != "" {
  1355. args = append(args, "from", pr.SrcAddress)
  1356. }
  1357. if pr.DstAddress != "" {
  1358. args = append(args, "to", pr.DstAddress)
  1359. }
  1360. if pr.RoutingMark != "" {
  1361. // Clean mark string if it has (hex)
  1362. markClean := pr.RoutingMark
  1363. if strings.Contains(markClean, "(") {
  1364. markClean = strings.TrimSpace(markClean[:strings.Index(markClean, "(")])
  1365. }
  1366. args = append(args, "fwmark", markClean)
  1367. }
  1368. if pr.Table != "" {
  1369. args = append(args, "lookup", pr.Table)
  1370. }
  1371. cmd := exec.Command("ip", args...)
  1372. out, err := cmd.CombinedOutput()
  1373. if err != nil {
  1374. return fmt.Errorf("failed to add rule (%s): %s: %w", strings.Join(args, " "), string(out), err)
  1375. }
  1376. return nil
  1377. }
  1378. func (m *RoutingManager) removeRuleFromKernel(pr PolicyRuleItem) error {
  1379. if pr.Priority > 0 {
  1380. _ = exec.Command("ip", "rule", "del", "pref", strconv.Itoa(pr.Priority)).Run()
  1381. return nil
  1382. }
  1383. args := []string{"rule", "del"}
  1384. if pr.RoutingMark != "" {
  1385. markClean := pr.RoutingMark
  1386. if strings.Contains(markClean, "(") {
  1387. markClean = strings.TrimSpace(markClean[:strings.Index(markClean, "(")])
  1388. }
  1389. args = append(args, "fwmark", markClean)
  1390. }
  1391. if pr.Table != "" {
  1392. args = append(args, "lookup", pr.Table)
  1393. }
  1394. _ = exec.Command("ip", args...).Run()
  1395. return nil
  1396. }
  1397. // ==================== Routing Tables ====================
  1398. func (m *RoutingManager) ListTables() []RoutingTableItem {
  1399. m.mu.Lock()
  1400. defer m.mu.Unlock()
  1401. tables := []RoutingTableItem{
  1402. {Name: "main", TableID: 254, Comment: "Default system routing table"},
  1403. {Name: "default", TableID: 253, Comment: "Default fallback table"},
  1404. {Name: "local", TableID: 255, Comment: "Local kernel device table"},
  1405. }
  1406. tables = append(tables, m.customTables...)
  1407. return tables
  1408. }
  1409. func (m *RoutingManager) SaveTable(t RoutingTableItem) error {
  1410. m.mu.Lock()
  1411. defer m.mu.Unlock()
  1412. if t.Name == "" {
  1413. return fmt.Errorf("table name is required")
  1414. }
  1415. if t.TableID == 0 {
  1416. // Auto assign FIB ID starting at 100
  1417. t.TableID = 100 + len(m.customTables)
  1418. }
  1419. for _, existing := range m.customTables {
  1420. if existing.Name == t.Name || existing.TableID == t.TableID {
  1421. return fmt.Errorf("table with name %q or ID %d already exists", t.Name, t.TableID)
  1422. }
  1423. }
  1424. m.customTables = append(m.customTables, t)
  1425. _ = m.save()
  1426. m.syncCustomTablesToSystem()
  1427. return nil
  1428. }
  1429. func (m *RoutingManager) getTableMap() map[int]string {
  1430. tableMap := map[int]string{
  1431. 254: "main",
  1432. 253: "default",
  1433. 255: "local",
  1434. }
  1435. // Read /etc/iproute2/rt_tables
  1436. if f, err := os.Open(RTTableFilePath); err == nil {
  1437. defer f.Close()
  1438. scanner := bufio.NewScanner(f)
  1439. for scanner.Scan() {
  1440. line := strings.TrimSpace(scanner.Text())
  1441. if line == "" || strings.HasPrefix(line, "#") {
  1442. continue
  1443. }
  1444. parts := strings.Fields(line)
  1445. if len(parts) >= 2 {
  1446. if id, err := strconv.Atoi(parts[0]); err == nil {
  1447. tableMap[id] = parts[1]
  1448. }
  1449. }
  1450. }
  1451. }
  1452. for _, t := range m.customTables {
  1453. tableMap[t.TableID] = t.Name
  1454. }
  1455. return tableMap
  1456. }
  1457. func (m *RoutingManager) Stop() {
  1458. if m.stopChan != nil {
  1459. select {
  1460. case <-m.stopChan:
  1461. default:
  1462. close(m.stopChan)
  1463. }
  1464. }
  1465. }
  1466. // RenameInterface updates static routes and policy rules when an interface is renamed.
  1467. func (m *RoutingManager) RenameInterface(oldName, newName string) {
  1468. m.mu.Lock()
  1469. defer m.mu.Unlock()
  1470. changed := false
  1471. for i := range m.staticRoutes {
  1472. if m.staticRoutes[i].Interface == oldName {
  1473. m.staticRoutes[i].Interface = newName
  1474. changed = true
  1475. }
  1476. if m.staticRoutes[i].Gateway == oldName {
  1477. m.staticRoutes[i].Gateway = newName
  1478. changed = true
  1479. }
  1480. }
  1481. if changed {
  1482. _ = m.save()
  1483. }
  1484. }