v6alias_test.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. package amneziawgnet
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  7. )
  8. func peerWithIPs(email string, ips ...string) amneziawg.Peer {
  9. return amneziawg.Peer{Email: email, PublicKey: "pub-" + email, AllowedIPs: ips}
  10. }
  11. func instV6(enabled bool, extIface, v6ExtIface string, peers ...amneziawg.Peer) amneziawg.Instance {
  12. return amneziawg.Instance{
  13. Id: 1,
  14. IPv6Enabled: enabled,
  15. ExternalInterface: extIface,
  16. IPv6ExternalInterface: v6ExtIface,
  17. Peers: peers,
  18. }
  19. }
  20. func TestV6AliasesActive(t *testing.T) {
  21. cases := []struct {
  22. name string
  23. inst amneziawg.Instance
  24. want bool
  25. }{
  26. {"enabled with interface", instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::2/128")), true},
  27. {"enabled, IPv6ExternalInterface only", instV6(true, "", "eth1", peerWithIPs("a@x", "fd86::2/128")), true},
  28. {"disabled", instV6(false, "eth0", "", peerWithIPs("a@x", "fd86::2/128")), false},
  29. {"enabled, no interface either way", instV6(true, "", "", peerWithIPs("a@x", "fd86::2/128")), false},
  30. }
  31. for _, c := range cases {
  32. if got := V6AliasesActive(c.inst); got != c.want {
  33. t.Errorf("%s: V6AliasesActive = %v, want %v", c.name, got, c.want)
  34. }
  35. }
  36. }
  37. func TestDesiredV6AliasesDisabledOrNoInterfaceReturnsEmpty(t *testing.T) {
  38. cases := []struct {
  39. name string
  40. inst amneziawg.Instance
  41. }{
  42. {"IPv6Enabled false", instV6(false, "", "eth0", peerWithIPs("a@x", "fd86::2/128"))},
  43. {"no interface either way", instV6(true, "", "", peerWithIPs("a@x", "fd86::2/128"))},
  44. }
  45. for _, c := range cases {
  46. if got := desiredV6Aliases(c.inst); len(got) != 0 {
  47. t.Errorf("%s: desiredV6Aliases = %v, want empty", c.name, got)
  48. }
  49. }
  50. }
  51. func TestDesiredV6AliasesFallsBackToExternalInterface(t *testing.T) {
  52. inst := instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::2/128"))
  53. got := desiredV6Aliases(inst)
  54. if got["a@x"].Iface != "eth0" {
  55. t.Fatalf("expected fallback to ExternalInterface eth0, got %+v", got)
  56. }
  57. inst2 := instV6(true, "eth0", "eth1", peerWithIPs("a@x", "fd86::2/128"))
  58. got2 := desiredV6Aliases(inst2)
  59. if got2["a@x"].Iface != "eth1" {
  60. t.Fatalf("expected IPv6ExternalInterface eth1 to win over ExternalInterface, got %+v", got2)
  61. }
  62. }
  63. func TestDesiredV6AliasesSkipsPeersWithoutEmailOrV6Address(t *testing.T) {
  64. inst := instV6(true, "eth0", "",
  65. peerWithIPs("", "fd86::2/128"), // no email
  66. peerWithIPs("b@x", "10.8.1.2/32"), // v4 only, no v6
  67. peerWithIPs("c@x", "fd86::3/128"), // qualifies
  68. )
  69. got := desiredV6Aliases(inst)
  70. if len(got) != 1 {
  71. t.Fatalf("desiredV6Aliases = %+v, want exactly one entry (c@x)", got)
  72. }
  73. if _, ok := got["c@x"]; !ok {
  74. t.Fatalf("desiredV6Aliases = %+v, want c@x present", got)
  75. }
  76. }
  77. func TestDiffV6AliasesNoOpWhenUnchanged(t *testing.T) {
  78. inst := instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::2/128"))
  79. add, remove := diffV6Aliases(inst, inst)
  80. if len(add) != 0 || len(remove) != 0 {
  81. t.Fatalf("expected no-op for an unchanged instance, got add=%v remove=%v", add, remove)
  82. }
  83. }
  84. func TestDiffV6AliasesBrandNewInstanceIsAddOnly(t *testing.T) {
  85. newInst := instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::2/128"), peerWithIPs("b@x", "fd86::3/128"))
  86. add, remove := diffV6Aliases(amneziawg.Instance{}, newInst)
  87. if len(remove) != 0 {
  88. t.Fatalf("expected no removals for a brand new instance, got %v", remove)
  89. }
  90. if len(add) != 2 {
  91. t.Fatalf("expected both peers added, got %v", add)
  92. }
  93. }
  94. func TestDiffV6AliasesTornDownInstanceIsRemoveOnly(t *testing.T) {
  95. oldInst := instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::2/128"), peerWithIPs("b@x", "fd86::3/128"))
  96. add, remove := diffV6Aliases(oldInst, amneziawg.Instance{})
  97. if len(add) != 0 {
  98. t.Fatalf("expected no adds when tearing down, got %v", add)
  99. }
  100. if len(remove) != 2 {
  101. t.Fatalf("expected both peers removed, got %v", remove)
  102. }
  103. }
  104. func TestDiffV6AliasesIPv6EnabledToggledOffRemovesAllAddsNone(t *testing.T) {
  105. oldInst := instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::2/128"))
  106. newInst := instV6(false, "eth0", "", peerWithIPs("a@x", "fd86::2/128")) // same peers, feature disabled
  107. add, remove := diffV6Aliases(oldInst, newInst)
  108. if len(add) != 0 {
  109. t.Fatalf("expected no adds when IPv6Enabled is toggled off, got %v", add)
  110. }
  111. if len(remove) != 1 {
  112. t.Fatalf("expected the previously-aliased peer removed, got %v", remove)
  113. }
  114. }
  115. func TestDiffV6AliasesAddressChangeForSamePeerIsRemoveOldAddNew(t *testing.T) {
  116. oldInst := instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::2/128"))
  117. newInst := instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::99/128"))
  118. add, remove := diffV6Aliases(oldInst, newInst)
  119. if len(add) != 1 || add[0].Addr != "fd86::99" {
  120. t.Fatalf("expected new address added, got %v", add)
  121. }
  122. if len(remove) != 1 || remove[0].Addr != "fd86::2" {
  123. t.Fatalf("expected old address removed, got %v", remove)
  124. }
  125. }
  126. func TestDiffV6AliasesInterfaceChangeReAliasesUnchangedPeers(t *testing.T) {
  127. oldInst := instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::2/128"))
  128. newInst := instV6(true, "eth1", "", peerWithIPs("a@x", "fd86::2/128")) // same address, interface moved
  129. add, remove := diffV6Aliases(oldInst, newInst)
  130. if len(add) != 1 || add[0].Iface != "eth1" {
  131. t.Fatalf("expected re-add on the new interface, got %v", add)
  132. }
  133. if len(remove) != 1 || remove[0].Iface != "eth0" {
  134. t.Fatalf("expected removal from the old interface, got %v", remove)
  135. }
  136. }
  137. func TestDiffV6AliasesPeerRemovedFromInstanceIsRemoveOnly(t *testing.T) {
  138. oldInst := instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::2/128"), peerWithIPs("b@x", "fd86::3/128"))
  139. newInst := instV6(true, "eth0", "", peerWithIPs("a@x", "fd86::2/128")) // b@x removed
  140. add, remove := diffV6Aliases(oldInst, newInst)
  141. if len(add) != 0 {
  142. t.Fatalf("expected no adds, got %v", add)
  143. }
  144. if len(remove) != 1 || remove[0].Addr != "fd86::3" {
  145. t.Fatalf("expected only b@x's address removed, got %v", remove)
  146. }
  147. }
  148. // --- exec-layer tests: swap runIP, never invoke a real ip binary ---
  149. func withFakeRunIP(t *testing.T, fn func(ctx context.Context, args ...string) (string, error)) *[][]string {
  150. t.Helper()
  151. var calls [][]string
  152. orig := runIP
  153. runIP = func(ctx context.Context, args ...string) (string, error) {
  154. calls = append(calls, append([]string(nil), args...))
  155. return fn(ctx, args...)
  156. }
  157. t.Cleanup(func() { runIP = orig })
  158. return &calls
  159. }
  160. func TestAddV6AliasPassesExpectedArgs(t *testing.T) {
  161. calls := withFakeRunIP(t, func(ctx context.Context, args ...string) (string, error) {
  162. return "", nil
  163. })
  164. addV6Alias(v6Alias{Addr: "fd86::2", Iface: "eth0"})
  165. if len(*calls) != 1 {
  166. t.Fatalf("expected exactly one runIP call, got %d", len(*calls))
  167. }
  168. want := []string{"-6", "addr", "add", "fd86::2/128", "dev", "eth0", "nodad"}
  169. got := (*calls)[0]
  170. if len(got) != len(want) {
  171. t.Fatalf("args = %v, want %v", got, want)
  172. }
  173. for i := range want {
  174. if got[i] != want[i] {
  175. t.Fatalf("args = %v, want %v", got, want)
  176. }
  177. }
  178. }
  179. func TestAddV6AliasFileExistsIsSwallowed(t *testing.T) {
  180. withFakeRunIP(t, func(ctx context.Context, args ...string) (string, error) {
  181. return "RTNETLINK answers: File exists", errors.New("exit status 2")
  182. })
  183. // Must not panic and must return normally -- there is nothing else to
  184. // assert on since addV6Alias has no return value, matching this
  185. // codebase's existing best-effort exec-call conventions (no test in
  186. // this repo asserts on logger output for a swallowed vs. warned
  187. // classification; see internal/web/service/server.go's own untested
  188. // exec.CommandContext call sites).
  189. addV6Alias(v6Alias{Addr: "fd86::2", Iface: "eth0"})
  190. }
  191. func TestAddV6AliasOtherFailureDoesNotPanic(t *testing.T) {
  192. withFakeRunIP(t, func(ctx context.Context, args ...string) (string, error) {
  193. return "RTNETLINK answers: Cannot find device \"eth9\"", errors.New("exit status 1")
  194. })
  195. addV6Alias(v6Alias{Addr: "fd86::2", Iface: "eth9"})
  196. }
  197. func TestRemoveV6AliasPassesExpectedArgs(t *testing.T) {
  198. calls := withFakeRunIP(t, func(ctx context.Context, args ...string) (string, error) {
  199. return "", nil
  200. })
  201. removeV6Alias(v6Alias{Addr: "fd86::2", Iface: "eth0"})
  202. want := []string{"-6", "addr", "del", "fd86::2/128", "dev", "eth0"}
  203. got := (*calls)[0]
  204. if len(got) != len(want) {
  205. t.Fatalf("args = %v, want %v", got, want)
  206. }
  207. for i := range want {
  208. if got[i] != want[i] {
  209. t.Fatalf("args = %v, want %v", got, want)
  210. }
  211. }
  212. }
  213. func TestRemoveV6AliasAddressAlreadyGoneIsSwallowed(t *testing.T) {
  214. withFakeRunIP(t, func(ctx context.Context, args ...string) (string, error) {
  215. return "RTNETLINK answers: Cannot assign requested address", errors.New("exit status 2")
  216. })
  217. removeV6Alias(v6Alias{Addr: "fd86::2", Iface: "eth0"})
  218. }
  219. func TestRemoveV6AliasDeviceAlreadyGoneIsSwallowed(t *testing.T) {
  220. withFakeRunIP(t, func(ctx context.Context, args ...string) (string, error) {
  221. return "Cannot find device \"eth0\"", errors.New("exit status 1")
  222. })
  223. removeV6Alias(v6Alias{Addr: "fd86::2", Iface: "eth0"})
  224. }
  225. func TestRemoveV6AliasOtherFailureDoesNotPanic(t *testing.T) {
  226. withFakeRunIP(t, func(ctx context.Context, args ...string) (string, error) {
  227. return "some unrelated failure", errors.New("exit status 1")
  228. })
  229. removeV6Alias(v6Alias{Addr: "fd86::2", Iface: "eth0"})
  230. }
  231. func TestApplyV6AliasesAddsBeforeRemoves(t *testing.T) {
  232. var order []string
  233. calls := withFakeRunIP(t, func(ctx context.Context, args ...string) (string, error) {
  234. if args[2] == "add" {
  235. order = append(order, "add")
  236. } else {
  237. order = append(order, "del")
  238. }
  239. return "", nil
  240. })
  241. applyV6Aliases(
  242. []v6Alias{{Addr: "fd86::99", Iface: "eth0"}},
  243. []v6Alias{{Addr: "fd86::2", Iface: "eth0"}},
  244. )
  245. if len(*calls) != 2 {
  246. t.Fatalf("expected exactly 2 calls, got %d", len(*calls))
  247. }
  248. if order[0] != "add" || order[1] != "del" {
  249. t.Fatalf("expected add before del, got order=%v", order)
  250. }
  251. }