manager.go 52 KB

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