manager.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. package routing
  2. import (
  3. "bufio"
  4. "encoding/json"
  5. "fmt"
  6. "net"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "sort"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "time"
  15. "github.com/vishvananda/netlink"
  16. )
  17. const (
  18. StaticRoutesStorePath = "/etc/proxmox-router/routing/static_routes.json"
  19. PolicyRulesStorePath = "/etc/proxmox-router/routing/policy_rules.json"
  20. CustomTablesStorePath = "/etc/proxmox-router/routing/custom_tables.json"
  21. RTTableFilePath = "/etc/iproute2/rt_tables"
  22. )
  23. type RoutingManager struct {
  24. mu sync.Mutex
  25. staticRoutes []RouteItem
  26. policyRules []PolicyRuleItem
  27. customTables []RoutingTableItem
  28. }
  29. func NewRoutingManager() *RoutingManager {
  30. m := &RoutingManager{
  31. staticRoutes: make([]RouteItem, 0),
  32. policyRules: make([]PolicyRuleItem, 0),
  33. customTables: make([]RoutingTableItem, 0),
  34. }
  35. m.initRTTableFile()
  36. m.load()
  37. m.syncCustomTablesToSystem()
  38. m.applyAll()
  39. return m
  40. }
  41. func (m *RoutingManager) initRTTableFile() {
  42. _ = os.MkdirAll(filepath.Dir(RTTableFilePath), 0755)
  43. if _, err := os.Stat(RTTableFilePath); os.IsNotExist(err) {
  44. // Copy from /usr/share/iproute2/rt_tables or seed default
  45. content, err := os.ReadFile("/usr/share/iproute2/rt_tables")
  46. if err == nil {
  47. _ = os.WriteFile(RTTableFilePath, content, 0644)
  48. } else {
  49. defaultContent := "# reserved values\n255\tlocal\n254\tmain\n253\tdefault\n0\tunspec\n"
  50. _ = os.WriteFile(RTTableFilePath, []byte(defaultContent), 0644)
  51. }
  52. }
  53. }
  54. func (m *RoutingManager) load() {
  55. if data, err := os.ReadFile(StaticRoutesStorePath); err == nil {
  56. _ = json.Unmarshal(data, &m.staticRoutes)
  57. }
  58. if data, err := os.ReadFile(PolicyRulesStorePath); err == nil {
  59. _ = json.Unmarshal(data, &m.policyRules)
  60. }
  61. if data, err := os.ReadFile(CustomTablesStorePath); err == nil {
  62. _ = json.Unmarshal(data, &m.customTables)
  63. }
  64. }
  65. func (m *RoutingManager) save() error {
  66. _ = os.MkdirAll("/etc/proxmox-router/routing", 0755)
  67. if d, err := json.MarshalIndent(m.staticRoutes, "", " "); err == nil {
  68. _ = os.WriteFile(StaticRoutesStorePath, d, 0600)
  69. }
  70. if d, err := json.MarshalIndent(m.policyRules, "", " "); err == nil {
  71. _ = os.WriteFile(PolicyRulesStorePath, d, 0600)
  72. }
  73. if d, err := json.MarshalIndent(m.customTables, "", " "); err == nil {
  74. _ = os.WriteFile(CustomTablesStorePath, d, 0600)
  75. }
  76. return nil
  77. }
  78. func (m *RoutingManager) syncCustomTablesToSystem() {
  79. content, _ := os.ReadFile(RTTableFilePath)
  80. str := string(content)
  81. var sb strings.Builder
  82. sb.WriteString(str)
  83. for _, t := range m.customTables {
  84. check := fmt.Sprintf("\t%s", t.Name)
  85. if !strings.Contains(str, check) {
  86. sb.WriteString(fmt.Sprintf("%d\t%s\n", t.TableID, t.Name))
  87. }
  88. }
  89. _ = os.WriteFile(RTTableFilePath, []byte(sb.String()), 0644)
  90. }
  91. func (m *RoutingManager) applyAll() {
  92. // Apply all static routes
  93. for _, r := range m.staticRoutes {
  94. if !r.Disabled {
  95. _ = m.applyRouteToKernel(r)
  96. }
  97. }
  98. // Apply policy rules
  99. for _, pr := range m.policyRules {
  100. _ = m.applyRuleToKernel(pr)
  101. }
  102. }
  103. // ==================== Routes (/ip route) ====================
  104. func isDynamicProtocol(proto netlink.RouteProtocol) bool {
  105. switch int(proto) {
  106. case 11, // RTPROT_ZEBRA
  107. 16, // RTPROT_DHCP
  108. 42, // RTPROT_BABEL
  109. 186, // RTPROT_BGP
  110. 187, // RTPROT_ISIS
  111. 188, // RTPROT_OSPF
  112. 189, // RTPROT_RIP
  113. 192: // RTPROT_EIGRP
  114. return true
  115. default:
  116. return false
  117. }
  118. }
  119. func (m *RoutingManager) listLiveRoutesLocked() []RouteItem {
  120. tableNames := m.getTableMap()
  121. links, _ := netlink.LinkList()
  122. linkMap := make(map[int]string)
  123. for _, l := range links {
  124. linkMap[l.Attrs().Index] = l.Attrs().Name
  125. }
  126. var liveRoutes []RouteItem
  127. routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
  128. if err == nil {
  129. for _, r := range routes {
  130. dst := "0.0.0.0/0"
  131. if r.Dst != nil {
  132. dst = r.Dst.String()
  133. }
  134. gw := ""
  135. if r.Gw != nil {
  136. gw = r.Gw.String()
  137. }
  138. tableName := "main"
  139. if name, ok := tableNames[r.Table]; ok {
  140. tableName = name
  141. } else if r.Table != 254 && r.Table != 0 {
  142. tableName = strconv.Itoa(r.Table)
  143. }
  144. ifName := linkMap[r.LinkIndex]
  145. isDirect := r.Scope == netlink.SCOPE_LINK || r.Protocol == netlink.RouteProtocol(2)
  146. isDyn := isDynamicProtocol(r.Protocol)
  147. isStatic := !isDirect && !isDyn
  148. cleanDst := strings.ReplaceAll(dst, "/", "_")
  149. cleanGw := gw
  150. if cleanGw == "" {
  151. cleanGw = ifName
  152. }
  153. routeID := fmt.Sprintf("kr-%d-%s-%s", r.Table, cleanDst, cleanGw)
  154. liveRoutes = append(liveRoutes, RouteItem{
  155. ID: routeID,
  156. DstAddress: dst,
  157. Gateway: gw,
  158. Interface: ifName,
  159. Distance: r.Priority,
  160. RoutingTable: tableName,
  161. TableID: r.Table,
  162. Active: true,
  163. Connect: isDirect,
  164. Dynamic: isDyn,
  165. Static: isStatic,
  166. Disabled: false,
  167. })
  168. }
  169. }
  170. return liveRoutes
  171. }
  172. func (m *RoutingManager) ListRoutes() []RouteItem {
  173. m.mu.Lock()
  174. defer m.mu.Unlock()
  175. liveRoutes := m.listLiveRoutesLocked()
  176. // Merge static routes that might be disabled or have custom metadata
  177. for _, sr := range m.staticRoutes {
  178. found := false
  179. for i, lr := range liveRoutes {
  180. if lr.DstAddress == sr.DstAddress && lr.RoutingTable == sr.RoutingTable && (sr.Gateway == "" || lr.Gateway == sr.Gateway) {
  181. liveRoutes[i].Comment = sr.Comment
  182. liveRoutes[i].Static = true
  183. liveRoutes[i].Dynamic = false
  184. liveRoutes[i].ID = sr.ID
  185. if sr.Distance > 0 && liveRoutes[i].Distance == 0 {
  186. liveRoutes[i].Distance = sr.Distance
  187. }
  188. found = true
  189. break
  190. }
  191. }
  192. if !found {
  193. // Inactive or disabled static route
  194. srCopy := sr
  195. srCopy.Active = false
  196. srCopy.Static = true
  197. srCopy.Dynamic = false
  198. liveRoutes = append(liveRoutes, srCopy)
  199. }
  200. }
  201. sort.Slice(liveRoutes, func(i, j int) bool {
  202. if liveRoutes[i].RoutingTable != liveRoutes[j].RoutingTable {
  203. return liveRoutes[i].RoutingTable < liveRoutes[j].RoutingTable
  204. }
  205. if liveRoutes[i].DstAddress != liveRoutes[j].DstAddress {
  206. return liveRoutes[i].DstAddress < liveRoutes[j].DstAddress
  207. }
  208. return liveRoutes[i].Gateway < liveRoutes[j].Gateway
  209. })
  210. return liveRoutes
  211. }
  212. func (m *RoutingManager) SaveRoute(r RouteItem) error {
  213. m.mu.Lock()
  214. defer m.mu.Unlock()
  215. if r.DstAddress == "" {
  216. return fmt.Errorf("destination address is required")
  217. }
  218. if r.DstAddress == "default" {
  219. r.DstAddress = "0.0.0.0/0"
  220. }
  221. if r.RoutingTable == "" {
  222. r.RoutingTable = "main"
  223. }
  224. r.Static = true
  225. // 1. If old route details are provided from edit modal, remove old route first
  226. if r.OldDstAddress != "" {
  227. oldDist := 0
  228. if r.OldDistance != nil {
  229. oldDist = *r.OldDistance
  230. }
  231. oldRoute := RouteItem{
  232. DstAddress: r.OldDstAddress,
  233. Gateway: r.OldGateway,
  234. RoutingTable: r.OldRoutingTable,
  235. Distance: oldDist,
  236. Interface: r.OldInterface,
  237. }
  238. _ = m.removeRouteFromKernel(oldRoute)
  239. }
  240. // 2. If existing entry in m.staticRoutes exists, remove its old route from kernel
  241. foundIndex := -1
  242. for i, existing := range m.staticRoutes {
  243. if existing.ID == r.ID || (r.ID == "" && existing.DstAddress == r.DstAddress && existing.RoutingTable == r.RoutingTable) {
  244. foundIndex = i
  245. _ = m.removeRouteFromKernel(existing)
  246. break
  247. }
  248. }
  249. // 3. If r.ID starts with kr- (kernel static route being edited/adopted), remove it from kernel
  250. if strings.HasPrefix(r.ID, "kr-") {
  251. for _, lr := range m.listLiveRoutesLocked() {
  252. if lr.ID == r.ID {
  253. _ = m.removeRouteFromKernel(lr)
  254. break
  255. }
  256. }
  257. r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
  258. }
  259. if r.ID == "" {
  260. r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
  261. }
  262. if foundIndex >= 0 {
  263. m.staticRoutes[foundIndex] = r
  264. } else {
  265. m.staticRoutes = append(m.staticRoutes, r)
  266. }
  267. _ = m.save()
  268. if r.Disabled {
  269. _ = m.removeRouteFromKernel(r)
  270. } else {
  271. if err := m.applyRouteToKernel(r); err != nil {
  272. return err
  273. }
  274. }
  275. return nil
  276. }
  277. func (m *RoutingManager) DeleteRoute(id string) error {
  278. m.mu.Lock()
  279. defer m.mu.Unlock()
  280. var updated []RouteItem
  281. var target *RouteItem
  282. for _, r := range m.staticRoutes {
  283. if r.ID == id {
  284. target = &r
  285. } else {
  286. updated = append(updated, r)
  287. }
  288. }
  289. if target != nil {
  290. _ = m.removeRouteFromKernel(*target)
  291. m.staticRoutes = updated
  292. return m.save()
  293. }
  294. // If not in staticRoutes, could be a live route (e.g. kr-...)
  295. for _, lr := range m.listLiveRoutesLocked() {
  296. if lr.ID == id {
  297. _ = m.removeRouteFromKernel(lr)
  298. return nil
  299. }
  300. }
  301. return fmt.Errorf("route not found")
  302. }
  303. func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
  304. m.mu.Lock()
  305. defer m.mu.Unlock()
  306. for i := range m.staticRoutes {
  307. if m.staticRoutes[i].ID == id {
  308. m.staticRoutes[i].Disabled = disabled
  309. _ = m.save()
  310. if disabled {
  311. _ = m.removeRouteFromKernel(m.staticRoutes[i])
  312. } else {
  313. _ = m.applyRouteToKernel(m.staticRoutes[i])
  314. }
  315. return nil
  316. }
  317. }
  318. // If not in staticRoutes, could be a kernel static route to disable
  319. if disabled {
  320. for _, lr := range m.listLiveRoutesLocked() {
  321. if lr.ID == id {
  322. lr.Disabled = true
  323. lr.Static = true
  324. lr.Active = false
  325. lr.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
  326. _ = m.removeRouteFromKernel(lr)
  327. m.staticRoutes = append(m.staticRoutes, lr)
  328. return m.save()
  329. }
  330. }
  331. }
  332. return fmt.Errorf("route not found")
  333. }
  334. func (m *RoutingManager) applyRouteToKernel(r RouteItem) error {
  335. dst := strings.TrimSpace(r.DstAddress)
  336. if dst == "default" || dst == "0.0.0.0/0" {
  337. dst = "default"
  338. } else if !strings.Contains(dst, "/") && net.ParseIP(dst) != nil {
  339. dst = dst + "/32"
  340. }
  341. args := []string{"route", "replace", dst}
  342. gw := strings.TrimSpace(r.Gateway)
  343. if gw != "" {
  344. if net.ParseIP(gw) != nil {
  345. args = append(args, "via", gw)
  346. } else {
  347. args = append(args, "dev", gw)
  348. }
  349. }
  350. iface := strings.TrimSpace(r.Interface)
  351. if iface != "" && gw != iface && (gw == "" || net.ParseIP(gw) != nil) {
  352. args = append(args, "dev", iface)
  353. }
  354. if r.Distance > 0 {
  355. args = append(args, "metric", strconv.Itoa(r.Distance))
  356. }
  357. if r.RoutingTable != "" && r.RoutingTable != "main" {
  358. args = append(args, "table", r.RoutingTable)
  359. }
  360. args = append(args, "proto", "static")
  361. cmd := exec.Command("ip", args...)
  362. out, err := cmd.CombinedOutput()
  363. if err != nil {
  364. return fmt.Errorf("failed to apply route (%s): %s: %w", strings.Join(args, " "), string(out), err)
  365. }
  366. return nil
  367. }
  368. func (m *RoutingManager) removeRouteFromKernel(r RouteItem) error {
  369. dst := strings.TrimSpace(r.DstAddress)
  370. if dst == "default" || dst == "0.0.0.0/0" {
  371. dst = "default"
  372. } else if !strings.Contains(dst, "/") && net.ParseIP(dst) != nil {
  373. dst = dst + "/32"
  374. }
  375. args := []string{"route", "del", dst}
  376. gw := strings.TrimSpace(r.Gateway)
  377. if gw != "" {
  378. if net.ParseIP(gw) != nil {
  379. args = append(args, "via", gw)
  380. } else {
  381. args = append(args, "dev", gw)
  382. }
  383. }
  384. if r.Distance > 0 {
  385. args = append(args, "metric", strconv.Itoa(r.Distance))
  386. }
  387. if r.RoutingTable != "" && r.RoutingTable != "main" {
  388. args = append(args, "table", r.RoutingTable)
  389. }
  390. _ = exec.Command("ip", args...).Run()
  391. // Fallback deletion without metric
  392. fallbackArgs := []string{"route", "del", dst}
  393. if r.RoutingTable != "" && r.RoutingTable != "main" {
  394. fallbackArgs = append(fallbackArgs, "table", r.RoutingTable)
  395. }
  396. _ = exec.Command("ip", fallbackArgs...).Run()
  397. return nil
  398. }
  399. // ==================== Policy Rules (/ip route rule) ====================
  400. func (m *RoutingManager) ListPolicyRules() []PolicyRuleItem {
  401. m.mu.Lock()
  402. defer m.mu.Unlock()
  403. // Query live rules from kernel
  404. rules, err := netlink.RuleList(netlink.FAMILY_V4)
  405. if err != nil {
  406. return m.policyRules
  407. }
  408. tableNames := m.getTableMap()
  409. var res []PolicyRuleItem
  410. userRulesByPrio := make(map[int]PolicyRuleItem)
  411. for _, pr := range m.policyRules {
  412. userRulesByPrio[pr.Priority] = pr
  413. }
  414. for _, r := range rules {
  415. tableName := "main"
  416. if name, ok := tableNames[r.Table]; ok {
  417. tableName = name
  418. } else if r.Table != 254 && r.Table != 0 {
  419. tableName = strconv.Itoa(r.Table)
  420. }
  421. src := ""
  422. if r.Src != nil {
  423. src = r.Src.String()
  424. }
  425. dst := ""
  426. if r.Dst != nil {
  427. dst = r.Dst.String()
  428. }
  429. markStr := ""
  430. if r.Mark > 0 {
  431. markStr = fmt.Sprintf("0x%x (%d)", r.Mark, r.Mark)
  432. }
  433. id := fmt.Sprintf("rule-%d-%d", r.Priority, r.Table)
  434. comment := ""
  435. isDefault := false
  436. isSystem := false
  437. if r.Priority == 0 || r.Priority == 32766 || r.Priority == 32767 {
  438. isDefault = true
  439. isSystem = true
  440. } else if userRule, ok := userRulesByPrio[r.Priority]; ok {
  441. if userRule.ID != "" {
  442. id = userRule.ID
  443. }
  444. comment = userRule.Comment
  445. isDefault = false
  446. isSystem = false
  447. } else {
  448. // System/daemon rule not added by user in proxmox-router (e.g. WireGuard, IPsec)
  449. isSystem = true
  450. }
  451. res = append(res, PolicyRuleItem{
  452. ID: id,
  453. Priority: r.Priority,
  454. SrcAddress: src,
  455. DstAddress: dst,
  456. RoutingMark: markStr,
  457. Table: tableName,
  458. TableID: r.Table,
  459. Action: "lookup",
  460. Comment: comment,
  461. IsDefault: isDefault,
  462. IsSystem: isSystem,
  463. })
  464. }
  465. return res
  466. }
  467. func (m *RoutingManager) SavePolicyRule(pr PolicyRuleItem) error {
  468. m.mu.Lock()
  469. defer m.mu.Unlock()
  470. if pr.Table == "" {
  471. pr.Table = "main"
  472. }
  473. if pr.ID == "" {
  474. pr.ID = fmt.Sprintf("prule-%d", time.Now().UnixNano())
  475. }
  476. if pr.Priority == 0 {
  477. pr.Priority = 1000 + len(m.policyRules)*10
  478. }
  479. if err := m.applyRuleToKernel(pr); err != nil {
  480. return err
  481. }
  482. m.policyRules = append(m.policyRules, pr)
  483. return m.save()
  484. }
  485. func (m *RoutingManager) DeletePolicyRule(id string) error {
  486. m.mu.Lock()
  487. defer m.mu.Unlock()
  488. var updated []PolicyRuleItem
  489. var target *PolicyRuleItem
  490. for _, pr := range m.policyRules {
  491. if pr.ID == id || fmt.Sprintf("rule-%d-%d", pr.Priority, pr.TableID) == id {
  492. target = &pr
  493. } else {
  494. updated = append(updated, pr)
  495. }
  496. }
  497. if target != nil {
  498. _ = m.removeRuleFromKernel(*target)
  499. } else if strings.HasPrefix(id, "rule-") {
  500. parts := strings.Split(id, "-")
  501. if len(parts) >= 2 {
  502. if prio, err := strconv.Atoi(parts[1]); err == nil && prio > 0 {
  503. _ = exec.Command("ip", "rule", "del", "pref", strconv.Itoa(prio)).Run()
  504. }
  505. }
  506. }
  507. m.policyRules = updated
  508. return m.save()
  509. }
  510. func (m *RoutingManager) applyRuleToKernel(pr PolicyRuleItem) error {
  511. args := []string{"rule", "add"}
  512. if pr.Priority > 0 {
  513. args = append(args, "pref", strconv.Itoa(pr.Priority))
  514. }
  515. if pr.SrcAddress != "" {
  516. args = append(args, "from", pr.SrcAddress)
  517. }
  518. if pr.DstAddress != "" {
  519. args = append(args, "to", pr.DstAddress)
  520. }
  521. if pr.RoutingMark != "" {
  522. // Clean mark string if it has (hex)
  523. markClean := pr.RoutingMark
  524. if strings.Contains(markClean, "(") {
  525. markClean = strings.TrimSpace(markClean[:strings.Index(markClean, "(")])
  526. }
  527. args = append(args, "fwmark", markClean)
  528. }
  529. if pr.Table != "" {
  530. args = append(args, "lookup", pr.Table)
  531. }
  532. cmd := exec.Command("ip", args...)
  533. out, err := cmd.CombinedOutput()
  534. if err != nil {
  535. return fmt.Errorf("failed to add rule (%s): %s: %w", strings.Join(args, " "), string(out), err)
  536. }
  537. return nil
  538. }
  539. func (m *RoutingManager) removeRuleFromKernel(pr PolicyRuleItem) error {
  540. if pr.Priority > 0 {
  541. _ = exec.Command("ip", "rule", "del", "pref", strconv.Itoa(pr.Priority)).Run()
  542. return nil
  543. }
  544. args := []string{"rule", "del"}
  545. if pr.RoutingMark != "" {
  546. markClean := pr.RoutingMark
  547. if strings.Contains(markClean, "(") {
  548. markClean = strings.TrimSpace(markClean[:strings.Index(markClean, "(")])
  549. }
  550. args = append(args, "fwmark", markClean)
  551. }
  552. if pr.Table != "" {
  553. args = append(args, "lookup", pr.Table)
  554. }
  555. _ = exec.Command("ip", args...).Run()
  556. return nil
  557. }
  558. // ==================== Routing Tables ====================
  559. func (m *RoutingManager) ListTables() []RoutingTableItem {
  560. m.mu.Lock()
  561. defer m.mu.Unlock()
  562. tables := []RoutingTableItem{
  563. {Name: "main", TableID: 254, Comment: "Default system routing table"},
  564. {Name: "default", TableID: 253, Comment: "Default fallback table"},
  565. {Name: "local", TableID: 255, Comment: "Local kernel device table"},
  566. }
  567. tables = append(tables, m.customTables...)
  568. return tables
  569. }
  570. func (m *RoutingManager) SaveTable(t RoutingTableItem) error {
  571. m.mu.Lock()
  572. defer m.mu.Unlock()
  573. if t.Name == "" {
  574. return fmt.Errorf("table name is required")
  575. }
  576. if t.TableID == 0 {
  577. // Auto assign FIB ID starting at 100
  578. t.TableID = 100 + len(m.customTables)
  579. }
  580. for _, existing := range m.customTables {
  581. if existing.Name == t.Name || existing.TableID == t.TableID {
  582. return fmt.Errorf("table with name %q or ID %d already exists", t.Name, t.TableID)
  583. }
  584. }
  585. m.customTables = append(m.customTables, t)
  586. _ = m.save()
  587. m.syncCustomTablesToSystem()
  588. return nil
  589. }
  590. func (m *RoutingManager) getTableMap() map[int]string {
  591. tableMap := map[int]string{
  592. 254: "main",
  593. 253: "default",
  594. 255: "local",
  595. }
  596. // Read /etc/iproute2/rt_tables
  597. if f, err := os.Open(RTTableFilePath); err == nil {
  598. defer f.Close()
  599. scanner := bufio.NewScanner(f)
  600. for scanner.Scan() {
  601. line := strings.TrimSpace(scanner.Text())
  602. if line == "" || strings.HasPrefix(line, "#") {
  603. continue
  604. }
  605. parts := strings.Fields(line)
  606. if len(parts) >= 2 {
  607. if id, err := strconv.Atoi(parts[0]); err == nil {
  608. tableMap[id] = parts[1]
  609. }
  610. }
  611. }
  612. }
  613. for _, t := range m.customTables {
  614. tableMap[t.TableID] = t.Name
  615. }
  616. return tableMap
  617. }