|
|
@@ -3,6 +3,7 @@ package routing
|
|
|
import (
|
|
|
"bufio"
|
|
|
"encoding/json"
|
|
|
+ "errors"
|
|
|
"fmt"
|
|
|
"log"
|
|
|
"net"
|
|
|
@@ -766,7 +767,6 @@ func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
|
if strings.HasPrefix(origID, "kr-") {
|
|
|
for _, lr := range m.listLiveRoutesLocked() {
|
|
|
if lr.ID == origID {
|
|
|
- _ = m.deleteDirectKernelRoute(lr)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
@@ -840,6 +840,7 @@ func (m *RoutingManager) DeleteRoute(id string) error {
|
|
|
m.mu.Lock()
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
|
+ before := append([]RouteItem(nil), m.staticRoutes...)
|
|
|
var updated []RouteItem
|
|
|
var target *RouteItem
|
|
|
|
|
|
@@ -855,6 +856,7 @@ func (m *RoutingManager) DeleteRoute(id string) error {
|
|
|
|
|
|
// 2. If not found by direct ID, check if it's a live kernel route ID (e.g. kr-...)
|
|
|
if target == nil {
|
|
|
+ updated = nil
|
|
|
for _, lr := range m.listLiveRoutesLocked() {
|
|
|
if lr.ID == id {
|
|
|
// Check if any static route corresponds to this live route
|
|
|
@@ -887,30 +889,47 @@ func (m *RoutingManager) DeleteRoute(id string) error {
|
|
|
|
|
|
if target != nil {
|
|
|
m.staticRoutes = updated
|
|
|
- _ = m.save()
|
|
|
- _ = m.syncRouteGroupToKernel(target.DstAddress, target.RoutingTable, target.Distance)
|
|
|
- _ = m.deleteDirectKernelRoute(*target)
|
|
|
- return nil
|
|
|
+ return m.commitManagedRouteChange(before, *target)
|
|
|
}
|
|
|
|
|
|
return fmt.Errorf("route not found")
|
|
|
}
|
|
|
|
|
|
+// commitManagedRouteChange is called with the routing manager lock held.
|
|
|
+func (m *RoutingManager) commitManagedRouteChange(before []RouteItem, route RouteItem) error {
|
|
|
+ if err := m.save(); err != nil {
|
|
|
+ m.staticRoutes = before
|
|
|
+ return errors.Join(fmt.Errorf("saving route change: %w", err), m.save())
|
|
|
+ }
|
|
|
+ if err := m.syncRouteGroupToKernel(route.DstAddress, route.RoutingTable, route.Distance); err != nil {
|
|
|
+ m.staticRoutes = before
|
|
|
+ saveErr := m.save()
|
|
|
+ reapplyErr := m.syncRouteGroupToKernel(route.DstAddress, route.RoutingTable, route.Distance)
|
|
|
+ if saveErr != nil {
|
|
|
+ saveErr = fmt.Errorf("restoring stored routes: %w", saveErr)
|
|
|
+ }
|
|
|
+ if reapplyErr != nil {
|
|
|
+ reapplyErr = fmt.Errorf("reapplying previous route: %w", reapplyErr)
|
|
|
+ }
|
|
|
+ return errors.Join(fmt.Errorf("applying route change: %w", err), saveErr, reapplyErr)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
|
|
|
m.mu.Lock()
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
|
for i := range m.staticRoutes {
|
|
|
if m.staticRoutes[i].ID == id {
|
|
|
+ before := append([]RouteItem(nil), m.staticRoutes...)
|
|
|
m.staticRoutes[i].Disabled = disabled
|
|
|
if !disabled && m.staticRoutes[i].HealthStatus != "offline" {
|
|
|
m.staticRoutes[i].Active = true
|
|
|
} else if disabled {
|
|
|
m.staticRoutes[i].Active = false
|
|
|
}
|
|
|
- _ = m.save()
|
|
|
- _ = m.syncRouteGroupToKernel(m.staticRoutes[i].DstAddress, m.staticRoutes[i].RoutingTable, m.staticRoutes[i].Distance)
|
|
|
- return nil
|
|
|
+ return m.commitManagedRouteChange(before, m.staticRoutes[i])
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -922,15 +941,14 @@ func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
|
|
|
for i := range m.staticRoutes {
|
|
|
gwMatches := m.staticRoutes[i].Gateway == "" || lr.Gateway == m.staticRoutes[i].Gateway || lr.Interface == m.staticRoutes[i].Gateway || (m.staticRoutes[i].Interface != "" && lr.Interface == m.staticRoutes[i].Interface)
|
|
|
if normalizeDst(m.staticRoutes[i].DstAddress) == normLrDst && m.staticRoutes[i].RoutingTable == lr.RoutingTable && gwMatches {
|
|
|
+ before := append([]RouteItem(nil), m.staticRoutes...)
|
|
|
m.staticRoutes[i].Disabled = disabled
|
|
|
if !disabled && m.staticRoutes[i].HealthStatus != "offline" {
|
|
|
m.staticRoutes[i].Active = true
|
|
|
} else if disabled {
|
|
|
m.staticRoutes[i].Active = false
|
|
|
}
|
|
|
- _ = m.save()
|
|
|
- _ = m.syncRouteGroupToKernel(m.staticRoutes[i].DstAddress, m.staticRoutes[i].RoutingTable, m.staticRoutes[i].Distance)
|
|
|
- return nil
|
|
|
+ return m.commitManagedRouteChange(before, m.staticRoutes[i])
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -940,9 +958,16 @@ func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
|
|
|
lr.Static = true
|
|
|
lr.Active = false
|
|
|
lr.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
|
|
|
+ before := append([]RouteItem(nil), m.staticRoutes...)
|
|
|
m.staticRoutes = append(m.staticRoutes, lr)
|
|
|
- _ = m.save()
|
|
|
- _ = m.syncRouteGroupToKernel(lr.DstAddress, lr.RoutingTable, lr.Distance)
|
|
|
+ if err := m.save(); err != nil {
|
|
|
+ m.staticRoutes = before
|
|
|
+ return fmt.Errorf("saving adopted route: %w", err)
|
|
|
+ }
|
|
|
+ if err := m.syncRouteGroupToKernel(lr.DstAddress, lr.RoutingTable, lr.Distance); err != nil {
|
|
|
+ m.staticRoutes = before
|
|
|
+ return errors.Join(fmt.Errorf("disabling adopted route: %w", err), m.save())
|
|
|
+ }
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
@@ -1052,38 +1077,20 @@ func (m *RoutingManager) syncRouteGroupToKernel(dstAddress, routingTable string,
|
|
|
}
|
|
|
|
|
|
if len(activeRoutes) == 0 {
|
|
|
- args := []string{"route", "del", normDst}
|
|
|
- if distance > 0 {
|
|
|
- args = append(args, "metric", strconv.Itoa(distance))
|
|
|
- }
|
|
|
- if tbl != "main" {
|
|
|
- args = append(args, "table", tbl)
|
|
|
- }
|
|
|
- args = append(args, "proto", "static")
|
|
|
- _ = exec.Command("ip", args...).Run()
|
|
|
-
|
|
|
- // Also delete without proto static in case route protocol differed
|
|
|
- fbArgs := []string{"route", "del", normDst}
|
|
|
- if distance > 0 {
|
|
|
- fbArgs = append(fbArgs, "metric", strconv.Itoa(distance))
|
|
|
- }
|
|
|
- if tbl != "main" {
|
|
|
- fbArgs = append(fbArgs, "table", tbl)
|
|
|
- }
|
|
|
- _ = exec.Command("ip", fbArgs...).Run()
|
|
|
-
|
|
|
- // Bare fallback
|
|
|
- _ = exec.Command("ip", "route", "del", normDst).Run()
|
|
|
-
|
|
|
- // Also delete blackhole route variant if it existed
|
|
|
- bhArgs := []string{"route", "del", "blackhole", normDst}
|
|
|
- if distance > 0 {
|
|
|
- bhArgs = append(bhArgs, "metric", strconv.Itoa(distance))
|
|
|
- }
|
|
|
- if tbl != "main" {
|
|
|
- bhArgs = append(bhArgs, "table", tbl)
|
|
|
+ // Only touch the requested static route group. Broad fallbacks can delete
|
|
|
+ // a route from another table, metric, or routing protocol.
|
|
|
+ for _, routeType := range []string{"", "blackhole"} {
|
|
|
+ args := []string{"route", "del"}
|
|
|
+ if routeType != "" {
|
|
|
+ args = append(args, routeType)
|
|
|
+ }
|
|
|
+ args = append(args, normDst, "metric", strconv.Itoa(distance), "table", tbl, "proto", "static")
|
|
|
+ out, err := exec.Command("ip", args...).CombinedOutput()
|
|
|
+ missing := strings.Contains(string(out), "No such process") || (tbl == "main" && strings.Contains(string(out), "FIB table does not exist"))
|
|
|
+ if err != nil && !missing {
|
|
|
+ return fmt.Errorf("failed to delete static route group (%s): %s: %w", strings.Join(args, " "), strings.TrimSpace(string(out)), err)
|
|
|
+ }
|
|
|
}
|
|
|
- _ = exec.Command("ip", bhArgs...).Run()
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
@@ -1322,39 +1329,11 @@ func (m *RoutingManager) deleteDirectKernelRoute(r RouteItem) error {
|
|
|
if r.Distance > 0 {
|
|
|
args = append(args, "metric", strconv.Itoa(r.Distance))
|
|
|
}
|
|
|
- cmd := exec.Command("ip", args...)
|
|
|
- if out, err := cmd.CombinedOutput(); err == nil {
|
|
|
- return nil
|
|
|
- } else {
|
|
|
- log.Printf("[RouteManager] direct ip route del (%s): %s (%v), attempting fallback", strings.Join(args, " "), string(out), err)
|
|
|
- }
|
|
|
-
|
|
|
- if isBlackhole {
|
|
|
- fbBhArgs := []string{"route", "del", "blackhole", normDst}
|
|
|
- if r.RoutingTable != "" && r.RoutingTable != "main" {
|
|
|
- fbBhArgs = append(fbBhArgs, "table", r.RoutingTable)
|
|
|
- }
|
|
|
- if r.Distance > 0 {
|
|
|
- fbBhArgs = append(fbBhArgs, "metric", strconv.Itoa(r.Distance))
|
|
|
- }
|
|
|
- if err := exec.Command("ip", fbBhArgs...).Run(); err == nil {
|
|
|
- return nil
|
|
|
- }
|
|
|
- return exec.Command("ip", "route", "del", "blackhole", normDst).Run()
|
|
|
- }
|
|
|
-
|
|
|
- fbArgs := []string{"route", "del", normDst}
|
|
|
- if r.RoutingTable != "" && r.RoutingTable != "main" {
|
|
|
- fbArgs = append(fbArgs, "table", r.RoutingTable)
|
|
|
- }
|
|
|
- if r.Distance > 0 {
|
|
|
- fbArgs = append(fbArgs, "metric", strconv.Itoa(r.Distance))
|
|
|
- }
|
|
|
- if err := exec.Command("ip", fbArgs...).Run(); err == nil {
|
|
|
- return nil
|
|
|
+ out, err := exec.Command("ip", args...).CombinedOutput()
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("failed to delete route (%s): %s: %w", strings.Join(args, " "), strings.TrimSpace(string(out)), err)
|
|
|
}
|
|
|
-
|
|
|
- return exec.Command("ip", "route", "del", normDst).Run()
|
|
|
+ return nil
|
|
|
}
|
|
|
|
|
|
// ==================== Policy Rules (/ip route rule) ====================
|