|
|
@@ -10,6 +10,7 @@ import (
|
|
|
"os"
|
|
|
"os/exec"
|
|
|
"path/filepath"
|
|
|
+ "regexp"
|
|
|
"sort"
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
@@ -44,7 +45,9 @@ func NewRoutingManager() *RoutingManager {
|
|
|
}
|
|
|
m.initRTTableFile()
|
|
|
m.load()
|
|
|
- m.syncCustomTablesToSystem()
|
|
|
+ if err := m.syncCustomTablesToSystem(); err != nil {
|
|
|
+ log.Printf("routing table synchronization failed: %v", err)
|
|
|
+ }
|
|
|
m.applyAll()
|
|
|
m.startHealthCheckWorker()
|
|
|
m.startRouteSyncWorker()
|
|
|
@@ -151,21 +154,49 @@ func (m *RoutingManager) save() error {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-func (m *RoutingManager) syncCustomTablesToSystem() {
|
|
|
- content, _ := os.ReadFile(RTTableFilePath)
|
|
|
- str := string(content)
|
|
|
-
|
|
|
+func (m *RoutingManager) syncCustomTablesToSystem() error {
|
|
|
+ content, err := os.ReadFile(RTTableFilePath)
|
|
|
+ if err != nil && !os.IsNotExist(err) {
|
|
|
+ return fmt.Errorf("reading routing table registry: %w", err)
|
|
|
+ }
|
|
|
var sb strings.Builder
|
|
|
- sb.WriteString(str)
|
|
|
-
|
|
|
- for _, t := range m.customTables {
|
|
|
- check := fmt.Sprintf("\t%s", t.Name)
|
|
|
- if !strings.Contains(str, check) {
|
|
|
- sb.WriteString(fmt.Sprintf("%d\t%s\n", t.TableID, t.Name))
|
|
|
+ sb.Write(content)
|
|
|
+ if len(content) > 0 && content[len(content)-1] != '\n' {
|
|
|
+ sb.WriteByte('\n')
|
|
|
+ }
|
|
|
+ existing := map[string]bool{}
|
|
|
+ for _, line := range strings.Split(string(content), "\n") {
|
|
|
+ fields := strings.Fields(line)
|
|
|
+ if len(fields) >= 2 && !strings.HasPrefix(fields[0], "#") {
|
|
|
+ existing[fields[1]] = true
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- _ = os.WriteFile(RTTableFilePath, []byte(sb.String()), 0644)
|
|
|
+ for _, table := range m.customTables {
|
|
|
+ if !existing[table.Name] {
|
|
|
+ fmt.Fprintf(&sb, "%d\t%s\n", table.TableID, table.Name)
|
|
|
+ existing[table.Name] = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ dir := filepath.Dir(RTTableFilePath)
|
|
|
+ f, err := os.CreateTemp(dir, ".rt-tables-*.tmp")
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ name := f.Name()
|
|
|
+ defer os.Remove(name)
|
|
|
+ if err = f.Chmod(0644); err == nil {
|
|
|
+ _, err = f.WriteString(sb.String())
|
|
|
+ }
|
|
|
+ if err == nil {
|
|
|
+ err = f.Sync()
|
|
|
+ }
|
|
|
+ if closeErr := f.Close(); err == nil {
|
|
|
+ err = closeErr
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ return os.Rename(name, RTTableFilePath)
|
|
|
}
|
|
|
|
|
|
func (m *RoutingManager) cleanRogueDefaultRoutes() {
|
|
|
@@ -1379,10 +1410,15 @@ func (m *RoutingManager) ListPolicyRules() []PolicyRuleItem {
|
|
|
tableNames := m.getTableMap()
|
|
|
var res []PolicyRuleItem
|
|
|
|
|
|
- userRulesByPrio := make(map[int]PolicyRuleItem)
|
|
|
+ actions, actionErr := m.listKernelPolicyActions()
|
|
|
+ if actionErr != nil {
|
|
|
+ log.Printf("policy rule action lookup failed: %v", actionErr)
|
|
|
+ }
|
|
|
+ userRulesByPrio := make(map[int][]PolicyRuleItem)
|
|
|
for _, pr := range m.policyRules {
|
|
|
- userRulesByPrio[pr.Priority] = pr
|
|
|
+ userRulesByPrio[pr.Priority] = append(userRulesByPrio[pr.Priority], pr)
|
|
|
}
|
|
|
+ matchedManaged := make(map[string]bool)
|
|
|
|
|
|
for _, r := range rules {
|
|
|
tableName := "main"
|
|
|
@@ -1406,6 +1442,13 @@ func (m *RoutingManager) ListPolicyRules() []PolicyRuleItem {
|
|
|
markStr = fmt.Sprintf("0x%x (%d)", r.Mark, r.Mark)
|
|
|
}
|
|
|
|
|
|
+ action := actions[policyKernelKey(r.Priority, src, dst, r.Mark, r.Table)]
|
|
|
+ if action == "" {
|
|
|
+ action = "unknown"
|
|
|
+ }
|
|
|
+ if action != "lookup" {
|
|
|
+ tableName = ""
|
|
|
+ }
|
|
|
id := fmt.Sprintf("rule-%d-%d", r.Priority, r.Table)
|
|
|
comment := ""
|
|
|
isDefault := false
|
|
|
@@ -1414,16 +1457,16 @@ func (m *RoutingManager) ListPolicyRules() []PolicyRuleItem {
|
|
|
if r.Priority == 0 || r.Priority == 32766 || r.Priority == 32767 {
|
|
|
isDefault = true
|
|
|
isSystem = true
|
|
|
- } else if userRule, ok := userRulesByPrio[r.Priority]; ok {
|
|
|
- if userRule.ID != "" {
|
|
|
- id = userRule.ID
|
|
|
- }
|
|
|
- comment = userRule.Comment
|
|
|
- isDefault = false
|
|
|
- isSystem = false
|
|
|
} else {
|
|
|
- // System/daemon rule not added by user in debtik (e.g. WireGuard, IPsec)
|
|
|
+ // A priority alone does not identify a rule: kernel priorities may be shared.
|
|
|
isSystem = true
|
|
|
+ for _, userRule := range userRulesByPrio[r.Priority] {
|
|
|
+ if !matchedManaged[userRule.ID] && m.matchesPolicyRule(userRule, r, action) {
|
|
|
+ id, comment, isSystem = userRule.ID, userRule.Comment, false
|
|
|
+ matchedManaged[userRule.ID] = true
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
res = append(res, PolicyRuleItem{
|
|
|
@@ -1434,7 +1477,7 @@ func (m *RoutingManager) ListPolicyRules() []PolicyRuleItem {
|
|
|
RoutingMark: markStr,
|
|
|
Table: tableName,
|
|
|
TableID: r.Table,
|
|
|
- Action: "lookup",
|
|
|
+ Action: action,
|
|
|
Comment: comment,
|
|
|
IsDefault: isDefault,
|
|
|
IsSystem: isSystem,
|
|
|
@@ -1444,12 +1487,161 @@ func (m *RoutingManager) ListPolicyRules() []PolicyRuleItem {
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
+type policyJSONRule struct {
|
|
|
+ Priority int `json:"priority"`
|
|
|
+ Src string `json:"src"`
|
|
|
+ SrcLen *int `json:"srclen"`
|
|
|
+ Dst string `json:"dst"`
|
|
|
+ DstLen *int `json:"dstlen"`
|
|
|
+ Mark string `json:"fwmark"`
|
|
|
+ Table json.RawMessage `json:"table"`
|
|
|
+ Action string `json:"action"`
|
|
|
+}
|
|
|
+
|
|
|
+func policyKernelKey(priority int, src, dst string, mark uint32, table int) string {
|
|
|
+ return fmt.Sprintf("%d|%s|%s|%d|%d", priority, normalizedRulePrefix(src), normalizedRulePrefix(dst), mark, table)
|
|
|
+}
|
|
|
+func jsonRulePrefix(addr string, bits *int) string {
|
|
|
+ if addr == "" || addr == "all" {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ if bits == nil {
|
|
|
+ return addr
|
|
|
+ }
|
|
|
+ return fmt.Sprintf("%s/%d", addr, *bits)
|
|
|
+}
|
|
|
+
|
|
|
+// netlink v1.3.1 does not populate Rule.Type, so obtain actions from iproute2.
|
|
|
+// Ambiguous snapshots are intentionally not attributed to managed rules.
|
|
|
+func (m *RoutingManager) listKernelPolicyActions() (map[string]string, error) {
|
|
|
+ out, err := exec.Command("ip", "-j", "-4", "rule", "show").CombinedOutput()
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("listing policy actions: %s: %w", strings.TrimSpace(string(out)), err)
|
|
|
+ }
|
|
|
+ var rules []policyJSONRule
|
|
|
+ if err = json.Unmarshal(out, &rules); err != nil {
|
|
|
+ return nil, fmt.Errorf("parsing policy rules: %w", err)
|
|
|
+ }
|
|
|
+ result := make(map[string]string)
|
|
|
+ for _, r := range rules {
|
|
|
+ var table int
|
|
|
+ if len(r.Table) > 0 && string(r.Table) != "null" {
|
|
|
+ var name string
|
|
|
+ if json.Unmarshal(r.Table, &name) == nil {
|
|
|
+ table = m.getTableIDByName(name)
|
|
|
+ } else if err = json.Unmarshal(r.Table, &table); err != nil {
|
|
|
+ return nil, fmt.Errorf("invalid policy table: %w", err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ action := strings.ToLower(r.Action)
|
|
|
+ if action == "" && table != 0 {
|
|
|
+ action = "lookup"
|
|
|
+ }
|
|
|
+ if action == "" {
|
|
|
+ action = "unknown"
|
|
|
+ }
|
|
|
+ var mark uint64
|
|
|
+ if r.Mark != "" {
|
|
|
+ mark, err = strconv.ParseUint(strings.Split(r.Mark, "/")[0], 0, 32)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("invalid policy mark: %w", err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ key := policyKernelKey(r.Priority, jsonRulePrefix(r.Src, r.SrcLen), jsonRulePrefix(r.Dst, r.DstLen), uint32(mark), table)
|
|
|
+ if _, exists := result[key]; exists {
|
|
|
+ result[key] = ""
|
|
|
+ } else {
|
|
|
+ result[key] = action
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result, nil
|
|
|
+}
|
|
|
+func normalizedRulePrefix(value string) string {
|
|
|
+ value = strings.TrimSpace(value)
|
|
|
+ if value == "" {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ if ip := net.ParseIP(value); ip != nil {
|
|
|
+ if ip.To4() != nil {
|
|
|
+ return ip.String() + "/32"
|
|
|
+ }
|
|
|
+ return ip.String() + "/128"
|
|
|
+ }
|
|
|
+ _, network, err := net.ParseCIDR(value)
|
|
|
+ if err != nil {
|
|
|
+ return value
|
|
|
+ }
|
|
|
+ ones, _ := network.Mask.Size()
|
|
|
+ if ones == 0 {
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+ return network.String()
|
|
|
+}
|
|
|
+func (m *RoutingManager) matchesPolicyRule(saved PolicyRuleItem, live netlink.Rule, liveAction string) bool {
|
|
|
+ if saved.Priority != live.Priority || live.IifName != "" || live.OifName != "" || live.Invert || live.Tos != 0 {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ action := saved.Action
|
|
|
+ if action == "" {
|
|
|
+ action = "lookup"
|
|
|
+ }
|
|
|
+ if action != liveAction {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if action == "lookup" && m.getTableIDByName(saved.Table) != live.Table {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ src, dst := "", ""
|
|
|
+ if live.Src != nil {
|
|
|
+ src = live.Src.String()
|
|
|
+ }
|
|
|
+ if live.Dst != nil {
|
|
|
+ dst = live.Dst.String()
|
|
|
+ }
|
|
|
+ if normalizedRulePrefix(saved.SrcAddress) != normalizedRulePrefix(src) || normalizedRulePrefix(saved.DstAddress) != normalizedRulePrefix(dst) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ mark := strings.TrimSpace(strings.Split(saved.RoutingMark, "(")[0])
|
|
|
+ var expected uint64
|
|
|
+ if mark != "" {
|
|
|
+ var err error
|
|
|
+ expected, err = strconv.ParseUint(mark, 0, 32)
|
|
|
+ if err != nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if uint32(expected) != live.Mark {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if mark == "" && live.Mask != nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if mark != "" && expected == 0 && live.Mask == nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if live.Mask != nil && *live.Mask != ^uint32(0) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
func (m *RoutingManager) SavePolicyRule(pr PolicyRuleItem) error {
|
|
|
m.mu.Lock()
|
|
|
defer m.mu.Unlock()
|
|
|
-
|
|
|
- if pr.Table == "" {
|
|
|
- pr.Table = "main"
|
|
|
+ pr.Action = strings.ToLower(strings.TrimSpace(pr.Action))
|
|
|
+ if pr.Action == "" {
|
|
|
+ pr.Action = "lookup"
|
|
|
+ }
|
|
|
+ switch pr.Action {
|
|
|
+ case "lookup":
|
|
|
+ if pr.Table == "" {
|
|
|
+ pr.Table = "main"
|
|
|
+ }
|
|
|
+ case "blackhole", "unreachable", "prohibit":
|
|
|
+ pr.Table = ""
|
|
|
+ pr.TableID = 0
|
|
|
+ default:
|
|
|
+ return fmt.Errorf("invalid policy rule action: %s", pr.Action)
|
|
|
}
|
|
|
if pr.ID == "" {
|
|
|
pr.ID = fmt.Sprintf("prule-%d", time.Now().UnixNano())
|
|
|
@@ -1457,45 +1649,99 @@ func (m *RoutingManager) SavePolicyRule(pr PolicyRuleItem) error {
|
|
|
if pr.Priority == 0 {
|
|
|
pr.Priority = 1000 + len(m.policyRules)*10
|
|
|
}
|
|
|
-
|
|
|
+ if pr.Priority <= 0 || pr.Priority == 32766 || pr.Priority == 32767 {
|
|
|
+ return fmt.Errorf("invalid or reserved policy rule priority")
|
|
|
+ }
|
|
|
+ for _, existing := range m.policyRules {
|
|
|
+ if existing.ID == pr.ID || existing.Priority == pr.Priority {
|
|
|
+ return fmt.Errorf("policy rule ID or priority already exists")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ liveRules, err := netlink.RuleList(netlink.FAMILY_V4)
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("listing existing policy rules: %w", err)
|
|
|
+ }
|
|
|
+ actions, err := m.listKernelPolicyActions()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ for _, live := range liveRules {
|
|
|
+ if live.Priority != pr.Priority {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ src, dst := "", ""
|
|
|
+ if live.Src != nil {
|
|
|
+ src = live.Src.String()
|
|
|
+ }
|
|
|
+ if live.Dst != nil {
|
|
|
+ dst = live.Dst.String()
|
|
|
+ }
|
|
|
+ action := actions[policyKernelKey(live.Priority, src, dst, live.Mark, live.Table)]
|
|
|
+ if action == "" {
|
|
|
+ return fmt.Errorf("ambiguous existing policy rule state; retry")
|
|
|
+ }
|
|
|
+ if m.matchesPolicyRule(pr, live, action) {
|
|
|
+ return fmt.Errorf("matching policy rule already exists in kernel")
|
|
|
+ }
|
|
|
+ }
|
|
|
if err := m.applyRuleToKernel(pr); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
-
|
|
|
+ before := append([]PolicyRuleItem(nil), m.policyRules...)
|
|
|
m.policyRules = append(m.policyRules, pr)
|
|
|
- return m.save()
|
|
|
+ if err := m.save(); err != nil {
|
|
|
+ m.policyRules = before
|
|
|
+ persistErr := m.save()
|
|
|
+ if persistErr != nil {
|
|
|
+ persistErr = fmt.Errorf("restoring saved policy rules: %w", persistErr)
|
|
|
+ }
|
|
|
+ rollbackErr := m.removeRuleFromKernel(pr)
|
|
|
+ if rollbackErr != nil {
|
|
|
+ rollbackErr = fmt.Errorf("removing new kernel rule: %w", rollbackErr)
|
|
|
+ }
|
|
|
+ return errors.Join(fmt.Errorf("saving policy rule: %w", err), persistErr, rollbackErr)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
}
|
|
|
-
|
|
|
func (m *RoutingManager) DeletePolicyRule(id string) error {
|
|
|
m.mu.Lock()
|
|
|
defer m.mu.Unlock()
|
|
|
-
|
|
|
- var updated []PolicyRuleItem
|
|
|
- var target *PolicyRuleItem
|
|
|
- for _, pr := range m.policyRules {
|
|
|
+ index := -1
|
|
|
+ for i, pr := range m.policyRules {
|
|
|
if pr.ID == id || fmt.Sprintf("rule-%d-%d", pr.Priority, pr.TableID) == id {
|
|
|
- target = &pr
|
|
|
- } else {
|
|
|
- updated = append(updated, pr)
|
|
|
+ index = i
|
|
|
+ break
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- if target != nil {
|
|
|
- _ = m.removeRuleFromKernel(*target)
|
|
|
- } else if strings.HasPrefix(id, "rule-") {
|
|
|
- parts := strings.Split(id, "-")
|
|
|
- if len(parts) >= 2 {
|
|
|
- if prio, err := strconv.Atoi(parts[1]); err == nil && prio > 0 {
|
|
|
- _ = exec.Command("ip", "rule", "del", "pref", strconv.Itoa(prio)).Run()
|
|
|
- }
|
|
|
+ if index < 0 {
|
|
|
+ return fmt.Errorf("managed policy rule not found: %s", id)
|
|
|
+ }
|
|
|
+ before := append([]PolicyRuleItem(nil), m.policyRules...)
|
|
|
+ target := m.policyRules[index]
|
|
|
+ if err := m.removeRuleFromKernel(target); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ m.policyRules = append(m.policyRules[:index:index], m.policyRules[index+1:]...)
|
|
|
+ if err := m.save(); err != nil {
|
|
|
+ m.policyRules = before
|
|
|
+ persistErr := m.save()
|
|
|
+ if persistErr != nil {
|
|
|
+ persistErr = fmt.Errorf("restoring saved policy rules: %w", persistErr)
|
|
|
}
|
|
|
+ rollbackErr := m.applyRuleToKernel(target)
|
|
|
+ if rollbackErr != nil {
|
|
|
+ rollbackErr = fmt.Errorf("restoring kernel rule: %w", rollbackErr)
|
|
|
+ }
|
|
|
+ return errors.Join(fmt.Errorf("saving policy rule deletion: %w", err), persistErr, rollbackErr)
|
|
|
}
|
|
|
- m.policyRules = updated
|
|
|
- return m.save()
|
|
|
+ return nil
|
|
|
}
|
|
|
-
|
|
|
-func (m *RoutingManager) applyRuleToKernel(pr PolicyRuleItem) error {
|
|
|
- args := []string{"rule", "add"}
|
|
|
+func policyRuleArgs(verb string, pr PolicyRuleItem) ([]string, error) {
|
|
|
+ action := strings.ToLower(strings.TrimSpace(pr.Action))
|
|
|
+ if action == "" {
|
|
|
+ action = "lookup"
|
|
|
+ }
|
|
|
+ args := []string{"rule", verb}
|
|
|
if pr.Priority > 0 {
|
|
|
args = append(args, "pref", strconv.Itoa(pr.Priority))
|
|
|
}
|
|
|
@@ -1506,43 +1752,46 @@ func (m *RoutingManager) applyRuleToKernel(pr PolicyRuleItem) error {
|
|
|
args = append(args, "to", pr.DstAddress)
|
|
|
}
|
|
|
if pr.RoutingMark != "" {
|
|
|
- // Clean mark string if it has (hex)
|
|
|
- markClean := pr.RoutingMark
|
|
|
- if strings.Contains(markClean, "(") {
|
|
|
- markClean = strings.TrimSpace(markClean[:strings.Index(markClean, "(")])
|
|
|
+ mark := pr.RoutingMark
|
|
|
+ if strings.Contains(mark, "(") {
|
|
|
+ mark = strings.TrimSpace(mark[:strings.Index(mark, "(")])
|
|
|
}
|
|
|
- args = append(args, "fwmark", markClean)
|
|
|
+ args = append(args, "fwmark", mark)
|
|
|
}
|
|
|
- if pr.Table != "" {
|
|
|
- args = append(args, "lookup", pr.Table)
|
|
|
+ switch action {
|
|
|
+ case "lookup":
|
|
|
+ table := pr.Table
|
|
|
+ if table == "" {
|
|
|
+ table = "main"
|
|
|
+ }
|
|
|
+ args = append(args, "lookup", table)
|
|
|
+ case "blackhole", "unreachable", "prohibit":
|
|
|
+ args = append(args, action)
|
|
|
+ default:
|
|
|
+ return nil, fmt.Errorf("invalid policy rule action: %s", action)
|
|
|
}
|
|
|
-
|
|
|
- cmd := exec.Command("ip", args...)
|
|
|
- out, err := cmd.CombinedOutput()
|
|
|
+ return args, nil
|
|
|
+}
|
|
|
+func (m *RoutingManager) applyRuleToKernel(pr PolicyRuleItem) error {
|
|
|
+ args, err := policyRuleArgs("add", pr)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ out, err := exec.Command("ip", args...).CombinedOutput()
|
|
|
if err != nil {
|
|
|
- return fmt.Errorf("failed to add rule (%s): %s: %w", strings.Join(args, " "), string(out), err)
|
|
|
+ return fmt.Errorf("failed to add rule (%s): %s: %w", strings.Join(args, " "), strings.TrimSpace(string(out)), err)
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
-
|
|
|
func (m *RoutingManager) removeRuleFromKernel(pr PolicyRuleItem) error {
|
|
|
- if pr.Priority > 0 {
|
|
|
- _ = exec.Command("ip", "rule", "del", "pref", strconv.Itoa(pr.Priority)).Run()
|
|
|
- return nil
|
|
|
- }
|
|
|
- args := []string{"rule", "del"}
|
|
|
- if pr.RoutingMark != "" {
|
|
|
- markClean := pr.RoutingMark
|
|
|
- if strings.Contains(markClean, "(") {
|
|
|
- markClean = strings.TrimSpace(markClean[:strings.Index(markClean, "(")])
|
|
|
- }
|
|
|
- args = append(args, "fwmark", markClean)
|
|
|
+ args, err := policyRuleArgs("del", pr)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
}
|
|
|
- if pr.Table != "" {
|
|
|
- args = append(args, "lookup", pr.Table)
|
|
|
+ out, err := exec.Command("ip", args...).CombinedOutput()
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("failed to delete rule (%s): %s: %w", strings.Join(args, " "), strings.TrimSpace(string(out)), err)
|
|
|
}
|
|
|
-
|
|
|
- _ = exec.Command("ip", args...).Run()
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
@@ -1566,23 +1815,38 @@ func (m *RoutingManager) SaveTable(t RoutingTableItem) error {
|
|
|
m.mu.Lock()
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
|
- if t.Name == "" {
|
|
|
- return fmt.Errorf("table name is required")
|
|
|
+ t.Name = strings.TrimSpace(t.Name)
|
|
|
+ if t.Name == "" || !regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_.-]*$`).MatchString(t.Name) {
|
|
|
+ return fmt.Errorf("invalid routing table name")
|
|
|
+ }
|
|
|
+ if t.TableID < 0 || uint64(t.TableID) > uint64(^uint32(0)) {
|
|
|
+ return fmt.Errorf("invalid routing table ID")
|
|
|
}
|
|
|
if t.TableID == 0 {
|
|
|
// Auto assign FIB ID starting at 100
|
|
|
t.TableID = 100 + len(m.customTables)
|
|
|
}
|
|
|
|
|
|
- for _, existing := range m.customTables {
|
|
|
- if existing.Name == t.Name || existing.TableID == t.TableID {
|
|
|
+ for id, name := range m.getTableMap() {
|
|
|
+ if strings.EqualFold(name, t.Name) || id == t.TableID {
|
|
|
return fmt.Errorf("table with name %q or ID %d already exists", t.Name, t.TableID)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ before := append([]RoutingTableItem(nil), m.customTables...)
|
|
|
m.customTables = append(m.customTables, t)
|
|
|
- _ = m.save()
|
|
|
- m.syncCustomTablesToSystem()
|
|
|
+ if err := m.save(); err != nil {
|
|
|
+ m.customTables = before
|
|
|
+ return errors.Join(fmt.Errorf("saving routing table: %w", err), m.save())
|
|
|
+ }
|
|
|
+ if err := m.syncCustomTablesToSystem(); err != nil {
|
|
|
+ m.customTables = before
|
|
|
+ restoreErr := m.save()
|
|
|
+ if restoreErr != nil {
|
|
|
+ restoreErr = fmt.Errorf("restoring saved routing tables: %w", restoreErr)
|
|
|
+ }
|
|
|
+ return errors.Join(fmt.Errorf("registering routing table: %w", err), restoreErr)
|
|
|
+ }
|
|
|
return nil
|
|
|
}
|
|
|
|