engine.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  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. // 1. Bypass inbound traffic from all member WAN interfaces so return traffic is never classified/marked
  62. seenInIfaces := make(map[string]bool)
  63. for _, g := range groups {
  64. for _, m := range g.Members {
  65. if m.Interface != "" && !seenInIfaces[m.Interface] {
  66. seenInIfaces[m.Interface] = true
  67. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-i", m.Interface, "-j", "RETURN").Run()
  68. }
  69. }
  70. }
  71. // 2. Bypass private RFC1918, loopback, link-local, and multicast
  72. 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"}
  73. for _, net := range privateNets {
  74. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-d", net, "-j", "RETURN").Run()
  75. }
  76. // 3. Ensure high-priority policy routing rules for private subnets so return traffic and LAN traffic always lookup main table
  77. e.ensurePrivateRoutingRules()
  78. // Restore connection mark: if a flow has already been classified, keep its mark (Session Stickiness!)
  79. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-j", "CONNMARK", "--restore-mark").Run()
  80. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN", "-m", "mark", "!", "--mark", "0", "-j", "RETURN").Run()
  81. markBase := 0x5d00
  82. tableBase := 1100
  83. memberCounter := 1
  84. for _, g := range groups {
  85. if g.Disabled || len(g.Members) == 0 {
  86. g.Running = false
  87. continue
  88. }
  89. // Filter active candidates: prefer healthy/degraded over down
  90. candidates := make([]*SDWANMember, 0)
  91. for _, m := range g.Members {
  92. if !m.Disabled {
  93. candidates = append(candidates, m)
  94. }
  95. }
  96. if len(candidates) == 0 {
  97. g.Running = false
  98. continue
  99. }
  100. // Determine online candidates
  101. onlineCandidates := make([]*SDWANMember, 0)
  102. for _, m := range candidates {
  103. if m.Status != StatusDown {
  104. onlineCandidates = append(onlineCandidates, m)
  105. }
  106. }
  107. // If all candidates are down, fallback to using all enabled candidates so traffic isn't completely blackholed
  108. activePool := onlineCandidates
  109. if len(activePool) == 0 {
  110. activePool = candidates
  111. }
  112. // Assign table ID and marks
  113. for _, m := range g.Members {
  114. m.TableID = tableBase + memberCounter
  115. m.MarkInt = uint32(markBase + memberCounter)
  116. m.MarkHex = fmt.Sprintf("0x%x", m.MarkInt)
  117. memberCounter++
  118. // Apply kernel routing table
  119. e.applyMemberRoute(m)
  120. // Apply policy routing rule (ip rule)
  121. e.applyMemberRule(m)
  122. // Auto-NAT if configured
  123. if g.AutoNAT && !m.Disabled {
  124. e.ensureNAT(m.Interface)
  125. }
  126. }
  127. // Apply Mangle classification rules based on group mode
  128. switch g.Mode {
  129. case ModeWeighted:
  130. e.applyWeightedRules(activePool)
  131. case ModePCC:
  132. e.applyPCCRules(activePool)
  133. case ModeLowestLatency:
  134. e.applyBestQualityRules(activePool, true)
  135. case ModeLowestLoss:
  136. e.applyBestQualityRules(activePool, false)
  137. case ModeFailover:
  138. e.applyFailoverRules(activePool)
  139. default:
  140. e.applyWeightedRules(activePool)
  141. }
  142. g.Running = true
  143. }
  144. return nil
  145. }
  146. func (e *KernelEngine) applyMemberRoute(m *SDWANMember) {
  147. if m.Interface == "" {
  148. return
  149. }
  150. tableStr := strconv.Itoa(m.TableID)
  151. // Flush table first
  152. _ = exec.Command("ip", "route", "flush", "table", tableStr).Run()
  153. gw := m.Gateway
  154. if gw == "" {
  155. gw = e.detectInterfaceGateway(m.Interface)
  156. }
  157. args := []string{"route", "replace", "default"}
  158. if gw != "" {
  159. args = append(args, "via", gw)
  160. }
  161. args = append(args, "dev", m.Interface, "table", tableStr)
  162. if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil {
  163. log.Printf("[SD-WAN] Warning: failed adding route for %s (table %s): %s (%v)", m.Interface, tableStr, strings.TrimSpace(string(out)), err)
  164. }
  165. }
  166. func (e *KernelEngine) detectInterfaceGateway(ifaceName string) string {
  167. link, err := netlink.LinkByName(ifaceName)
  168. if err != nil {
  169. return ""
  170. }
  171. routes, err := netlink.RouteList(link, netlink.FAMILY_V4)
  172. if err != nil {
  173. return ""
  174. }
  175. for _, r := range routes {
  176. if r.Gw != nil && !r.Gw.IsUnspecified() {
  177. return r.Gw.String()
  178. }
  179. }
  180. return ""
  181. }
  182. func (e *KernelEngine) applyMemberRule(m *SDWANMember) {
  183. tableStr := strconv.Itoa(m.TableID)
  184. prefStr := strconv.Itoa(2100 + m.TableID%1000)
  185. // Delete matching rule if already present
  186. _ = exec.Command("ip", "rule", "del", "fwmark", m.MarkHex, "table", tableStr).Run()
  187. // Add ip rule
  188. _ = exec.Command("ip", "rule", "add", "fwmark", m.MarkHex, "table", tableStr, "pref", prefStr).Run()
  189. }
  190. func (e *KernelEngine) applyWeightedRules(pool []*SDWANMember) {
  191. if len(pool) == 0 {
  192. return
  193. }
  194. if len(pool) == 1 {
  195. e.addMarkRule(pool[0].MarkHex, 1.0)
  196. return
  197. }
  198. totalWeight := 0
  199. for _, m := range pool {
  200. w := m.Weight
  201. if w <= 0 {
  202. w = 1
  203. }
  204. totalWeight += w
  205. }
  206. remainingWeight := totalWeight
  207. for i, m := range pool {
  208. if i == len(pool)-1 {
  209. // Last member gets 100% of the remainder
  210. e.addMarkRule(m.MarkHex, 1.0)
  211. break
  212. }
  213. w := m.Weight
  214. if w <= 0 {
  215. w = 1
  216. }
  217. prob := float64(w) / float64(remainingWeight)
  218. // Clamp probability between 0.0001 and 0.9999
  219. prob = math.Max(0.0001, math.Min(0.9999, prob))
  220. e.addMarkRule(m.MarkHex, prob)
  221. remainingWeight -= w
  222. }
  223. }
  224. func (e *KernelEngine) applyPCCRules(pool []*SDWANMember) {
  225. n := len(pool)
  226. if n == 0 {
  227. return
  228. }
  229. if n == 1 {
  230. e.addMarkRule(pool[0].MarkHex, 1.0)
  231. return
  232. }
  233. // Using nth packet mode for new connection distribution
  234. for i, m := range pool {
  235. args := []string{
  236. "-t", "mangle", "-A", "DEBTIK_SDWAN",
  237. "-m", "conntrack", "--ctstate", "NEW",
  238. "-m", "statistic", "--mode", "nth",
  239. "--every", strconv.Itoa(n - i),
  240. "--packet", "0",
  241. "-j", "MARK", "--set-mark", m.MarkHex,
  242. }
  243. _ = exec.Command("iptables", args...).Run()
  244. // Save mark into conntrack
  245. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  246. "-m", "conntrack", "--ctstate", "NEW",
  247. "-m", "mark", "--mark", m.MarkHex,
  248. "-j", "CONNMARK", "--save-mark").Run()
  249. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  250. "-m", "conntrack", "--ctstate", "NEW",
  251. "-m", "mark", "--mark", m.MarkHex,
  252. "-j", "RETURN").Run()
  253. }
  254. }
  255. func (e *KernelEngine) applyBestQualityRules(pool []*SDWANMember, latencyMode bool) {
  256. if len(pool) == 0 {
  257. return
  258. }
  259. best := pool[0]
  260. for _, m := range pool[1:] {
  261. if latencyMode {
  262. if m.LiveLatency > 0 && (best.LiveLatency <= 0 || m.LiveLatency < best.LiveLatency) {
  263. best = m
  264. }
  265. } else {
  266. if m.PacketLoss < best.PacketLoss {
  267. best = m
  268. }
  269. }
  270. }
  271. e.addMarkRule(best.MarkHex, 1.0)
  272. }
  273. func (e *KernelEngine) applyFailoverRules(pool []*SDWANMember) {
  274. if len(pool) == 0 {
  275. return
  276. }
  277. // Sort by priority (1 is highest)
  278. primary := pool[0]
  279. for _, m := range pool[1:] {
  280. prio := m.Priority
  281. if prio <= 0 {
  282. prio = 100
  283. }
  284. bestPrio := primary.Priority
  285. if bestPrio <= 0 {
  286. bestPrio = 100
  287. }
  288. if prio < bestPrio {
  289. primary = m
  290. }
  291. }
  292. e.addMarkRule(primary.MarkHex, 1.0)
  293. }
  294. func (e *KernelEngine) addMarkRule(markHex string, prob float64) {
  295. if prob >= 1.0 {
  296. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  297. "-m", "conntrack", "--ctstate", "NEW",
  298. "-j", "MARK", "--set-mark", markHex).Run()
  299. } else {
  300. probStr := fmt.Sprintf("%.4f", prob)
  301. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  302. "-m", "conntrack", "--ctstate", "NEW",
  303. "-m", "statistic", "--mode", "random", "--probability", probStr,
  304. "-j", "MARK", "--set-mark", markHex).Run()
  305. }
  306. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  307. "-m", "conntrack", "--ctstate", "NEW",
  308. "-m", "mark", "--mark", markHex,
  309. "-j", "CONNMARK", "--save-mark").Run()
  310. _ = exec.Command("iptables", "-t", "mangle", "-A", "DEBTIK_SDWAN",
  311. "-m", "conntrack", "--ctstate", "NEW",
  312. "-m", "mark", "--mark", markHex,
  313. "-j", "RETURN").Run()
  314. }
  315. func (e *KernelEngine) ensureMangleJump(chain string) {
  316. // Check if jump exists
  317. check := exec.Command("iptables", "-t", "mangle", "-C", chain, "-j", "DEBTIK_SDWAN")
  318. if check.Run() != nil {
  319. _ = exec.Command("iptables", "-t", "mangle", "-I", chain, "1", "-j", "DEBTIK_SDWAN").Run()
  320. }
  321. }
  322. func (e *KernelEngine) ensureNAT(iface string) {
  323. if iface == "" {
  324. return
  325. }
  326. check := exec.Command("iptables", "-t", "nat", "-C", "POSTROUTING", "-o", iface, "-m", "comment", "--comment", "sdwan-auto-nat", "-j", "MASQUERADE")
  327. if check.Run() != nil {
  328. _ = exec.Command("iptables", "-t", "nat", "-A", "POSTROUTING", "-o", iface, "-m", "comment", "--comment", "sdwan-auto-nat", "-j", "MASQUERADE").Run()
  329. }
  330. }
  331. func (e *KernelEngine) cleanupMangle() {
  332. _ = exec.Command("iptables", "-t", "mangle", "-D", "PREROUTING", "-j", "DEBTIK_SDWAN").Run()
  333. _ = exec.Command("iptables", "-t", "mangle", "-D", "OUTPUT", "-j", "DEBTIK_SDWAN").Run()
  334. _ = exec.Command("iptables", "-t", "mangle", "-F", "DEBTIK_SDWAN").Run()
  335. _ = exec.Command("iptables", "-t", "mangle", "-X", "DEBTIK_SDWAN").Run()
  336. }
  337. func (e *KernelEngine) ensurePrivateRoutingRules() {
  338. privateNets := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
  339. for _, net := range privateNets {
  340. _ = exec.Command("ip", "rule", "del", "to", net, "table", "main", "pref", "2000").Run()
  341. _ = exec.Command("ip", "rule", "add", "to", net, "table", "main", "pref", "2000").Run()
  342. }
  343. }
  344. func (e *KernelEngine) cleanupIPRules() {
  345. privateNets := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
  346. for _, net := range privateNets {
  347. _ = exec.Command("ip", "rule", "del", "to", net, "table", "main", "pref", "2000").Run()
  348. }
  349. out, err := exec.Command("ip", "rule", "show").CombinedOutput()
  350. if err != nil {
  351. return
  352. }
  353. lines := strings.Split(string(out), "\n")
  354. for _, l := range lines {
  355. if strings.Contains(l, "0x5d") {
  356. // Extract fwmark and table
  357. fields := strings.Fields(l)
  358. for i, f := range fields {
  359. if f == "fwmark" && i+1 < len(fields) {
  360. mark := fields[i+1]
  361. _ = exec.Command("ip", "rule", "del", "fwmark", mark).Run()
  362. }
  363. }
  364. }
  365. }
  366. }
  367. func (e *KernelEngine) ensureVirtualDevice(name string) {
  368. if name == "" {
  369. return
  370. }
  371. link, err := netlink.LinkByName(name)
  372. if err == nil {
  373. if link.Attrs().Flags&net.FlagUp == 0 {
  374. _ = netlink.LinkSetUp(link)
  375. }
  376. return
  377. }
  378. dummy := &netlink.Dummy{
  379. LinkAttrs: netlink.LinkAttrs{
  380. Name: name,
  381. MTU: 1500,
  382. },
  383. }
  384. if err := netlink.LinkAdd(dummy); err != nil {
  385. _ = exec.Command("ip", "link", "add", name, "type", "dummy").Run()
  386. _ = exec.Command("ip", "link", "set", name, "up", "mtu", "1500").Run()
  387. return
  388. }
  389. _ = netlink.LinkSetUp(dummy)
  390. }
  391. func (e *KernelEngine) removeVirtualDevice(name string) {
  392. if name == "" {
  393. return
  394. }
  395. link, err := netlink.LinkByName(name)
  396. if err == nil {
  397. _ = netlink.LinkDel(link)
  398. } else {
  399. _ = exec.Command("ip", "link", "del", name).Run()
  400. }
  401. }