Преглед изворни кода

fix(routing): prevent PERVASIVE and ONLINK flag conflict on device-only routes & ECMP nexthops (v1.8.22)

Proxmox Router пре 12 часа
родитељ
комит
b9275207b7
7 измењених фајлова са 87 додато и 3 уклоњено
  1. 1 1
      VERSION
  2. BIN
      bin/debtik
  3. BIN
      bin/debtik-arm64
  4. BIN
      bin/debtik-cli
  5. BIN
      bin/debtik-cli-arm64
  6. 42 2
      internal/routing/manager.go
  7. 44 0
      internal/routing/manager_test.go

+ 1 - 1
VERSION

@@ -1 +1 @@
-1.8.21
+1.8.22




BIN
bin/debtik-cli-arm64


+ 42 - 2
internal/routing/manager.go

@@ -928,6 +928,15 @@ func (m *RoutingManager) SetRouteState(id string, disabled bool) error {
 	return fmt.Errorf("route not found")
 	return fmt.Errorf("route not found")
 }
 }
 
 
+func sliceContains(list []string, item string) bool {
+	for _, s := range list {
+		if s == item {
+			return true
+		}
+	}
+	return false
+}
+
 func normalizeDst(dst string) string {
 func normalizeDst(dst string) string {
 	dst = strings.TrimSpace(dst)
 	dst = strings.TrimSpace(dst)
 	if dst == "" || dst == "default" || dst == "0.0.0.0/0" {
 	if dst == "" || dst == "default" || dst == "0.0.0.0/0" {
@@ -1066,9 +1075,11 @@ func (m *RoutingManager) syncRouteGroupToKernel(dstAddress, routingTable string,
 		} else {
 		} else {
 			args = []string{"route", "replace", normDst}
 			args = []string{"route", "replace", normDst}
 			iface := strings.TrimSpace(r.Interface)
 			iface := strings.TrimSpace(r.Interface)
+			hasVia := false
 			if gw != "" {
 			if gw != "" {
 				if net.ParseIP(gw) != nil {
 				if net.ParseIP(gw) != nil {
 					args = append(args, "via", gw)
 					args = append(args, "via", gw)
+					hasVia = true
 				} else {
 				} else {
 					// Gateway field contains an interface name
 					// Gateway field contains an interface name
 					targetDev := gw
 					targetDev := gw
@@ -1077,6 +1088,7 @@ func (m *RoutingManager) syncRouteGroupToKernel(dstAddress, routingTable string,
 					}
 					}
 					if detectedGw := detectGatewayForInterface(targetDev); detectedGw != "" {
 					if detectedGw := detectGatewayForInterface(targetDev); detectedGw != "" {
 						args = append(args, "via", detectedGw)
 						args = append(args, "via", detectedGw)
+						hasVia = true
 					} else {
 					} else {
 						args = append(args, "dev", targetDev)
 						args = append(args, "dev", targetDev)
 					}
 					}
@@ -1085,7 +1097,7 @@ func (m *RoutingManager) syncRouteGroupToKernel(dstAddress, routingTable string,
 			if iface != "" && (gw == "" || net.ParseIP(gw) != nil || (gw != "" && detectGatewayForInterface(gw) != "")) {
 			if iface != "" && (gw == "" || net.ParseIP(gw) != nil || (gw != "" && detectGatewayForInterface(gw) != "")) {
 				args = append(args, "dev", iface)
 				args = append(args, "dev", iface)
 			}
 			}
-			if iface != "" && (gw != "" || detectGatewayForInterface(iface) != "") {
+			if hasVia && iface != "" {
 				args = append(args, "onlink")
 				args = append(args, "onlink")
 			}
 			}
 		}
 		}
@@ -1099,6 +1111,17 @@ func (m *RoutingManager) syncRouteGroupToKernel(dstAddress, routingTable string,
 
 
 		cmd := exec.Command("ip", args...)
 		cmd := exec.Command("ip", args...)
 		if out, err := cmd.CombinedOutput(); err != nil {
 		if out, err := cmd.CombinedOutput(); err != nil {
+			if sliceContains(args, "onlink") {
+				var noOnlinkArgs []string
+				for _, a := range args {
+					if a != "onlink" {
+						noOnlinkArgs = append(noOnlinkArgs, a)
+					}
+				}
+				if _, fbErr := exec.Command("ip", noOnlinkArgs...).CombinedOutput(); fbErr == nil {
+					return nil
+				}
+			}
 			return fmt.Errorf("failed to apply route (%s): %s: %w", strings.Join(args, " "), string(out), err)
 			return fmt.Errorf("failed to apply route (%s): %s: %w", strings.Join(args, " "), string(out), err)
 		}
 		}
 		return nil
 		return nil
@@ -1121,21 +1144,38 @@ func (m *RoutingManager) syncRouteGroupToKernel(dstAddress, routingTable string,
 		if iface == "" && gw != "" && net.ParseIP(gw) == nil {
 		if iface == "" && gw != "" && net.ParseIP(gw) == nil {
 			iface = gw
 			iface = gw
 		}
 		}
+		hasVia := false
 		if gw != "" && net.ParseIP(gw) != nil {
 		if gw != "" && net.ParseIP(gw) != nil {
 			args = append(args, "via", gw)
 			args = append(args, "via", gw)
+			hasVia = true
 		} else if iface != "" {
 		} else if iface != "" {
 			if detectedGw := detectGatewayForInterface(iface); detectedGw != "" {
 			if detectedGw := detectGatewayForInterface(iface); detectedGw != "" {
 				args = append(args, "via", detectedGw)
 				args = append(args, "via", detectedGw)
+				hasVia = true
 			}
 			}
 		}
 		}
 		if iface != "" {
 		if iface != "" {
-			args = append(args, "dev", iface, "onlink")
+			args = append(args, "dev", iface)
+		}
+		if hasVia && iface != "" {
+			args = append(args, "onlink")
 		}
 		}
 		args = append(args, "weight", "1")
 		args = append(args, "weight", "1")
 	}
 	}
 
 
 	cmd := exec.Command("ip", args...)
 	cmd := exec.Command("ip", args...)
 	if out, err := cmd.CombinedOutput(); err != nil {
 	if out, err := cmd.CombinedOutput(); err != nil {
+		if sliceContains(args, "onlink") {
+			var noOnlinkArgs []string
+			for _, a := range args {
+				if a != "onlink" {
+					noOnlinkArgs = append(noOnlinkArgs, a)
+				}
+			}
+			if _, fbErr := exec.Command("ip", noOnlinkArgs...).CombinedOutput(); fbErr == nil {
+				return nil
+			}
+		}
 		return fmt.Errorf("failed to apply multipath route (%s): %s: %w", strings.Join(args, " "), string(out), err)
 		return fmt.Errorf("failed to apply multipath route (%s): %s: %w", strings.Join(args, " "), string(out), err)
 	}
 	}
 	return nil
 	return nil

+ 44 - 0
internal/routing/manager_test.go

@@ -344,3 +344,47 @@ func TestSaveAndDeleteRouteLifecycle(t *testing.T) {
 		t.Fatalf("expected 0 static routes after delete, got %d", len(mgr.staticRoutes))
 		t.Fatalf("expected 0 static routes after delete, got %d", len(mgr.staticRoutes))
 	}
 	}
 }
 }
+
+func TestMultipathMixedNexthopFlags(t *testing.T) {
+	tmpDir := t.TempDir()
+	origStore := StaticRoutesStorePath
+	defer func() { StaticRoutesStorePath = origStore }()
+	StaticRoutesStorePath = filepath.Join(tmpDir, "static_routes.json")
+
+	mgr := &RoutingManager{
+		staticRoutes: make([]RouteItem, 0),
+	}
+
+	// Route 1: via IP with device (hasVia = true)
+	r1 := RouteItem{
+		DstAddress:   "198.51.100.0/24",
+		Gateway:      "100.68.0.1",
+		Interface:    "Fibergrid",
+		RoutingTable: "main",
+		Distance:     10,
+	}
+	if err := mgr.SaveRoute(&r1); err != nil {
+		t.Fatalf("failed to save route 1: %v", err)
+	}
+
+	// Route 2: device only without gateway IP (hasVia = false, e.g. sdwan0-Internet)
+	r2 := RouteItem{
+		DstAddress:   "198.51.100.0/24",
+		Gateway:      "sdwan0-Internet",
+		Interface:    "sdwan0-Internet",
+		RoutingTable: "main",
+		Distance:     10,
+	}
+	// This previously threw "Error: Invalid flags for nexthop - PERVASIVE and ONLINK can not be set."
+	if err := mgr.SaveRoute(&r2); err != nil {
+		t.Fatalf("failed to save multipath route 2: %v", err)
+	}
+
+	if len(mgr.staticRoutes) != 2 {
+		t.Fatalf("expected 2 routes in multipath group, got %d", len(mgr.staticRoutes))
+	}
+
+	// Cleanup
+	_ = mgr.DeleteRoute(r1.ID)
+	_ = mgr.DeleteRoute(r2.ID)
+}