|
|
@@ -71,6 +71,11 @@ func (m *RoutingManager) load() {
|
|
|
if data, err := os.ReadFile(CustomTablesStorePath); err == nil {
|
|
|
_ = json.Unmarshal(data, &m.customTables)
|
|
|
}
|
|
|
+ for i := range m.staticRoutes {
|
|
|
+ if !m.staticRoutes[i].Disabled && m.staticRoutes[i].HealthStatus != "offline" {
|
|
|
+ m.staticRoutes[i].Active = true
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
func (m *RoutingManager) save() error {
|
|
|
@@ -106,10 +111,23 @@ func (m *RoutingManager) syncCustomTablesToSystem() {
|
|
|
}
|
|
|
|
|
|
func (m *RoutingManager) applyAll() {
|
|
|
- // Apply all static routes
|
|
|
+ // Apply all static routes grouped by (dst, table, distance) for ECMP
|
|
|
+ type groupKey struct {
|
|
|
+ dst string
|
|
|
+ table string
|
|
|
+ dist int
|
|
|
+ }
|
|
|
+ seen := make(map[groupKey]bool)
|
|
|
for _, r := range m.staticRoutes {
|
|
|
- if !r.Disabled {
|
|
|
- _ = m.applyRouteToKernel(r)
|
|
|
+ normDst := normalizeDst(r.DstAddress)
|
|
|
+ tbl := strings.TrimSpace(r.RoutingTable)
|
|
|
+ if tbl == "" {
|
|
|
+ tbl = "main"
|
|
|
+ }
|
|
|
+ k := groupKey{dst: normDst, table: tbl, dist: r.Distance}
|
|
|
+ if !seen[k] {
|
|
|
+ seen[k] = true
|
|
|
+ _ = m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -165,10 +183,47 @@ func (m *RoutingManager) listLiveRoutesLocked() []RouteItem {
|
|
|
tableName = strconv.Itoa(r.Table)
|
|
|
}
|
|
|
|
|
|
- ifName := linkMap[r.LinkIndex]
|
|
|
isDirect := r.Scope == netlink.SCOPE_LINK || r.Protocol == netlink.RouteProtocol(2)
|
|
|
isDyn := isDynamicProtocol(r.Protocol)
|
|
|
isStatic := !isDirect && !isDyn
|
|
|
+ isMulticast := r.Type == 5 || strings.HasPrefix(dst, "224.") || strings.HasPrefix(dst, "ff") || dst == "224.0.0.0/4"
|
|
|
+
|
|
|
+ if len(r.MultiPath) > 0 {
|
|
|
+ for _, nh := range r.MultiPath {
|
|
|
+ nhGw := ""
|
|
|
+ if nh.Gw != nil {
|
|
|
+ nhGw = nh.Gw.String()
|
|
|
+ }
|
|
|
+ nhIfName := linkMap[nh.LinkIndex]
|
|
|
+ cleanDst := strings.ReplaceAll(dst, "/", "_")
|
|
|
+ cleanGw := nhGw
|
|
|
+ if cleanGw == "" {
|
|
|
+ cleanGw = nhIfName
|
|
|
+ }
|
|
|
+ routeID := fmt.Sprintf("kr-%d-%s-%s", r.Table, cleanDst, cleanGw)
|
|
|
+
|
|
|
+ liveRoutes = append(liveRoutes, RouteItem{
|
|
|
+ ID: routeID,
|
|
|
+ DstAddress: dst,
|
|
|
+ Gateway: nhGw,
|
|
|
+ Interface: nhIfName,
|
|
|
+ Distance: r.Priority,
|
|
|
+ RoutingTable: tableName,
|
|
|
+ TableID: r.Table,
|
|
|
+ Active: true,
|
|
|
+ Connect: isDirect,
|
|
|
+ Dynamic: isDyn,
|
|
|
+ Static: isStatic,
|
|
|
+ ECMP: true,
|
|
|
+ IsMultiPath: true,
|
|
|
+ Multicast: isMulticast,
|
|
|
+ Disabled: false,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ ifName := linkMap[r.LinkIndex]
|
|
|
|
|
|
cleanDst := strings.ReplaceAll(dst, "/", "_")
|
|
|
cleanGw := gw
|
|
|
@@ -189,6 +244,7 @@ func (m *RoutingManager) listLiveRoutesLocked() []RouteItem {
|
|
|
Connect: isDirect,
|
|
|
Dynamic: isDyn,
|
|
|
Static: isStatic,
|
|
|
+ Multicast: isMulticast,
|
|
|
Disabled: false,
|
|
|
})
|
|
|
}
|
|
|
@@ -237,6 +293,25 @@ func (m *RoutingManager) ListRoutes() []RouteItem {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Detect Equal-Cost Multi-Path (ECMP) routes
|
|
|
+ groupCounts := make(map[string]int)
|
|
|
+ for _, r := range liveRoutes {
|
|
|
+ if r.Active && !r.Disabled {
|
|
|
+ key := fmt.Sprintf("%s|%s|%d", r.DstAddress, r.RoutingTable, r.Distance)
|
|
|
+ groupCounts[key]++
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for i := range liveRoutes {
|
|
|
+ key := fmt.Sprintf("%s|%s|%d", liveRoutes[i].DstAddress, liveRoutes[i].RoutingTable, liveRoutes[i].Distance)
|
|
|
+ if liveRoutes[i].IsMultiPath || (liveRoutes[i].Active && !liveRoutes[i].Disabled && groupCounts[key] > 1) {
|
|
|
+ liveRoutes[i].ECMP = true
|
|
|
+ }
|
|
|
+ if liveRoutes[i].DstAddress == "224.0.0.0/4" || strings.HasPrefix(liveRoutes[i].DstAddress, "224.") || strings.HasPrefix(liveRoutes[i].DstAddress, "ff") {
|
|
|
+ liveRoutes[i].Multicast = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
sort.Slice(liveRoutes, func(i, j int) bool {
|
|
|
if liveRoutes[i].RoutingTable != liveRoutes[j].RoutingTable {
|
|
|
return liveRoutes[i].RoutingTable < liveRoutes[j].RoutingTable
|
|
|
@@ -336,13 +411,27 @@ func (m *RoutingManager) SaveRoute(r RouteItem) error {
|
|
|
m.staticRoutes = append(m.staticRoutes, r)
|
|
|
}
|
|
|
|
|
|
+ for i := range m.staticRoutes {
|
|
|
+ if m.staticRoutes[i].ID == r.ID {
|
|
|
+ if !m.staticRoutes[i].Disabled && m.staticRoutes[i].HealthStatus != "offline" {
|
|
|
+ m.staticRoutes[i].Active = true
|
|
|
+ } else {
|
|
|
+ m.staticRoutes[i].Active = false
|
|
|
+ }
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
_ = m.save()
|
|
|
|
|
|
- if r.Disabled {
|
|
|
- _ = m.removeRouteFromKernel(r)
|
|
|
- } else {
|
|
|
- if err := m.applyRouteToKernel(r); err != nil {
|
|
|
- return err
|
|
|
+ _ = m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
|
|
|
+ if r.OldDstAddress != "" {
|
|
|
+ oldDist := 0
|
|
|
+ if r.OldDistance != nil {
|
|
|
+ oldDist = *r.OldDistance
|
|
|
+ }
|
|
|
+ if r.OldDstAddress != r.DstAddress || r.OldRoutingTable != r.RoutingTable || oldDist != r.Distance {
|
|
|
+ _ = m.syncRouteGroupToKernel(r.OldDstAddress, r.OldRoutingTable, oldDist)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -364,9 +453,10 @@ func (m *RoutingManager) DeleteRoute(id string) error {
|
|
|
}
|
|
|
|
|
|
if target != nil {
|
|
|
- _ = m.removeRouteFromKernel(*target)
|
|
|
m.staticRoutes = updated
|
|
|
- return m.save()
|
|
|
+ _ = m.save()
|
|
|
+ _ = m.syncRouteGroupToKernel(target.DstAddress, target.RoutingTable, target.Distance)
|
|
|
+ return nil
|
|
|
}
|
|
|
|
|
|
// If not in staticRoutes, could be a live route (e.g. kr-...)
|
|
|
@@ -387,12 +477,13 @@ func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
|
|
|
for i := range m.staticRoutes {
|
|
|
if m.staticRoutes[i].ID == id {
|
|
|
m.staticRoutes[i].Disabled = disabled
|
|
|
- _ = m.save()
|
|
|
- if disabled {
|
|
|
- _ = m.removeRouteFromKernel(m.staticRoutes[i])
|
|
|
- } else {
|
|
|
- _ = m.applyRouteToKernel(m.staticRoutes[i])
|
|
|
+ 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
|
|
|
}
|
|
|
}
|
|
|
@@ -405,9 +496,10 @@ func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
|
|
|
lr.Static = true
|
|
|
lr.Active = false
|
|
|
lr.ID = fmt.Sprintf("route-%d", time.Now().UnixNano())
|
|
|
- _ = m.removeRouteFromKernel(lr)
|
|
|
m.staticRoutes = append(m.staticRoutes, lr)
|
|
|
- return m.save()
|
|
|
+ _ = m.save()
|
|
|
+ _ = m.syncRouteGroupToKernel(lr.DstAddress, lr.RoutingTable, lr.Distance)
|
|
|
+ return nil
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -415,85 +507,126 @@ func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
|
|
|
return fmt.Errorf("route not found")
|
|
|
}
|
|
|
|
|
|
-func (m *RoutingManager) applyRouteToKernel(r RouteItem) error {
|
|
|
- dst := strings.TrimSpace(r.DstAddress)
|
|
|
- if dst == "default" || dst == "0.0.0.0/0" {
|
|
|
- dst = "default"
|
|
|
- } else if !strings.Contains(dst, "/") && net.ParseIP(dst) != nil {
|
|
|
- dst = dst + "/32"
|
|
|
+func normalizeDst(dst string) string {
|
|
|
+ dst = strings.TrimSpace(dst)
|
|
|
+ if dst == "" || dst == "default" || dst == "0.0.0.0/0" {
|
|
|
+ return "default"
|
|
|
}
|
|
|
-
|
|
|
- args := []string{"route", "replace", dst}
|
|
|
-
|
|
|
- gw := strings.TrimSpace(r.Gateway)
|
|
|
- if gw != "" {
|
|
|
- if net.ParseIP(gw) != nil {
|
|
|
- args = append(args, "via", gw)
|
|
|
- } else {
|
|
|
- args = append(args, "dev", gw)
|
|
|
- }
|
|
|
+ if !strings.Contains(dst, "/") && net.ParseIP(dst) != nil {
|
|
|
+ return dst + "/32"
|
|
|
}
|
|
|
+ return dst
|
|
|
+}
|
|
|
|
|
|
- iface := strings.TrimSpace(r.Interface)
|
|
|
- if iface != "" && gw != iface && (gw == "" || net.ParseIP(gw) != nil) {
|
|
|
- args = append(args, "dev", iface)
|
|
|
+func (m *RoutingManager) syncRouteGroupToKernel(dstAddress, routingTable string, distance int) error {
|
|
|
+ normDst := normalizeDst(dstAddress)
|
|
|
+ tbl := strings.TrimSpace(routingTable)
|
|
|
+ if tbl == "" {
|
|
|
+ tbl = "main"
|
|
|
}
|
|
|
|
|
|
- if r.Distance > 0 {
|
|
|
- args = append(args, "metric", strconv.Itoa(r.Distance))
|
|
|
+ var activeRoutes []RouteItem
|
|
|
+ for _, r := range m.staticRoutes {
|
|
|
+ rDst := normalizeDst(r.DstAddress)
|
|
|
+ rTbl := strings.TrimSpace(r.RoutingTable)
|
|
|
+ if rTbl == "" {
|
|
|
+ rTbl = "main"
|
|
|
+ }
|
|
|
+ if rDst == normDst && rTbl == tbl && r.Distance == distance {
|
|
|
+ if !r.Disabled && r.Active && r.HealthStatus != "offline" {
|
|
|
+ activeRoutes = append(activeRoutes, r)
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- if r.RoutingTable != "" && r.RoutingTable != "main" {
|
|
|
- args = append(args, "table", r.RoutingTable)
|
|
|
+ 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()
|
|
|
+
|
|
|
+ fallbackArgs := []string{"route", "del", normDst}
|
|
|
+ if tbl != "main" {
|
|
|
+ fallbackArgs = append(fallbackArgs, "table", tbl)
|
|
|
+ }
|
|
|
+ _ = exec.Command("ip", fallbackArgs...).Run()
|
|
|
+ return nil
|
|
|
}
|
|
|
|
|
|
- args = append(args, "proto", "static")
|
|
|
+ if len(activeRoutes) == 1 {
|
|
|
+ r := activeRoutes[0]
|
|
|
+ args := []string{"route", "replace", normDst}
|
|
|
+ gw := strings.TrimSpace(r.Gateway)
|
|
|
+ if gw != "" {
|
|
|
+ if net.ParseIP(gw) != nil {
|
|
|
+ args = append(args, "via", gw)
|
|
|
+ } else {
|
|
|
+ args = append(args, "dev", gw)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ iface := strings.TrimSpace(r.Interface)
|
|
|
+ if iface != "" && gw != iface && (gw == "" || net.ParseIP(gw) != nil) {
|
|
|
+ args = append(args, "dev", iface)
|
|
|
+ }
|
|
|
+ if distance > 0 {
|
|
|
+ args = append(args, "metric", strconv.Itoa(distance))
|
|
|
+ }
|
|
|
+ if tbl != "main" {
|
|
|
+ args = append(args, "table", tbl)
|
|
|
+ }
|
|
|
+ args = append(args, "proto", "static")
|
|
|
|
|
|
- cmd := exec.Command("ip", args...)
|
|
|
- out, err := cmd.CombinedOutput()
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("failed to apply route (%s): %s: %w", strings.Join(args, " "), string(out), err)
|
|
|
+ cmd := exec.Command("ip", args...)
|
|
|
+ if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
+ return fmt.Errorf("failed to apply route (%s): %s: %w", strings.Join(args, " "), string(out), err)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
}
|
|
|
- return nil
|
|
|
-}
|
|
|
|
|
|
-func (m *RoutingManager) removeRouteFromKernel(r RouteItem) error {
|
|
|
- dst := strings.TrimSpace(r.DstAddress)
|
|
|
- if dst == "default" || dst == "0.0.0.0/0" {
|
|
|
- dst = "default"
|
|
|
- } else if !strings.Contains(dst, "/") && net.ParseIP(dst) != nil {
|
|
|
- dst = dst + "/32"
|
|
|
+ // Multiple routes: ECMP multipath route
|
|
|
+ args := []string{"route", "replace", normDst, "scope", "global"}
|
|
|
+ if distance > 0 {
|
|
|
+ args = append(args, "metric", strconv.Itoa(distance))
|
|
|
}
|
|
|
+ if tbl != "main" {
|
|
|
+ args = append(args, "table", tbl)
|
|
|
+ }
|
|
|
+ args = append(args, "proto", "static")
|
|
|
|
|
|
- args := []string{"route", "del", dst}
|
|
|
-
|
|
|
- gw := strings.TrimSpace(r.Gateway)
|
|
|
- if gw != "" {
|
|
|
- if net.ParseIP(gw) != nil {
|
|
|
+ for _, r := range activeRoutes {
|
|
|
+ args = append(args, "nexthop")
|
|
|
+ gw := strings.TrimSpace(r.Gateway)
|
|
|
+ if gw != "" && net.ParseIP(gw) != nil {
|
|
|
args = append(args, "via", gw)
|
|
|
- } else {
|
|
|
- args = append(args, "dev", gw)
|
|
|
}
|
|
|
+ iface := strings.TrimSpace(r.Interface)
|
|
|
+ if iface == "" && gw != "" && net.ParseIP(gw) == nil {
|
|
|
+ iface = gw
|
|
|
+ }
|
|
|
+ if iface != "" {
|
|
|
+ args = append(args, "dev", iface)
|
|
|
+ }
|
|
|
+ args = append(args, "weight", "1")
|
|
|
}
|
|
|
|
|
|
- if r.Distance > 0 {
|
|
|
- args = append(args, "metric", strconv.Itoa(r.Distance))
|
|
|
- }
|
|
|
-
|
|
|
- if r.RoutingTable != "" && r.RoutingTable != "main" {
|
|
|
- args = append(args, "table", r.RoutingTable)
|
|
|
+ cmd := exec.Command("ip", args...)
|
|
|
+ if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
+ return fmt.Errorf("failed to apply multipath route (%s): %s: %w", strings.Join(args, " "), string(out), err)
|
|
|
}
|
|
|
+ return nil
|
|
|
+}
|
|
|
|
|
|
- _ = exec.Command("ip", args...).Run()
|
|
|
-
|
|
|
- // Fallback deletion without metric
|
|
|
- fallbackArgs := []string{"route", "del", dst}
|
|
|
- if r.RoutingTable != "" && r.RoutingTable != "main" {
|
|
|
- fallbackArgs = append(fallbackArgs, "table", r.RoutingTable)
|
|
|
- }
|
|
|
- _ = exec.Command("ip", fallbackArgs...).Run()
|
|
|
+func (m *RoutingManager) applyRouteToKernel(r RouteItem) error {
|
|
|
+ return m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
|
|
|
+}
|
|
|
|
|
|
- return nil
|
|
|
+func (m *RoutingManager) removeRouteFromKernel(r RouteItem) error {
|
|
|
+ return m.syncRouteGroupToKernel(r.DstAddress, r.RoutingTable, r.Distance)
|
|
|
}
|
|
|
|
|
|
// ==================== Policy Rules (/ip route rule) ====================
|
|
|
@@ -759,3 +892,24 @@ func (m *RoutingManager) Stop() {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// RenameInterface updates static routes and policy rules when an interface is renamed.
|
|
|
+func (m *RoutingManager) RenameInterface(oldName, newName string) {
|
|
|
+ m.mu.Lock()
|
|
|
+ defer m.mu.Unlock()
|
|
|
+
|
|
|
+ changed := false
|
|
|
+ for i := range m.staticRoutes {
|
|
|
+ if m.staticRoutes[i].Interface == oldName {
|
|
|
+ m.staticRoutes[i].Interface = newName
|
|
|
+ changed = true
|
|
|
+ }
|
|
|
+ if m.staticRoutes[i].Gateway == oldName {
|
|
|
+ m.staticRoutes[i].Gateway = newName
|
|
|
+ changed = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if changed {
|
|
|
+ _ = m.save()
|
|
|
+ }
|
|
|
+}
|