engine.go 11 KB

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