engine.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. package sdwan
  2. import (
  3. "fmt"
  4. "log"
  5. "math"
  6. "os/exec"
  7. "strconv"
  8. "strings"
  9. "sync"
  10. )
  11. // KernelEngine manages routing tables, ip rules, and netfilter mangle rules for SD-WAN.
  12. type KernelEngine struct {
  13. mu sync.Mutex
  14. }
  15. func NewKernelEngine() *KernelEngine {
  16. return &KernelEngine{}
  17. }
  18. // Sync applies the complete routing and iptables rules for all active SD-WAN groups.
  19. func (e *KernelEngine) Sync(groups []*SDWANGroup) error {
  20. e.mu.Lock()
  21. defer e.mu.Unlock()
  22. // 1. Clean existing SD-WAN mangle chain & rules
  23. e.cleanupMangle()
  24. // 2. Clear existing SD-WAN ip rules
  25. e.cleanupIPRules()
  26. hasActiveGroups := false
  27. for _, g := range groups {
  28. if !g.Disabled && len(g.Members) > 0 {
  29. hasActiveGroups = true
  30. break
  31. }
  32. }
  33. if !hasActiveGroups {
  34. return nil
  35. }
  36. // 3. Create fresh DEBTIK_SDWAN mangle chain
  37. _ = exec.Command("iptables", "-t", "mangle", "-N", "DEBTIK_SDWAN").Run()
  38. _ = exec.Command("iptables", "-t", "mangle", "-F", "DEBTIK_SDWAN").Run()
  39. // Ensure jumps from PREROUTING and OUTPUT
  40. e.ensureMangleJump("PREROUTING")
  41. e.ensureMangleJump("OUTPUT")
  42. // Bypass private RFC1918, loopback, link-local, and multicast
  43. 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"}
  44. for _, net := range privateNets {
  45. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-d", net, "-j", "RETURN").Run()
  46. }
  47. // Restore connection mark: if a flow has already been classified, keep its mark (Session Stickiness!)
  48. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-j", "CONNMARK", "--restore-mark").Run()
  49. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-m", "mark", "!", "--mark", "0", "-j", "RETURN").Run()
  50. markBase := 0x5d00
  51. tableBase := 1100
  52. memberCounter := 1
  53. for _, g := range groups {
  54. if g.Disabled || len(g.Members) == 0 {
  55. g.Running = false
  56. continue
  57. }
  58. // Filter active candidates: prefer healthy/degraded over down
  59. candidates := make([]*SDWANMember, 0)
  60. for _, m := range g.Members {
  61. if !m.Disabled {
  62. candidates = append(candidates, m)
  63. }
  64. }
  65. if len(candidates) == 0 {
  66. g.Running = false
  67. continue
  68. }
  69. // Determine online candidates
  70. onlineCandidates := make([]*SDWANMember, 0)
  71. for _, m := range candidates {
  72. if m.Status != StatusDown {
  73. onlineCandidates = append(onlineCandidates, m)
  74. }
  75. }
  76. // If all candidates are down, fallback to using all enabled candidates so traffic isn't completely blackholed
  77. activePool := onlineCandidates
  78. if len(activePool) == 0 {
  79. activePool = candidates
  80. }
  81. // Assign table ID and marks
  82. for _, m := range g.Members {
  83. m.TableID = tableBase + memberCounter
  84. m.MarkInt = uint32(markBase + memberCounter)
  85. m.MarkHex = fmt.Sprintf("0x%x", m.MarkInt)
  86. memberCounter++
  87. // Apply kernel routing table
  88. e.applyMemberRoute(m)
  89. // Apply policy routing rule (ip rule)
  90. e.applyMemberRule(m)
  91. // Auto-NAT if configured
  92. if g.AutoNAT && !m.Disabled {
  93. e.ensureNAT(m.Interface)
  94. }
  95. }
  96. // Apply Mangle classification rules based on group mode
  97. switch g.Mode {
  98. case ModeWeighted:
  99. e.applyWeightedRules(activePool)
  100. case ModePCC:
  101. e.applyPCCRules(activePool)
  102. case ModeLowestLatency:
  103. e.applyBestQualityRules(activePool, true)
  104. case ModeLowestLoss:
  105. e.applyBestQualityRules(activePool, false)
  106. case ModeFailover:
  107. e.applyFailoverRules(activePool)
  108. default:
  109. e.applyWeightedRules(activePool)
  110. }
  111. g.Running = true
  112. }
  113. return nil
  114. }
  115. func (e *KernelEngine) applyMemberRoute(m *SDWANMember) {
  116. if m.Interface == "" {
  117. return
  118. }
  119. tableStr := strconv.Itoa(m.TableID)
  120. // Flush table first
  121. _ = exec.Command("ip", "route", "flush", "table", tableStr).Run()
  122. args := []string{"route", "replace", "default"}
  123. if m.Gateway != "" {
  124. args = append(args, "via", m.Gateway)
  125. }
  126. args = append(args, "dev", m.Interface, "table", tableStr)
  127. if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil {
  128. log.Printf("[SD-WAN] Warning: failed adding route for %s (table %s): %s (%v)", m.Interface, tableStr, strings.TrimSpace(string(out)), err)
  129. }
  130. }
  131. func (e *KernelEngine) applyMemberRule(m *SDWANMember) {
  132. tableStr := strconv.Itoa(m.TableID)
  133. prefStr := strconv.Itoa(2000 + m.TableID%1000)
  134. // Delete matching rule if already present
  135. _ = exec.Command("ip", "rule", "del", "fwmark", m.MarkHex, "table", tableStr).Run()
  136. // Add ip rule
  137. _ = exec.Command("ip", "rule", "add", "fwmark", m.MarkHex, "table", tableStr, "pref", prefStr).Run()
  138. }
  139. func (e *KernelEngine) applyWeightedRules(pool []*SDWANMember) {
  140. if len(pool) == 0 {
  141. return
  142. }
  143. if len(pool) == 1 {
  144. e.addMarkRule(pool[0].MarkHex, 1.0)
  145. return
  146. }
  147. totalWeight := 0
  148. for _, m := range pool {
  149. w := m.Weight
  150. if w <= 0 {
  151. w = 1
  152. }
  153. totalWeight += w
  154. }
  155. remainingWeight := totalWeight
  156. for i, m := range pool {
  157. if i == len(pool)-1 {
  158. // Last member gets 100% of the remainder
  159. e.addMarkRule(m.MarkHex, 1.0)
  160. break
  161. }
  162. w := m.Weight
  163. if w <= 0 {
  164. w = 1
  165. }
  166. prob := float64(w) / float64(remainingWeight)
  167. // Clamp probability between 0.0001 and 0.9999
  168. prob = math.Max(0.0001, math.Min(0.9999, prob))
  169. e.addMarkRule(m.MarkHex, prob)
  170. remainingWeight -= w
  171. }
  172. }
  173. func (e *KernelEngine) applyPCCRules(pool []*SDWANMember) {
  174. n := len(pool)
  175. if n == 0 {
  176. return
  177. }
  178. if n == 1 {
  179. e.addMarkRule(pool[0].MarkHex, 1.0)
  180. return
  181. }
  182. // Using nth packet mode for new connection distribution
  183. for i, m := range pool {
  184. args := []string{
  185. "-t", "mangle", "-A", "DEBTIK_SDWAN",
  186. "-m", "conntrack", "--ctstate", "NEW",
  187. "-m", "statistic", "--mode", "nth",
  188. "--every", strconv.Itoa(n - i),
  189. "--packet", "0",
  190. "-j", "MARK", "--set-mark", m.MarkHex,
  191. }
  192. _ = exec.Command("iptables", args...).Run()
  193. // Save mark into conntrack
  194. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  195. "-m", "conntrack", "--ctstate", "NEW",
  196. "-m", "mark", "--mark", m.MarkHex,
  197. "-j", "CONNMARK", "--save-mark").Run()
  198. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  199. "-m", "conntrack", "--ctstate", "NEW",
  200. "-m", "mark", "--mark", m.MarkHex,
  201. "-j", "RETURN").Run()
  202. }
  203. }
  204. func (e *KernelEngine) applyBestQualityRules(pool []*SDWANMember, latencyMode bool) {
  205. if len(pool) == 0 {
  206. return
  207. }
  208. best := pool[0]
  209. for _, m := range pool[1:] {
  210. if latencyMode {
  211. if m.LiveLatency > 0 && (best.LiveLatency <= 0 || m.LiveLatency < best.LiveLatency) {
  212. best = m
  213. }
  214. } else {
  215. if m.PacketLoss < best.PacketLoss {
  216. best = m
  217. }
  218. }
  219. }
  220. e.addMarkRule(best.MarkHex, 1.0)
  221. }
  222. func (e *KernelEngine) applyFailoverRules(pool []*SDWANMember) {
  223. if len(pool) == 0 {
  224. return
  225. }
  226. // Sort by priority (1 is highest)
  227. primary := pool[0]
  228. for _, m := range pool[1:] {
  229. prio := m.Priority
  230. if prio <= 0 {
  231. prio = 100
  232. }
  233. bestPrio := primary.Priority
  234. if bestPrio <= 0 {
  235. bestPrio = 100
  236. }
  237. if prio < bestPrio {
  238. primary = m
  239. }
  240. }
  241. e.addMarkRule(primary.MarkHex, 1.0)
  242. }
  243. func (e *KernelEngine) addMarkRule(markHex string, prob float64) {
  244. if prob >= 1.0 {
  245. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  246. "-m", "conntrack", "--ctstate", "NEW",
  247. "-j", "MARK", "--set-mark", markHex).Run()
  248. } else {
  249. probStr := fmt.Sprintf("%.4f", prob)
  250. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  251. "-m", "conntrack", "--ctstate", "NEW",
  252. "-m", "statistic", "--mode", "random", "--probability", probStr,
  253. "-j", "MARK", "--set-mark", markHex).Run()
  254. }
  255. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  256. "-m", "conntrack", "--ctstate", "NEW",
  257. "-m", "mark", "--mark", markHex,
  258. "-j", "CONNMARK", "--save-mark").Run()
  259. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  260. "-m", "conntrack", "--ctstate", "NEW",
  261. "-m", "mark", "--mark", markHex,
  262. "-j", "RETURN").Run()
  263. }
  264. func (e *KernelEngine) ensureMangleJump(chain string) {
  265. // Check if jump exists
  266. check := exec.Command("iptables", "-t", "mangle", "-C", chain, "-j", "DEBTIK_SDWAN")
  267. if check.Run() != nil {
  268. _ = exec.Command("iptables", "-t", "mangle", "-I", chain, "1", "-j", "DEBTIK_SDWAN").Run()
  269. }
  270. }
  271. func (e *KernelEngine) ensureNAT(iface string) {
  272. if iface == "" {
  273. return
  274. }
  275. check := exec.Command("iptables", "-t", "nat", "-C", "POSTROUTING", "-o", iface, "-m", "comment", "--comment", "sdwan-auto-nat", "-j", "MASQUERADE")
  276. if check.Run() != nil {
  277. _ = exec.Command("iptables", "-t", "nat", "-A", "POSTROUTING", "-o", iface, "-m", "comment", "--comment", "sdwan-auto-nat", "-j", "MASQUERADE").Run()
  278. }
  279. }
  280. func (e *KernelEngine) cleanupMangle() {
  281. _ = exec.Command("iptables", "-t", "mangle", "-D", "PREROUTING", "-j", "DEBTIK_SDWAN").Run()
  282. _ = exec.Command("iptables", "-t", "mangle", "-D", "OUTPUT", "-j", "DEBTIK_SDWAN").Run()
  283. _ = exec.Command("iptables", "-t", "mangle", "-F", "DEBTIK_SDWAN").Run()
  284. _ = exec.Command("iptables", "-t", "mangle", "-X", "DEBTIK_SDWAN").Run()
  285. }
  286. func (e *KernelEngine) cleanupIPRules() {
  287. out, err := exec.Command("ip", "rule", "show").CombinedOutput()
  288. if err != nil {
  289. return
  290. }
  291. lines := strings.Split(string(out), "\n")
  292. for _, l := range lines {
  293. if strings.Contains(l, "0x5d") {
  294. // Extract fwmark and table
  295. fields := strings.Fields(l)
  296. for i, f := range fields {
  297. if f == "fwmark" && i+1 < len(fields) {
  298. mark := fields[i+1]
  299. _ = exec.Command("ip", "rule", "del", "fwmark", mark).Run()
  300. }
  301. }
  302. }
  303. }
  304. }