|
|
@@ -67,9 +67,9 @@ func (e *KernelEngine) Sync(groups []*SDWANGroup) error {
|
|
|
_ = exec.Command("iptables", "-t", "mangle", "-N", "DEBTIK_SDWAN").Run()
|
|
|
_ = exec.Command("iptables", "-t", "mangle", "-F", "DEBTIK_SDWAN").Run()
|
|
|
|
|
|
- // Ensure jumps from PREROUTING and OUTPUT
|
|
|
+ // Ensure jumps from PREROUTING only. Local router management, WireGuard tunnel
|
|
|
+ // underlays, routing daemons (FRR/BGP/EIGRP), and health check probes must not be hijacked.
|
|
|
e.ensureMangleJump("PREROUTING")
|
|
|
- e.ensureMangleJump("OUTPUT")
|
|
|
|
|
|
// 1. Bypass inbound traffic from all member WAN interfaces so return traffic is never classified/marked
|
|
|
seenInIfaces := make(map[string]bool)
|
|
|
@@ -91,14 +91,17 @@ func (e *KernelEngine) Sync(groups []*SDWANGroup) error {
|
|
|
// 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()
|
|
|
-
|
|
|
markBase := 0x5d00
|
|
|
tableBase := 1100
|
|
|
memberCounter := 1
|
|
|
|
|
|
+ type groupPlan struct {
|
|
|
+ group *SDWANGroup
|
|
|
+ activePool []*SDWANMember
|
|
|
+ }
|
|
|
+ var plans []groupPlan
|
|
|
+ var allActiveMembers []*SDWANMember
|
|
|
+
|
|
|
for _, g := range groups {
|
|
|
if g.Disabled || len(g.Members) == 0 {
|
|
|
g.Running = false
|
|
|
@@ -151,7 +154,26 @@ func (e *KernelEngine) Sync(groups []*SDWANGroup) error {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Apply Mangle classification rules based on group mode
|
|
|
+ plans = append(plans, groupPlan{group: g, activePool: activePool})
|
|
|
+ allActiveMembers = append(allActiveMembers, activePool...)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. Restore connection mark:
|
|
|
+ _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-j", "CONNMARK", "--restore-mark").Run()
|
|
|
+
|
|
|
+ // Only return for active, healthy link marks (prevents sticky blackholes on failed links)
|
|
|
+ for _, m := range allActiveMembers {
|
|
|
+ _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-m", "mark", "--mark", m.MarkHex, "-j", "RETURN").Run()
|
|
|
+ }
|
|
|
+
|
|
|
+ // If mark does not belong to any currently active link (new connection or failed link), reset to 0 to reclassify:
|
|
|
+ _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-j", "MARK", "--set-mark", "0").Run()
|
|
|
+
|
|
|
+ // 5. Apply Mangle classification rules based on group mode
|
|
|
+ for _, p := range plans {
|
|
|
+ g := p.group
|
|
|
+ activePool := p.activePool
|
|
|
+
|
|
|
switch g.Mode {
|
|
|
case ModeWeighted:
|
|
|
e.applyWeightedRules(activePool)
|
|
|
@@ -182,16 +204,20 @@ func (e *KernelEngine) applyMemberRoute(m *SDWANMember) {
|
|
|
// Flush table first
|
|
|
_ = exec.Command("ip", "route", "flush", "table", tableStr).Run()
|
|
|
|
|
|
- gw := m.Gateway
|
|
|
+ gw := strings.TrimSpace(m.Gateway)
|
|
|
if gw == "" {
|
|
|
gw = e.detectInterfaceGateway(m.Interface)
|
|
|
}
|
|
|
|
|
|
args := []string{"route", "replace", "default"}
|
|
|
- if gw != "" {
|
|
|
+ if gw != "" && net.ParseIP(gw) != nil {
|
|
|
args = append(args, "via", gw)
|
|
|
}
|
|
|
- args = append(args, "dev", m.Interface, "table", tableStr)
|
|
|
+ args = append(args, "dev", m.Interface)
|
|
|
+ if gw != "" && net.ParseIP(gw) != nil {
|
|
|
+ args = append(args, "onlink")
|
|
|
+ }
|
|
|
+ args = append(args, "table", tableStr)
|
|
|
|
|
|
if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil {
|
|
|
log.Printf("[SD-WAN] Warning: failed adding route for %s (table %s): %s (%v)", m.Interface, tableStr, strings.TrimSpace(string(out)), err)
|
|
|
@@ -200,16 +226,40 @@ 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 ""
|
|
|
+ if err == nil {
|
|
|
+ routes, err := netlink.RouteList(link, netlink.FAMILY_V4)
|
|
|
+ if err == nil {
|
|
|
+ // Prefer default route gateway first
|
|
|
+ for _, r := range routes {
|
|
|
+ if (r.Dst == nil || r.Dst.IP.IsUnspecified()) && r.Gw != nil && !r.Gw.IsUnspecified() {
|
|
|
+ return r.Gw.String()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Fallback to any non-empty gateway on this link
|
|
|
+ for _, r := range routes {
|
|
|
+ if r.Gw != nil && !r.Gw.IsUnspecified() {
|
|
|
+ return r.Gw.String()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- for _, r := range routes {
|
|
|
- if r.Gw != nil && !r.Gw.IsUnspecified() {
|
|
|
- return r.Gw.String()
|
|
|
+
|
|
|
+ // Also check ip route show dev <ifaceName> via CLI as fallback
|
|
|
+ out, err := exec.Command("ip", "route", "show", "dev", ifaceName).CombinedOutput()
|
|
|
+ if err == nil {
|
|
|
+ lines := strings.Split(string(out), "\n")
|
|
|
+ for _, l := range lines {
|
|
|
+ if strings.Contains(l, "via ") {
|
|
|
+ fields := strings.Fields(l)
|
|
|
+ for i, f := range fields {
|
|
|
+ if f == "via" && i+1 < len(fields) {
|
|
|
+ gw := fields[i+1]
|
|
|
+ if net.ParseIP(gw) != nil {
|
|
|
+ return gw
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
return ""
|
|
|
@@ -275,11 +325,11 @@ func (e *KernelEngine) applyPCCRules(pool []*SDWANMember) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- // Using nth packet mode for new connection distribution
|
|
|
+ // Using nth packet mode for unclassified / failed-flow distribution (mark 0)
|
|
|
for i, m := range pool {
|
|
|
args := []string{
|
|
|
"-t", "mangle", "-A", "DEBTIK_SDWAN",
|
|
|
- "-m", "conntrack", "--ctstate", "NEW",
|
|
|
+ "-m", "mark", "--mark", "0",
|
|
|
"-m", "statistic", "--mode", "nth",
|
|
|
"--every", strconv.Itoa(n - i),
|
|
|
"--packet", "0",
|
|
|
@@ -289,12 +339,10 @@ func (e *KernelEngine) applyPCCRules(pool []*SDWANMember) {
|
|
|
|
|
|
// Save mark into conntrack
|
|
|
_ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
|
|
|
- "-m", "conntrack", "--ctstate", "NEW",
|
|
|
"-m", "mark", "--mark", m.MarkHex,
|
|
|
"-j", "CONNMARK", "--save-mark").Run()
|
|
|
|
|
|
_ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
|
|
|
- "-m", "conntrack", "--ctstate", "NEW",
|
|
|
"-m", "mark", "--mark", m.MarkHex,
|
|
|
"-j", "RETURN").Run()
|
|
|
}
|
|
|
@@ -344,23 +392,21 @@ func (e *KernelEngine) applyFailoverRules(pool []*SDWANMember) {
|
|
|
func (e *KernelEngine) addMarkRule(markHex string, prob float64) {
|
|
|
if prob >= 1.0 {
|
|
|
_ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
|
|
|
- "-m", "conntrack", "--ctstate", "NEW",
|
|
|
+ "-m", "mark", "--mark", "0",
|
|
|
"-j", "MARK", "--set-mark", markHex).Run()
|
|
|
} else {
|
|
|
probStr := fmt.Sprintf("%.4f", prob)
|
|
|
_ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
|
|
|
- "-m", "conntrack", "--ctstate", "NEW",
|
|
|
+ "-m", "mark", "--mark", "0",
|
|
|
"-m", "statistic", "--mode", "random", "--probability", probStr,
|
|
|
"-j", "MARK", "--set-mark", markHex).Run()
|
|
|
}
|
|
|
|
|
|
_ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
|
|
|
- "-m", "conntrack", "--ctstate", "NEW",
|
|
|
"-m", "mark", "--mark", markHex,
|
|
|
"-j", "CONNMARK", "--save-mark").Run()
|
|
|
|
|
|
_ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
|
|
|
- "-m", "conntrack", "--ctstate", "NEW",
|
|
|
"-m", "mark", "--mark", markHex,
|
|
|
"-j", "RETURN").Run()
|
|
|
}
|