manager.go 18 KB

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