|
@@ -441,7 +441,7 @@ func (m *RoutingManager) listLiveRoutesLocked() []RouteItem {
|
|
|
if cleanGw == "" {
|
|
if cleanGw == "" {
|
|
|
cleanGw = nhIfName
|
|
cleanGw = nhIfName
|
|
|
}
|
|
}
|
|
|
- routeID := fmt.Sprintf("kr-%d-%s-%s", r.Table, cleanDst, cleanGw)
|
|
|
|
|
|
|
+ routeID := fmt.Sprintf("kr-%d-%s-%s-m%d-p%d-t%d", r.Table, cleanDst, cleanGw, r.Priority, r.Protocol, r.Type)
|
|
|
|
|
|
|
|
liveRoutes = append(liveRoutes, RouteItem{
|
|
liveRoutes = append(liveRoutes, RouteItem{
|
|
|
ID: routeID,
|
|
ID: routeID,
|
|
@@ -477,7 +477,7 @@ func (m *RoutingManager) listLiveRoutesLocked() []RouteItem {
|
|
|
if cleanGw == "" {
|
|
if cleanGw == "" {
|
|
|
cleanGw = ifName
|
|
cleanGw = ifName
|
|
|
}
|
|
}
|
|
|
- routeID := fmt.Sprintf("kr-%d-%s-%s", r.Table, cleanDst, cleanGw)
|
|
|
|
|
|
|
+ routeID := fmt.Sprintf("kr-%d-%s-%s-m%d-p%d-t%d", r.Table, cleanDst, cleanGw, r.Priority, r.Protocol, r.Type)
|
|
|
|
|
|
|
|
liveRoutes = append(liveRoutes, RouteItem{
|
|
liveRoutes = append(liveRoutes, RouteItem{
|
|
|
ID: routeID,
|
|
ID: routeID,
|
|
@@ -509,17 +509,18 @@ func (m *RoutingManager) ListRoutes() []RouteItem {
|
|
|
|
|
|
|
|
// Merge static routes that might be disabled or have custom metadata
|
|
// Merge static routes that might be disabled or have custom metadata
|
|
|
matchedLive := make(map[int]bool)
|
|
matchedLive := make(map[int]bool)
|
|
|
|
|
+ kernelRouteCount := len(liveRoutes)
|
|
|
for _, sr := range m.staticRoutes {
|
|
for _, sr := range m.staticRoutes {
|
|
|
normSrDst := normalizeDst(sr.DstAddress)
|
|
normSrDst := normalizeDst(sr.DstAddress)
|
|
|
found := false
|
|
found := false
|
|
|
- for i, lr := range liveRoutes {
|
|
|
|
|
- if matchedLive[i] {
|
|
|
|
|
|
|
+ for i, lr := range liveRoutes[:kernelRouteCount] {
|
|
|
|
|
+ if sr.Disabled || !lr.Static || lr.Protocol != "static" || matchedLive[i] {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
if normalizeDst(lr.DstAddress) != normSrDst {
|
|
if normalizeDst(lr.DstAddress) != normSrDst {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
- if lr.RoutingTable != sr.RoutingTable {
|
|
|
|
|
|
|
+ if !sameRouteGroup(lr, sr) {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
gwMatches := sr.Gateway == "" || lr.Gateway == sr.Gateway || lr.Interface == sr.Gateway || (sr.Interface != "" && lr.Interface == sr.Interface)
|
|
gwMatches := sr.Gateway == "" || lr.Gateway == sr.Gateway || lr.Interface == sr.Gateway || (sr.Interface != "" && lr.Interface == sr.Interface)
|
|
@@ -609,6 +610,18 @@ func (m *RoutingManager) ListRoutes() []RouteItem {
|
|
|
return liveRoutes
|
|
return liveRoutes
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Route groups are keyed by destination, table and metric, not nexthop.
|
|
|
|
|
+func sameRouteGroup(a, b RouteItem) bool {
|
|
|
|
|
+ table := func(s string) string {
|
|
|
|
|
+ s = strings.TrimSpace(s)
|
|
|
|
|
+ if s == "" {
|
|
|
|
|
+ return "main"
|
|
|
|
|
+ }
|
|
|
|
|
+ return s
|
|
|
|
|
+ }
|
|
|
|
|
+ return normalizeDst(a.DstAddress) == normalizeDst(b.DstAddress) && table(a.RoutingTable) == table(b.RoutingTable) && a.Distance == b.Distance
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
|
if r == nil {
|
|
if r == nil {
|
|
|
return fmt.Errorf("route cannot be nil")
|
|
return fmt.Errorf("route cannot be nil")
|
|
@@ -616,16 +629,34 @@ func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
|
m.mu.Lock()
|
|
m.mu.Lock()
|
|
|
defer m.mu.Unlock()
|
|
defer m.mu.Unlock()
|
|
|
before := append([]RouteItem(nil), m.staticRoutes...)
|
|
before := append([]RouteItem(nil), m.staticRoutes...)
|
|
|
- dirty, applied := false, false
|
|
|
|
|
|
|
+ var oldRouteToClean *RouteItem
|
|
|
|
|
+ var unmanagedOriginal *RouteItem
|
|
|
|
|
+ dirty, newAttempted, oldAttempted := false, false, false
|
|
|
defer func() {
|
|
defer func() {
|
|
|
- if err != nil && dirty {
|
|
|
|
|
- m.staticRoutes = before
|
|
|
|
|
- if applied {
|
|
|
|
|
- _ = m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
|
|
|
|
|
|
|
+ if err == nil || !dirty {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ m.staticRoutes = append([]RouteItem(nil), before...)
|
|
|
|
|
+ if unmanagedOriginal != nil {
|
|
|
|
|
+ m.staticRoutes = append(m.staticRoutes, *unmanagedOriginal)
|
|
|
|
|
+ }
|
|
|
|
|
+ var failures []error
|
|
|
|
|
+ failures = append(failures, err)
|
|
|
|
|
+ if newAttempted {
|
|
|
|
|
+ if rollbackErr := m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance); rollbackErr != nil {
|
|
|
|
|
+ failures = append(failures, fmt.Errorf("restoring new route group: %w", rollbackErr))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if oldAttempted && oldRouteToClean != nil && !sameRouteGroup(*oldRouteToClean, *r) {
|
|
|
|
|
+ if rollbackErr := m.syncRouteGroupToKernel(oldRouteToClean.DstAddress, oldRouteToClean.RoutingTable, oldRouteToClean.Distance); rollbackErr != nil {
|
|
|
|
|
+ failures = append(failures, fmt.Errorf("restoring original route group: %w", rollbackErr))
|
|
|
}
|
|
}
|
|
|
- m.applyAll()
|
|
|
|
|
- _ = m.save()
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ m.staticRoutes = before
|
|
|
|
|
+ if saveErr := m.save(); saveErr != nil {
|
|
|
|
|
+ failures = append(failures, fmt.Errorf("restoring saved routes: %w", saveErr))
|
|
|
|
|
+ }
|
|
|
|
|
+ err = errors.Join(failures...)
|
|
|
}()
|
|
}()
|
|
|
|
|
|
|
|
if r.DstAddress == "" {
|
|
if r.DstAddress == "" {
|
|
@@ -640,7 +671,6 @@ func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
|
if _, _, parseErr := net.ParseCIDR(r.DstAddress); parseErr != nil && net.ParseIP(r.DstAddress) == nil {
|
|
if _, _, parseErr := net.ParseCIDR(r.DstAddress); parseErr != nil && net.ParseIP(r.DstAddress) == nil {
|
|
|
return fmt.Errorf("invalid destination: %s", r.DstAddress)
|
|
return fmt.Errorf("invalid destination: %s", r.DstAddress)
|
|
|
}
|
|
}
|
|
|
- dirty = true
|
|
|
|
|
r.Static = true
|
|
r.Static = true
|
|
|
|
|
|
|
|
origID := r.ID
|
|
origID := r.ID
|
|
@@ -668,7 +698,7 @@ func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
|
if exTbl == "" {
|
|
if exTbl == "" {
|
|
|
exTbl = "main"
|
|
exTbl = "main"
|
|
|
}
|
|
}
|
|
|
- if normalizeDst(existing.DstAddress) == normOldDst && exTbl == oldTable {
|
|
|
|
|
|
|
+ if normalizeDst(existing.DstAddress) == normOldDst && exTbl == oldTable && (r.OldDistance == nil || existing.Distance == *r.OldDistance) {
|
|
|
oldGwMatches := r.OldGateway == "" ||
|
|
oldGwMatches := r.OldGateway == "" ||
|
|
|
existing.Gateway == r.OldGateway ||
|
|
existing.Gateway == r.OldGateway ||
|
|
|
existing.Interface == r.OldGateway ||
|
|
existing.Interface == r.OldGateway ||
|
|
@@ -689,7 +719,7 @@ func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
|
if exTbl == "" {
|
|
if exTbl == "" {
|
|
|
exTbl = "main"
|
|
exTbl = "main"
|
|
|
}
|
|
}
|
|
|
- if normalizeDst(existing.DstAddress) == normNewDst && exTbl == r.RoutingTable {
|
|
|
|
|
|
|
+ if normalizeDst(existing.DstAddress) == normNewDst && exTbl == r.RoutingTable && existing.Distance == r.Distance {
|
|
|
gwMatches := (existing.Gateway != "" && existing.Gateway == r.Gateway) ||
|
|
gwMatches := (existing.Gateway != "" && existing.Gateway == r.Gateway) ||
|
|
|
(existing.Interface != "" && existing.Interface == r.Interface) ||
|
|
(existing.Interface != "" && existing.Interface == r.Interface) ||
|
|
|
(existing.Gateway == "" && r.Gateway == "")
|
|
(existing.Gateway == "" && r.Gateway == "")
|
|
@@ -713,7 +743,7 @@ func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
|
if exTbl == "" {
|
|
if exTbl == "" {
|
|
|
exTbl = "main"
|
|
exTbl = "main"
|
|
|
}
|
|
}
|
|
|
- if normalizeDst(existing.DstAddress) == normOldDst && exTbl == oldTable {
|
|
|
|
|
|
|
+ if normalizeDst(existing.DstAddress) == normOldDst && exTbl == oldTable && (r.OldDistance == nil || existing.Distance == *r.OldDistance) {
|
|
|
foundIndex = i
|
|
foundIndex = i
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
@@ -738,7 +768,6 @@ func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
|
r.HealthStatus = "none"
|
|
r.HealthStatus = "none"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- var oldRouteToClean *RouteItem
|
|
|
|
|
if foundIndex >= 0 {
|
|
if foundIndex >= 0 {
|
|
|
oldExisting := m.staticRoutes[foundIndex]
|
|
oldExisting := m.staticRoutes[foundIndex]
|
|
|
r.ID = oldExisting.ID // Preserve persistent ID!
|
|
r.ID = oldExisting.ID // Preserve persistent ID!
|
|
@@ -758,40 +787,32 @@ func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
|
r.HealthLossPct = oldExisting.HealthLossPct
|
|
r.HealthLossPct = oldExisting.HealthLossPct
|
|
|
r.HealthLastCheck = oldExisting.HealthLastCheck
|
|
r.HealthLastCheck = oldExisting.HealthLastCheck
|
|
|
}
|
|
}
|
|
|
|
|
+ dirty = true
|
|
|
m.staticRoutes[foundIndex] = *r
|
|
m.staticRoutes[foundIndex] = *r
|
|
|
} else {
|
|
} else {
|
|
|
if r.ID == "" || strings.HasPrefix(r.ID, "kr-") {
|
|
if r.ID == "" || strings.HasPrefix(r.ID, "kr-") {
|
|
|
r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
|
|
r.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
|
|
|
}
|
|
}
|
|
|
- // If adopting a live kernel route not in staticRoutes, remove it from direct kernel routes
|
|
|
|
|
|
|
+ // An unmanaged route may be adopted only when it is an identifiable,
|
|
|
|
|
+ // single static route. Preserve its actual kernel values for rollback.
|
|
|
if strings.HasPrefix(origID, "kr-") {
|
|
if strings.HasPrefix(origID, "kr-") {
|
|
|
for _, lr := range m.listLiveRoutesLocked() {
|
|
for _, lr := range m.listLiveRoutesLocked() {
|
|
|
if lr.ID == origID {
|
|
if lr.ID == origID {
|
|
|
|
|
+ if lr.Protocol != "static" || lr.IsMultiPath {
|
|
|
|
|
+ return fmt.Errorf("cannot edit a dynamic or multipath kernel route")
|
|
|
|
|
+ }
|
|
|
|
|
+ original := lr
|
|
|
|
|
+ unmanagedOriginal = &original
|
|
|
|
|
+ oldRouteToClean = &original
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- m.staticRoutes = append(m.staticRoutes, *r)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if r.OldDstAddress != "" {
|
|
|
|
|
- oldDist := 0
|
|
|
|
|
- if r.OldDistance != nil {
|
|
|
|
|
- oldDist = *r.OldDistance
|
|
|
|
|
- }
|
|
|
|
|
- if normalizeDst(r.OldDstAddress) != normalizeDst(r.DstAddress) ||
|
|
|
|
|
- r.OldRoutingTable != r.RoutingTable ||
|
|
|
|
|
- oldDist != r.Distance ||
|
|
|
|
|
- r.OldGateway != r.Gateway ||
|
|
|
|
|
- r.OldInterface != r.Interface {
|
|
|
|
|
- oldRouteToClean = &RouteItem{
|
|
|
|
|
- DstAddress: r.OldDstAddress,
|
|
|
|
|
- Gateway: r.OldGateway,
|
|
|
|
|
- RoutingTable: r.OldRoutingTable,
|
|
|
|
|
- Distance: oldDist,
|
|
|
|
|
- Interface: r.OldInterface,
|
|
|
|
|
|
|
+ if unmanagedOriginal == nil {
|
|
|
|
|
+ return fmt.Errorf("kernel route not found: %s", origID)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ dirty = true
|
|
|
|
|
+ m.staticRoutes = append(m.staticRoutes, *r)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
for i := range m.staticRoutes {
|
|
for i := range m.staticRoutes {
|
|
@@ -821,17 +842,18 @@ func (m *RoutingManager) SaveRoute(r *RouteItem) (err error) {
|
|
|
return saveErr
|
|
return saveErr
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Clean up old route group if destination, routing table, distance, gateway, or interface changed
|
|
|
|
|
- if oldRouteToClean != nil {
|
|
|
|
|
- _ = m.syncRouteGroupToKernel(oldRouteToClean.DstAddress, oldRouteToClean.RoutingTable, oldRouteToClean.Distance)
|
|
|
|
|
- _ = m.deleteDirectKernelRoute(*oldRouteToClean)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Apply updated/new route group to kernel
|
|
|
|
|
|
|
+ // Install the new group first to preserve connectivity when the destination
|
|
|
|
|
+ // changes. Reconcile the old group only if its key differs.
|
|
|
|
|
+ newAttempted = true
|
|
|
if err := m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance); err != nil {
|
|
if err := m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance); err != nil {
|
|
|
- return err
|
|
|
|
|
|
|
+ return fmt.Errorf("applying new route group: %w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if oldRouteToClean != nil && !sameRouteGroup(*oldRouteToClean, *r) && !oldRouteToClean.Disabled {
|
|
|
|
|
+ oldAttempted = true
|
|
|
|
|
+ if err := m.syncRouteGroupToKernel(oldRouteToClean.DstAddress, oldRouteToClean.RoutingTable, oldRouteToClean.Distance); err != nil {
|
|
|
|
|
+ return fmt.Errorf("cleaning previous route group: %w", err)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- applied = true
|
|
|
|
|
|
|
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
@@ -864,7 +886,7 @@ func (m *RoutingManager) DeleteRoute(id string) error {
|
|
|
normLrDst := normalizeDst(lr.DstAddress)
|
|
normLrDst := normalizeDst(lr.DstAddress)
|
|
|
for i, sr := range m.staticRoutes {
|
|
for i, sr := range m.staticRoutes {
|
|
|
gwMatches := sr.Gateway == "" || lr.Gateway == sr.Gateway || lr.Interface == sr.Gateway || (sr.Interface != "" && lr.Interface == sr.Interface)
|
|
gwMatches := sr.Gateway == "" || lr.Gateway == sr.Gateway || lr.Interface == sr.Gateway || (sr.Interface != "" && lr.Interface == sr.Interface)
|
|
|
- if normalizeDst(sr.DstAddress) == normLrDst && sr.RoutingTable == lr.RoutingTable && gwMatches {
|
|
|
|
|
|
|
+ if normalizeDst(sr.DstAddress) == normLrDst && sameRouteGroup(sr, lr) && gwMatches {
|
|
|
matchIdx = i
|
|
matchIdx = i
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
@@ -877,10 +899,13 @@ func (m *RoutingManager) DeleteRoute(id string) error {
|
|
|
updated = append(updated, r)
|
|
updated = append(updated, r)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- _ = m.deleteDirectKernelRoute(lr)
|
|
|
|
|
break
|
|
break
|
|
|
} else {
|
|
} else {
|
|
|
- // Pure kernel route, not managed in static_routes.json
|
|
|
|
|
|
|
+ // Do not remove routes belonging to other routing daemons or a
|
|
|
|
|
+ // single nexthop of a multipath route.
|
|
|
|
|
+ if lr.Protocol != "static" || lr.IsMultiPath {
|
|
|
|
|
+ return fmt.Errorf("cannot delete a dynamic or multipath kernel route")
|
|
|
|
|
+ }
|
|
|
return m.deleteDirectKernelRoute(lr)
|
|
return m.deleteDirectKernelRoute(lr)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -940,7 +965,7 @@ func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
|
|
|
normLrDst := normalizeDst(lr.DstAddress)
|
|
normLrDst := normalizeDst(lr.DstAddress)
|
|
|
for i := range m.staticRoutes {
|
|
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)
|
|
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 {
|
|
|
|
|
|
|
+ if normalizeDst(m.staticRoutes[i].DstAddress) == normLrDst && sameRouteGroup(m.staticRoutes[i], lr) && gwMatches {
|
|
|
before := append([]RouteItem(nil), m.staticRoutes...)
|
|
before := append([]RouteItem(nil), m.staticRoutes...)
|
|
|
m.staticRoutes[i].Disabled = disabled
|
|
m.staticRoutes[i].Disabled = disabled
|
|
|
if !disabled && m.staticRoutes[i].HealthStatus != "offline" {
|
|
if !disabled && m.staticRoutes[i].HealthStatus != "offline" {
|
|
@@ -954,6 +979,9 @@ func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
|
|
|
|
|
|
|
|
// If pure kernel static route, adopt into staticRoutes as disabled
|
|
// If pure kernel static route, adopt into staticRoutes as disabled
|
|
|
if disabled {
|
|
if disabled {
|
|
|
|
|
+ if lr.Protocol != "static" || lr.IsMultiPath {
|
|
|
|
|
+ return fmt.Errorf("cannot disable a dynamic or multipath kernel route")
|
|
|
|
|
+ }
|
|
|
lr.Disabled = true
|
|
lr.Disabled = true
|
|
|
lr.Static = true
|
|
lr.Static = true
|
|
|
lr.Active = false
|
|
lr.Active = false
|