Explorar el Código

fix(sdwan): bypass WAN inbound return traffic, add local subnet rule pref and preserve members (v1.8.3)

Proxmox Router hace 2 días
padre
commit
445e1a0fc0
Se han modificado 4 ficheros con 75 adiciones y 12 borrados
  1. 1 1
      VERSION
  2. 52 4
      internal/sdwan/engine.go
  3. 19 6
      internal/sdwan/manager.go
  4. 3 1
      internal/web/static/app.js

+ 1 - 1
VERSION

@@ -1 +1 @@
-1.8.2
+1.8.3

+ 52 - 4
internal/sdwan/engine.go

@@ -71,12 +71,26 @@ func (e *KernelEngine) Sync(groups []*SDWANGroup) error {
 	e.ensureMangleJump("PREROUTING")
 	e.ensureMangleJump("OUTPUT")
 
-	// Bypass private RFC1918, loopback, link-local, and multicast
+	// 1. Bypass inbound traffic from all member WAN interfaces so return traffic is never classified/marked
+	seenInIfaces := make(map[string]bool)
+	for _, g := range groups {
+		for _, m := range g.Members {
+			if m.Interface != "" && !seenInIfaces[m.Interface] {
+				seenInIfaces[m.Interface] = true
+				_ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-i", m.Interface, "-j", "RETURN").Run()
+			}
+		}
+	}
+
+	// 2. Bypass private RFC1918, loopback, link-local, and multicast
 	privateNets := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "169.254.0.0/16", "224.0.0.0/4"}
 	for _, net := range privateNets {
 		_ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-d", net, "-j", "RETURN").Run()
 	}
 
+	// 3. Ensure high-priority policy routing rules for private subnets so return traffic and LAN traffic always lookup main table
+	e.ensurePrivateRoutingRules()
+
 	// Restore connection mark: if a flow has already been classified, keep its mark (Session Stickiness!)
 	_ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-j", "CONNMARK", "--restore-mark").Run()
 	_ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-m", "mark", "!", "--mark", "0", "-j", "RETURN").Run()
@@ -168,9 +182,14 @@ func (e *KernelEngine) applyMemberRoute(m *SDWANMember) {
 	// Flush table first
 	_ = exec.Command("ip", "route", "flush", "table", tableStr).Run()
 
+	gw := m.Gateway
+	if gw == "" {
+		gw = e.detectInterfaceGateway(m.Interface)
+	}
+
 	args := []string{"route", "replace", "default"}
-	if m.Gateway != "" {
-		args = append(args, "via", m.Gateway)
+	if gw != "" {
+		args = append(args, "via", gw)
 	}
 	args = append(args, "dev", m.Interface, "table", tableStr)
 
@@ -179,9 +198,26 @@ func (e *KernelEngine) applyMemberRoute(m *SDWANMember) {
 	}
 }
 
+func (e *KernelEngine) detectInterfaceGateway(ifaceName string) string {
+	link, err := netlink.LinkByName(ifaceName)
+	if err != nil {
+		return ""
+	}
+	routes, err := netlink.RouteList(link, netlink.FAMILY_V4)
+	if err != nil {
+		return ""
+	}
+	for _, r := range routes {
+		if r.Gw != nil && !r.Gw.IsUnspecified() {
+			return r.Gw.String()
+		}
+	}
+	return ""
+}
+
 func (e *KernelEngine) applyMemberRule(m *SDWANMember) {
 	tableStr := strconv.Itoa(m.TableID)
-	prefStr := strconv.Itoa(2000 + m.TableID%1000)
+	prefStr := strconv.Itoa(2100 + m.TableID%1000)
 
 	// Delete matching rule if already present
 	_ = exec.Command("ip", "rule", "del", "fwmark", m.MarkHex, "table", tableStr).Run()
@@ -354,7 +390,19 @@ func (e *KernelEngine) cleanupMangle() {
 	_ = exec.Command("iptables", "-t", "mangle", "-X", "DEBTIK_SDWAN").Run()
 }
 
+func (e *KernelEngine) ensurePrivateRoutingRules() {
+	privateNets := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
+	for _, net := range privateNets {
+		_ = exec.Command("ip", "rule", "del", "to", net, "table", "main", "pref", "2000").Run()
+		_ = exec.Command("ip", "rule", "add", "to", net, "table", "main", "pref", "2000").Run()
+	}
+}
+
 func (e *KernelEngine) cleanupIPRules() {
+	privateNets := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
+	for _, net := range privateNets {
+		_ = exec.Command("ip", "rule", "del", "to", net, "table", "main", "pref", "2000").Run()
+	}
 	out, err := exec.Command("ip", "rule", "show").CombinedOutput()
 	if err != nil {
 		return

+ 19 - 6
internal/sdwan/manager.go

@@ -167,6 +167,13 @@ func (m *Manager) Save(g *SDWANGroup) error {
 		}
 	}
 
+	// Preserve existing members if not specified in update payload
+	if existing, ok := m.groups[g.ID]; ok {
+		if g.Members == nil && existing.Members != nil {
+			g.Members = existing.Members
+		}
+	}
+
 	// Ensure member IDs and initial statuses
 	for _, mem := range g.Members {
 		if mem.ID == "" {
@@ -188,7 +195,13 @@ func (m *Manager) Save(g *SDWANGroup) error {
 		return err
 	}
 
-	return m.engine.Sync(m.listLocked())
+	return m.syncLocked()
+}
+
+func (m *Manager) syncLocked() error {
+	err := m.engine.Sync(m.listLocked())
+	_ = m.save()
+	return err
 }
 
 // SetState enables or disables an SD-WAN group.
@@ -206,7 +219,7 @@ func (m *Manager) SetState(idOrName string, disabled bool) error {
 		return err
 	}
 
-	return m.engine.Sync(m.listLocked())
+	return m.syncLocked()
 }
 
 // Delete removes an SD-WAN group.
@@ -224,7 +237,7 @@ func (m *Manager) Delete(idOrName string) error {
 		return err
 	}
 
-	return m.engine.Sync(m.listLocked())
+	return m.syncLocked()
 }
 
 // AddMember adds an interface member to a group.
@@ -267,7 +280,7 @@ func (m *Manager) AddMember(groupIDOrName string, mem *SDWANMember) error {
 		return err
 	}
 
-	return m.engine.Sync(m.listLocked())
+	return m.syncLocked()
 }
 
 // UpdateMember modifies a member's configuration.
@@ -304,7 +317,7 @@ func (m *Manager) UpdateMember(groupIDOrName string, mem *SDWANMember) error {
 		return err
 	}
 
-	return m.engine.Sync(m.listLocked())
+	return m.syncLocked()
 }
 
 // DeleteMember removes a member from a group.
@@ -334,7 +347,7 @@ func (m *Manager) DeleteMember(groupIDOrName, memberIDOrIface string) error {
 		return err
 	}
 
-	return m.engine.Sync(m.listLocked())
+	return m.syncLocked()
 }
 
 // ResolveGroupInterfaces resolves an SD-WAN group to its member interfaces (used by Firewall).

+ 3 - 1
internal/web/static/app.js

@@ -19194,6 +19194,7 @@ async function saveSDWANGroup(event) {
   const autoNAT = document.getElementById('sdwan-group-auto-nat').checked;
   const comment = document.getElementById('sdwan-group-comment').value.trim();
 
+  const existing = (state.sdwanGroups || []).find(g => g.id === id);
   const payload = {
     id: id || undefined,
     name,
@@ -19201,7 +19202,8 @@ async function saveSDWANGroup(event) {
     hash_fields: mode === 'pcc' ? hashFields : undefined,
     probe_interval: probeInterval,
     auto_nat: autoNAT,
-    comment
+    comment,
+    members: existing ? existing.members : undefined
   };
 
   try {