engine.go 13 KB

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