routing.lua 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. --[[
  2. tests/routing.lua — Unit tests for routing.lua
  3. Pure helpers + injected-exec integration (no root needed).
  4. ]]
  5. package.path = package.path .. ";./src/?.lua"
  6. local routing = require("balancerlite.routing")
  7. local passed = 0
  8. local failed = 0
  9. local function check(name, ok, detail)
  10. if ok then
  11. passed = passed + 1
  12. print(" PASS " .. name)
  13. else
  14. failed = failed + 1
  15. print(" FAIL " .. name .. (detail and (" " .. detail) or ""))
  16. end
  17. end
  18. -- -----------------------------------------------------------------
  19. -- TEST 1: q() safe-quoting
  20. -- -----------------------------------------------------------------
  21. print("\n[1] q() safe-quoting")
  22. do
  23. local q = routing.q
  24. check("plain host accepted", q("1.2.3.4") == "'1.2.3.4'", tostring(q("1.2.3.4")))
  25. check("iface accepted", q("eth0") == "'eth0'", tostring(q("eth0")))
  26. check("colon-in-port accepted", q("8.8.8.8:53") == "'8.8.8.8:53'", tostring(q("8.8.8.8:53")))
  27. check("slash accepted", q("/dev/null") == "'/dev/null'", tostring(q("/dev/null")))
  28. check("unsafe spaces rejected", q("a b") == nil, tostring(q("a b")))
  29. check("unsafe shell-meta rejected", q("a;rm") == nil, tostring(q("a;rm")))
  30. check("nil rejected", q(nil) == nil, "nil returned non-nil")
  31. check("number rejected", q(42) == nil, "number returned non-nil")
  32. end
  33. -- -----------------------------------------------------------------
  34. -- TEST 2: build_switch_cmds happy path
  35. -- -----------------------------------------------------------------
  36. print("\n[2] build_switch_cmds")
  37. do
  38. local rcfg = { table_a = 100, table_b = 101, mark_a = 0x1, mark_b = 0x2 }
  39. local wan = {
  40. id = "wan-a", interface = "eth0", gateway = "1.2.3.1",
  41. routing_table = 100, mark = 0x1,
  42. }
  43. local r = routing.build_switch_cmds(rcfg, wan, 254)
  44. check("ok", r.ok, r.err)
  45. check("2 cmds emitted", r.cmds and #r.cmds == 2, "got " .. tostring(#r.cmds))
  46. check("main-table replace present",
  47. r.cmds[1]:match("table 254") and r.cmds[1]:match("via '1.2.3.1'") and
  48. r.cmds[1]:match("dev 'eth0'"),
  49. "cmd: " .. tostring(r.cmds[1]))
  50. check("per-WAN table replace present",
  51. r.cmds[2]:match("table 100") and r.cmds[2]:match("via '1.2.3.1'"),
  52. "cmd: " .. tostring(r.cmds[2]))
  53. end
  54. -- -----------------------------------------------------------------
  55. -- TEST 3: build_switch_cmds rejects unsafe iface
  56. -- -----------------------------------------------------------------
  57. print("\n[3] build_switch_cmds unsafe input")
  58. do
  59. local r = routing.build_switch_cmds({}, { interface = "eth0; rm", gateway = "1.2.3.1" }, 254)
  60. check("rejected", r.ok == false)
  61. check("err set", type(r.err) == "string" and r.err:match("unsafe"))
  62. end
  63. -- -----------------------------------------------------------------
  64. -- TEST 4: build_init_cmds covers both WANs + main
  65. -- -----------------------------------------------------------------
  66. print("\n[4] build_init_cmds")
  67. do
  68. local rcfg = { table_a = 100, table_b = 101, mark_a = 0x1, mark_b = 0x2 }
  69. local wans = {
  70. { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100, mark = 0x1 },
  71. { id = "wan-b", interface = "eth1", gateway = "4.5.6.1", routing_table = 101, mark = 0x2 },
  72. }
  73. local r = routing.build_init_cmds(rcfg, wans, 254)
  74. check("ok", r.ok, r.err)
  75. check("4 cmds emitted (2 tables + 2 del/add rules per-WAN + 1 main)",
  76. r.cmds and #r.cmds == 7, "got " .. tostring(#r.cmds))
  77. -- main table command is the last one
  78. local last = r.cmds[#r.cmds]
  79. check("main-table default via preferred WAN",
  80. last:match("table 254") and last:match("via '1.2.3.1'"),
  81. "cmd: " .. tostring(last))
  82. end
  83. -- -----------------------------------------------------------------
  84. -- TEST 5: parse_default_gw
  85. -- -----------------------------------------------------------------
  86. print("\n[5] parse_default_gw")
  87. do
  88. local p = routing.parse_default_gw
  89. check("happy path",
  90. p("default via 1.2.3.1 dev eth0 proto static metric 100") == "1.2.3.1")
  91. check("returns first default only",
  92. p("default via 1.2.3.1 dev eth0\ndefault via 4.5.6.1 dev eth1") == "1.2.3.1")
  93. check("nil on empty", p("") == nil)
  94. check("nil on nil", p(nil) == nil)
  95. check("nil on no default", p("1.2.3.0/24 dev eth0") == nil)
  96. end
  97. -- -----------------------------------------------------------------
  98. -- TEST 6: switch_to with injectable exec
  99. -- -----------------------------------------------------------------
  100. print("\n[6] switch_to with exec injection")
  101. do
  102. local cmds_run = {}
  103. local function fake_exec(cmd) cmds_run[#cmds_run+1] = cmd; return 0 end
  104. local rcfg = { table_a = 100, table_b = 101, mark_a = 0x1, mark_b = 0x2 }
  105. local wans = {
  106. { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100, mark = 0x1 },
  107. { id = "wan-b", interface = "eth1", gateway = "4.5.6.1", routing_table = 101, mark = 0x2 },
  108. }
  109. local R = routing.new(rcfg, wans, { exec = fake_exec })
  110. local r = R:switch_to("wan-b")
  111. check("ok", r.ok, r.err)
  112. check("2 cmds executed", #cmds_run == 2, "got " .. tostring(#cmds_run))
  113. check("main-table cmd is wan-b's gateway",
  114. cmds_run[1]:match("via '4.5.6.1'") and cmds_run[1]:match("table 254"))
  115. check("wan-b table cmd",
  116. cmds_run[2]:match("via '4.5.6.1'") and cmds_run[2]:match("table 101"))
  117. end
  118. -- -----------------------------------------------------------------
  119. -- TEST 7: switch_to returns failure when exec non-zero
  120. -- -----------------------------------------------------------------
  121. print("\n[7] switch_to failure handling")
  122. do
  123. local function fake_exec(cmd) return 1 end -- ip failure
  124. local rcfg = { table_a = 100, table_b = 101 }
  125. local wans = {
  126. { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100 },
  127. { id = "wan-b", interface = "eth1", gateway = "4.5.6.1", routing_table = 101 },
  128. }
  129. local R = routing.new(rcfg, wans, { exec = fake_exec })
  130. local r = R:switch_to("wan-a")
  131. check("ok=false", r.ok == false)
  132. check("err reports rc", type(r.err) == "string" and r.err:match("rc=1"))
  133. end
  134. -- -----------------------------------------------------------------
  135. -- TEST 8: switch_to unknown wan
  136. -- -----------------------------------------------------------------
  137. print("\n[8] switch_to unknown wan")
  138. do
  139. local R = routing.new({}, {
  140. { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100 },
  141. })
  142. local r = R:switch_to("wan-z")
  143. check("ok=false", r.ok == false)
  144. check("err mentions wan-z", type(r.err) == "string" and r.err:match("wan.z"))
  145. end
  146. -- -----------------------------------------------------------------
  147. -- TEST 9: apply_default with dry-run
  148. -- -----------------------------------------------------------------
  149. print("\n[9] apply_default dry-run")
  150. do
  151. local R = routing.new({}, {
  152. { id = "wan-a", interface = "eth0", gateway = "1.2.3.1", routing_table = 100, mark = 0x1 },
  153. }, { dry_run = true })
  154. local r = R:apply_default()
  155. check("ok", r.ok, r.err)
  156. check("dry=true", r.dry == true)
  157. end
  158. -- -----------------------------------------------------------------
  159. -- Done
  160. -- -----------------------------------------------------------------
  161. print(string.format("\n=== routing: %d/%d passed ===", passed, passed + failed))
  162. if failed > 0 then os.exit(1) end