manager.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. package routing
  2. import (
  3. "bufio"
  4. "encoding/json"
  5. "fmt"
  6. "net"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "time"
  14. "github.com/vishvananda/netlink"
  15. )
  16. const (
  17. StaticRoutesStorePath = "/etc/proxmox-router/routing/static_routes.json"
  18. PolicyRulesStorePath = "/etc/proxmox-router/routing/policy_rules.json"
  19. CustomTablesStorePath = "/etc/proxmox-router/routing/custom_tables.json"
  20. RTTableFilePath = "/etc/iproute2/rt_tables"
  21. )
  22. type RoutingManager struct {
  23. mu sync.Mutex
  24. staticRoutes []RouteItem
  25. policyRules []PolicyRuleItem
  26. customTables []RoutingTableItem
  27. }
  28. func NewRoutingManager() *RoutingManager {
  29. m := &RoutingManager{
  30. staticRoutes: make([]RouteItem, 0),
  31. policyRules: make([]PolicyRuleItem, 0),
  32. customTables: make([]RoutingTableItem, 0),
  33. }
  34. m.initRTTableFile()
  35. m.load()
  36. m.syncCustomTablesToSystem()
  37. m.applyAll()
  38. return m
  39. }
  40. func (m *RoutingManager) initRTTableFile() {
  41. _ = os.MkdirAll(filepath.Dir(RTTableFilePath), 0755)
  42. if _, err := os.Stat(RTTableFilePath); os.IsNotExist(err) {
  43. // Copy from /usr/share/iproute2/rt_tables or seed default
  44. content, err := os.ReadFile("/usr/share/iproute2/rt_tables")
  45. if err == nil {
  46. _ = os.WriteFile(RTTableFilePath, content, 0644)
  47. } else {
  48. defaultContent := "# reserved values\n255\tlocal\n254\tmain\n253\tdefault\n0\tunspec\n"
  49. _ = os.WriteFile(RTTableFilePath, []byte(defaultContent), 0644)
  50. }
  51. }
  52. }
  53. func (m *RoutingManager) load() {
  54. if data, err := os.ReadFile(StaticRoutesStorePath); err == nil {
  55. _ = json.Unmarshal(data, &m.staticRoutes)
  56. }
  57. if data, err := os.ReadFile(PolicyRulesStorePath); err == nil {
  58. _ = json.Unmarshal(data, &m.policyRules)
  59. }
  60. if data, err := os.ReadFile(CustomTablesStorePath); err == nil {
  61. _ = json.Unmarshal(data, &m.customTables)
  62. }
  63. }
  64. func (m *RoutingManager) save() error {
  65. _ = os.MkdirAll("/etc/proxmox-router/routing", 0755)
  66. if d, err := json.MarshalIndent(m.staticRoutes, "", " "); err == nil {
  67. _ = os.WriteFile(StaticRoutesStorePath, d, 0600)
  68. }
  69. if d, err := json.MarshalIndent(m.policyRules, "", " "); err == nil {
  70. _ = os.WriteFile(PolicyRulesStorePath, d, 0600)
  71. }
  72. if d, err := json.MarshalIndent(m.customTables, "", " "); err == nil {
  73. _ = os.WriteFile(CustomTablesStorePath, d, 0600)
  74. }
  75. return nil
  76. }
  77. func (m *RoutingManager) syncCustomTablesToSystem() {
  78. content, _ := os.ReadFile(RTTableFilePath)
  79. str := string(content)
  80. var sb strings.Builder
  81. sb.WriteString(str)
  82. for _, t := range m.customTables {
  83. check := fmt.Sprintf("\t%s", t.Name)
  84. if !strings.Contains(str, check) {
  85. sb.WriteString(fmt.Sprintf("%d\t%s\n", t.TableID, t.Name))
  86. }
  87. }
  88. _ = os.WriteFile(RTTableFilePath, []byte(sb.String()), 0644)
  89. }
  90. func (m *RoutingManager) applyAll() {
  91. // Apply all static routes
  92. for _, r := range m.staticRoutes {
  93. if !r.Disabled {
  94. _ = m.applyRouteToKernel(r)
  95. }
  96. }
  97. // Apply policy rules
  98. for _, pr := range m.policyRules {
  99. _ = m.applyRuleToKernel(pr)
  100. }
  101. }
  102. // ==================== Routes (/ip route) ====================
  103. func (m *RoutingManager) ListRoutes() []RouteItem {
  104. m.mu.Lock()
  105. defer m.mu.Unlock()
  106. // 1. Get live routes from netlink across all tables
  107. tableNames := m.getTableMap()
  108. links, _ := netlink.LinkList()
  109. linkMap := make(map[int]string)
  110. for _, l := range links {
  111. linkMap[l.Attrs().Index] = l.Attrs().Name
  112. }
  113. var liveRoutes []RouteItem
  114. routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
  115. if err == nil {
  116. for _, r := range routes {
  117. dst := "0.0.0.0/0"
  118. if r.Dst != nil {
  119. dst = r.Dst.String()
  120. }
  121. gw := ""
  122. if r.Gw != nil {
  123. gw = r.Gw.String()
  124. }
  125. tableName := "main"
  126. if name, ok := tableNames[r.Table]; ok {
  127. tableName = name
  128. } else if r.Table != 254 && r.Table != 0 {
  129. tableName = strconv.Itoa(r.Table)
  130. }
  131. ifName := linkMap[r.LinkIndex]
  132. isDirect := r.Scope == netlink.SCOPE_LINK
  133. liveRoutes = append(liveRoutes, RouteItem{
  134. ID: fmt.Sprintf("kr-%d-%s-%s", r.Table, dst, gw),
  135. DstAddress: dst,
  136. Gateway: gw,
  137. Interface: ifName,
  138. Distance: r.Priority,
  139. RoutingTable: tableName,
  140. TableID: r.Table,
  141. Active: true,
  142. Connect: isDirect,
  143. Dynamic: !isDirect && (r.Protocol == netlink.RouteProtocol(2) || r.Protocol == netlink.RouteProtocol(3)),
  144. Static: !isDirect && r.Protocol != netlink.RouteProtocol(2) && r.Protocol != netlink.RouteProtocol(3),
  145. Disabled: false,
  146. })
  147. }
  148. }
  149. // Merge static routes that might be disabled or pending
  150. for _, sr := range m.staticRoutes {
  151. found := false
  152. for i, lr := range liveRoutes {
  153. if lr.DstAddress == sr.DstAddress && lr.RoutingTable == sr.RoutingTable && (lr.Gateway == sr.Gateway || sr.Gateway == "") {
  154. liveRoutes[i].Comment = sr.Comment
  155. liveRoutes[i].Static = true
  156. liveRoutes[i].ID = sr.ID
  157. found = true
  158. break
  159. }
  160. }
  161. if !found {
  162. // Inactive or disabled static route
  163. srCopy := sr
  164. srCopy.Active = false
  165. srCopy.Static = true
  166. liveRoutes = append(liveRoutes, srCopy)
  167. }
  168. }
  169. return liveRoutes
  170. }
  171. func (m *RoutingManager) SaveRoute(r RouteItem) error {
  172. m.mu.Lock()
  173. defer m.mu.Unlock()
  174. if r.DstAddress == "" {
  175. return fmt.Errorf("destination address is required")
  176. }
  177. if r.RoutingTable == "" {
  178. r.RoutingTable = "main"
  179. }
  180. if r.ID == "" {
  181. r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
  182. }
  183. r.Static = true
  184. found := false
  185. for i, existing := range m.staticRoutes {
  186. if existing.ID == r.ID {
  187. m.staticRoutes[i] = r
  188. found = true
  189. break
  190. }
  191. }
  192. if !found {
  193. m.staticRoutes = append(m.staticRoutes, r)
  194. }
  195. _ = m.save()
  196. if r.Disabled {
  197. _ = m.removeRouteFromKernel(r)
  198. } else {
  199. if err := m.applyRouteToKernel(r); err != nil {
  200. return err
  201. }
  202. }
  203. return nil
  204. }
  205. func (m *RoutingManager) DeleteRoute(id string) error {
  206. m.mu.Lock()
  207. defer m.mu.Unlock()
  208. var updated []RouteItem
  209. var target *RouteItem
  210. for _, r := range m.staticRoutes {
  211. if r.ID == id {
  212. target = &r
  213. } else {
  214. updated = append(updated, r)
  215. }
  216. }
  217. if target != nil {
  218. _ = m.removeRouteFromKernel(*target)
  219. }
  220. m.staticRoutes = updated
  221. return m.save()
  222. }
  223. func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
  224. m.mu.Lock()
  225. defer m.mu.Unlock()
  226. for i := range m.staticRoutes {
  227. if m.staticRoutes[i].ID == id {
  228. m.staticRoutes[i].Disabled = disabled
  229. _ = m.save()
  230. if disabled {
  231. _ = m.removeRouteFromKernel(m.staticRoutes[i])
  232. } else {
  233. _ = m.applyRouteToKernel(m.staticRoutes[i])
  234. }
  235. return nil
  236. }
  237. }
  238. return fmt.Errorf("route not found")
  239. }
  240. func (m *RoutingManager) applyRouteToKernel(r RouteItem) error {
  241. args := []string{"route", "replace"}
  242. if r.DstAddress == "default" || r.DstAddress == "0.0.0.0/0" {
  243. args = append(args, "default")
  244. } else {
  245. args = append(args, r.DstAddress)
  246. }
  247. if r.Gateway != "" {
  248. // check if gateway is IP or device
  249. if net.ParseIP(r.Gateway) != nil {
  250. args = append(args, "via", r.Gateway)
  251. } else {
  252. args = append(args, "dev", r.Gateway)
  253. }
  254. }
  255. if r.Interface != "" && r.Gateway != r.Interface {
  256. args = append(args, "dev", r.Interface)
  257. }
  258. if r.Distance > 0 {
  259. args = append(args, "metric", strconv.Itoa(r.Distance))
  260. }
  261. if r.RoutingTable != "" && r.RoutingTable != "main" {
  262. args = append(args, "table", r.RoutingTable)
  263. }
  264. cmd := exec.Command("ip", args...)
  265. out, err := cmd.CombinedOutput()
  266. if err != nil {
  267. return fmt.Errorf("failed to apply route (%s): %s: %w", strings.Join(args, " "), string(out), err)
  268. }
  269. return nil
  270. }
  271. func (m *RoutingManager) removeRouteFromKernel(r RouteItem) error {
  272. args := []string{"route", "del"}
  273. if r.DstAddress == "default" || r.DstAddress == "0.0.0.0/0" {
  274. args = append(args, "default")
  275. } else {
  276. args = append(args, r.DstAddress)
  277. }
  278. if r.RoutingTable != "" && r.RoutingTable != "main" {
  279. args = append(args, "table", r.RoutingTable)
  280. }
  281. _ = exec.Command("ip", args...).Run()
  282. return nil
  283. }
  284. // ==================== Policy Rules (/ip route rule) ====================
  285. func (m *RoutingManager) ListPolicyRules() []PolicyRuleItem {
  286. m.mu.Lock()
  287. defer m.mu.Unlock()
  288. // Query live rules from kernel
  289. rules, err := netlink.RuleList(netlink.FAMILY_V4)
  290. if err != nil {
  291. return m.policyRules
  292. }
  293. tableNames := m.getTableMap()
  294. var res []PolicyRuleItem
  295. for _, r := range rules {
  296. tableName := "main"
  297. if name, ok := tableNames[r.Table]; ok {
  298. tableName = name
  299. } else if r.Table != 254 && r.Table != 0 {
  300. tableName = strconv.Itoa(r.Table)
  301. }
  302. src := ""
  303. if r.Src != nil {
  304. src = r.Src.String()
  305. }
  306. dst := ""
  307. if r.Dst != nil {
  308. dst = r.Dst.String()
  309. }
  310. markStr := ""
  311. if r.Mark > 0 {
  312. markStr = fmt.Sprintf("0x%x (%d)", r.Mark, r.Mark)
  313. }
  314. id := fmt.Sprintf("rule-%d-%d", r.Priority, r.Table)
  315. res = append(res, PolicyRuleItem{
  316. ID: id,
  317. Priority: r.Priority,
  318. SrcAddress: src,
  319. DstAddress: dst,
  320. RoutingMark: markStr,
  321. Table: tableName,
  322. TableID: r.Table,
  323. Action: "lookup",
  324. })
  325. }
  326. return res
  327. }
  328. func (m *RoutingManager) SavePolicyRule(pr PolicyRuleItem) error {
  329. m.mu.Lock()
  330. defer m.mu.Unlock()
  331. if pr.Table == "" {
  332. pr.Table = "main"
  333. }
  334. if pr.ID == "" {
  335. pr.ID = fmt.Sprintf("prule-%d", time.Now().UnixNano())
  336. }
  337. if pr.Priority == 0 {
  338. pr.Priority = 1000 + len(m.policyRules)*10
  339. }
  340. if err := m.applyRuleToKernel(pr); err != nil {
  341. return err
  342. }
  343. m.policyRules = append(m.policyRules, pr)
  344. return m.save()
  345. }
  346. func (m *RoutingManager) DeletePolicyRule(id string) error {
  347. m.mu.Lock()
  348. defer m.mu.Unlock()
  349. var updated []PolicyRuleItem
  350. var target *PolicyRuleItem
  351. for _, pr := range m.policyRules {
  352. if pr.ID == id {
  353. target = &pr
  354. } else {
  355. updated = append(updated, pr)
  356. }
  357. }
  358. if target != nil {
  359. _ = m.removeRuleFromKernel(*target)
  360. }
  361. m.policyRules = updated
  362. return m.save()
  363. }
  364. func (m *RoutingManager) applyRuleToKernel(pr PolicyRuleItem) error {
  365. args := []string{"rule", "add"}
  366. if pr.Priority > 0 {
  367. args = append(args, "pref", strconv.Itoa(pr.Priority))
  368. }
  369. if pr.SrcAddress != "" {
  370. args = append(args, "from", pr.SrcAddress)
  371. }
  372. if pr.DstAddress != "" {
  373. args = append(args, "to", pr.DstAddress)
  374. }
  375. if pr.RoutingMark != "" {
  376. // Clean mark string if it has (hex)
  377. markClean := pr.RoutingMark
  378. if strings.Contains(markClean, "(") {
  379. markClean = strings.TrimSpace(markClean[:strings.Index(markClean, "(")])
  380. }
  381. args = append(args, "fwmark", markClean)
  382. }
  383. if pr.Table != "" {
  384. args = append(args, "lookup", pr.Table)
  385. }
  386. cmd := exec.Command("ip", args...)
  387. out, err := cmd.CombinedOutput()
  388. if err != nil {
  389. return fmt.Errorf("failed to add rule (%s): %s: %w", strings.Join(args, " "), string(out), err)
  390. }
  391. return nil
  392. }
  393. func (m *RoutingManager) removeRuleFromKernel(pr PolicyRuleItem) error {
  394. args := []string{"rule", "del"}
  395. if pr.Priority > 0 {
  396. args = append(args, "pref", strconv.Itoa(pr.Priority))
  397. }
  398. if pr.RoutingMark != "" {
  399. markClean := pr.RoutingMark
  400. if strings.Contains(markClean, "(") {
  401. markClean = strings.TrimSpace(markClean[:strings.Index(markClean, "(")])
  402. }
  403. args = append(args, "fwmark", markClean)
  404. }
  405. if pr.Table != "" {
  406. args = append(args, "lookup", pr.Table)
  407. }
  408. _ = exec.Command("ip", args...).Run()
  409. return nil
  410. }
  411. // ==================== Routing Tables ====================
  412. func (m *RoutingManager) ListTables() []RoutingTableItem {
  413. m.mu.Lock()
  414. defer m.mu.Unlock()
  415. tables := []RoutingTableItem{
  416. {Name: "main", TableID: 254, Comment: "Default system routing table"},
  417. {Name: "default", TableID: 253, Comment: "Default fallback table"},
  418. {Name: "local", TableID: 255, Comment: "Local kernel device table"},
  419. }
  420. tables = append(tables, m.customTables...)
  421. return tables
  422. }
  423. func (m *RoutingManager) SaveTable(t RoutingTableItem) error {
  424. m.mu.Lock()
  425. defer m.mu.Unlock()
  426. if t.Name == "" {
  427. return fmt.Errorf("table name is required")
  428. }
  429. if t.TableID == 0 {
  430. // Auto assign FIB ID starting at 100
  431. t.TableID = 100 + len(m.customTables)
  432. }
  433. for _, existing := range m.customTables {
  434. if existing.Name == t.Name || existing.TableID == t.TableID {
  435. return fmt.Errorf("table with name %q or ID %d already exists", t.Name, t.TableID)
  436. }
  437. }
  438. m.customTables = append(m.customTables, t)
  439. _ = m.save()
  440. m.syncCustomTablesToSystem()
  441. return nil
  442. }
  443. func (m *RoutingManager) getTableMap() map[int]string {
  444. tableMap := map[int]string{
  445. 254: "main",
  446. 253: "default",
  447. 255: "local",
  448. }
  449. // Read /etc/iproute2/rt_tables
  450. if f, err := os.Open(RTTableFilePath); err == nil {
  451. defer f.Close()
  452. scanner := bufio.NewScanner(f)
  453. for scanner.Scan() {
  454. line := strings.TrimSpace(scanner.Text())
  455. if line == "" || strings.HasPrefix(line, "#") {
  456. continue
  457. }
  458. parts := strings.Fields(line)
  459. if len(parts) >= 2 {
  460. if id, err := strconv.Atoi(parts[0]); err == nil {
  461. tableMap[id] = parts[1]
  462. }
  463. }
  464. }
  465. }
  466. for _, t := range m.customTables {
  467. tableMap[t.TableID] = t.Name
  468. }
  469. return tableMap
  470. }