manager.go 16 KB

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