outbound_test.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. package amneziawg
  2. import (
  3. "encoding/json"
  4. "testing"
  5. wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  6. )
  7. // validOutboundJSON is a fully valid amneziawg outbound settings payload.
  8. func validOutboundJSON(t *testing.T) []byte {
  9. t.Helper()
  10. raw := map[string]any{
  11. "mtu": 1420,
  12. "secretKey": validPrivKey(t),
  13. "address": []string{"10.8.0.2/32"},
  14. "jc": 4,
  15. "jmin": 40,
  16. "jmax": 100,
  17. "s1": 15,
  18. "s2": 80,
  19. "s3": 12,
  20. "s4": 12,
  21. "h1": "100-800",
  22. "h2": "900-1600",
  23. "h3": "1700-2400",
  24. "h4": "2500-3200",
  25. "peers": []map[string]any{{
  26. "publicKey": validPubKey(t),
  27. "allowedIPs": []string{"0.0.0.0/0", "::/0"},
  28. "endpoint": "203.0.113.7:51820",
  29. "keepAlive": 25,
  30. }},
  31. }
  32. bs, err := json.Marshal(raw)
  33. if err != nil {
  34. t.Fatal(err)
  35. }
  36. return bs
  37. }
  38. func validPubKey(t *testing.T) string {
  39. t.Helper()
  40. _, pub, err := wgutil.GenerateWireguardKeypair()
  41. if err != nil {
  42. t.Fatal(err)
  43. }
  44. return pub
  45. }
  46. func validPrivKey(t *testing.T) string {
  47. t.Helper()
  48. priv, _, err := wgutil.GenerateWireguardKeypair()
  49. if err != nil {
  50. t.Fatal(err)
  51. }
  52. return priv
  53. }
  54. func TestInstanceFromOutbound_OK(t *testing.T) {
  55. wrapped, err := json.Marshal(map[string]any{
  56. "protocol": "amneziawg",
  57. "tag": "awg-out-test",
  58. "settings": json.RawMessage(validOutboundJSON(t)),
  59. })
  60. if err != nil {
  61. t.Fatal(err)
  62. }
  63. inst, ok := InstanceFromOutbound("awg-out-test", wrapped)
  64. if !ok {
  65. t.Fatal("InstanceFromOutbound returned false for a valid outbound")
  66. }
  67. if inst.Tag != "awg-out-test" {
  68. t.Fatalf("Tag = %q, want awg-out-test", inst.Tag)
  69. }
  70. if len(inst.Peers) != 1 {
  71. t.Fatalf("len(Peers) = %d, want 1", len(inst.Peers))
  72. }
  73. p := inst.Peers[0]
  74. if p.Endpoint != "203.0.113.7:51820" {
  75. t.Fatalf("Endpoint = %q", p.Endpoint)
  76. }
  77. if p.KeepAlive != 25 {
  78. t.Fatalf("KeepAlive = %d, want 25", p.KeepAlive)
  79. }
  80. if len(p.AllowedIPs) != 2 {
  81. t.Fatalf("AllowedIPs = %v", p.AllowedIPs)
  82. }
  83. if inst.MTU != 1420 {
  84. t.Fatalf("MTU = %d, want 1420", inst.MTU)
  85. }
  86. if inst.Obfuscation.Jc != 4 || inst.Obfuscation.S1 != 15 {
  87. t.Fatalf("Obfuscation not carried: %+v", inst.Obfuscation)
  88. }
  89. }
  90. func TestInstanceFromOutbound_RejectsIncompletePeer(t *testing.T) {
  91. m := validOutboundMapT(t)
  92. m["address"] = []any{}
  93. bs, _ := json.Marshal(m)
  94. wrapped, _ := json.Marshal(map[string]any{"protocol": "amneziawg", "settings": json.RawMessage(bs)})
  95. if _, ok := InstanceFromOutbound("t", wrapped); ok {
  96. t.Fatal("expected false when address list is empty")
  97. }
  98. m2 := validOutboundMapT(t)
  99. m2["peers"].([]any)[0].(map[string]any)["endpoint"] = ""
  100. bs2, _ := json.Marshal(m2)
  101. wrapped2, _ := json.Marshal(map[string]any{"protocol": "amneziawg", "settings": json.RawMessage(bs2)})
  102. // The only peer is incomplete -> skipped -> zero usable peers -> false,
  103. // mirroring InstanceFromInbound's "nothing to serve" contract.
  104. if _, ok := InstanceFromOutbound("t", wrapped2); ok {
  105. t.Fatal("outbound whose only peer lacks an endpoint must be unusable")
  106. }
  107. // With a second, complete peer the instance stays usable and only the
  108. // broken entry disappears.
  109. m3 := validOutboundMapT(t)
  110. brokenPeer := validOutboundMapT(t)["peers"].([]any)[0].(map[string]any)
  111. brokenPeer["endpoint"] = ""
  112. m3["peers"] = []any{brokenPeer, validOutboundMapT(t)["peers"].([]any)[0]}
  113. bs3, _ := json.Marshal(m3)
  114. wrapped3, _ := json.Marshal(map[string]any{"protocol": "amneziawg", "settings": json.RawMessage(bs3)})
  115. inst, ok := InstanceFromOutbound("t", wrapped3)
  116. if !ok {
  117. t.Fatal("one good peer should keep the outbound usable")
  118. }
  119. if len(inst.Peers) != 1 {
  120. t.Fatalf("broken peer must be dropped; got %d peers", len(inst.Peers))
  121. }
  122. }
  123. func TestValidateAmneziaWGOutbound_AcceptsValidAndRejectsBroken(t *testing.T) {
  124. if err := ValidateAmneziaWGOutbound("t", wrapOutboundSettings(validOutboundJSON(t))); err != nil {
  125. t.Fatalf("valid outbound rejected: %v", err)
  126. }
  127. cases := []struct {
  128. name string
  129. breakF func(m map[string]any)
  130. }{
  131. {"empty secretKey", func(m map[string]any) { m["secretKey"] = "" }},
  132. {"empty peer publicKey", func(m map[string]any) { peer(m)["publicKey"] = "" }},
  133. {"whitespace secretKey", func(m map[string]any) { m["secretKey"] = " " }},
  134. {"whitespace peer publicKey", func(m map[string]any) { peer(m)["publicKey"] = " " }},
  135. {"bad endpoint no port", func(m map[string]any) { peer(m)["endpoint"] = "203.0.113.7" }},
  136. {"endpoint control char", func(m map[string]any) { peer(m)["endpoint"] = "host:51820\nPostUp=x" }},
  137. {"empty allowedIPs", func(m map[string]any) { peer(m)["allowedIPs"] = []string{} }},
  138. {"no peers", func(m map[string]any) { m["peers"] = []any{} }},
  139. {"bad allowedIP", func(m map[string]any) { peer(m)["allowedIPs"] = []string{"not-a-prefix"} }},
  140. {"no address", func(m map[string]any) { m["address"] = []any{} }},
  141. {"bad jc/jmin order", func(m map[string]any) { m["jmin"] = 200; m["jmax"] = 100 }},
  142. }
  143. for _, tc := range cases {
  144. t.Run(tc.name, func(t *testing.T) {
  145. m := validOutboundMapT(t)
  146. tc.breakF(m)
  147. bs, _ := json.Marshal(m)
  148. if err := ValidateAmneziaWGOutbound("t", wrapOutboundSettings(bs)); err == nil {
  149. t.Fatalf("%s: expected error, got nil", tc.name)
  150. }
  151. })
  152. }
  153. }
  154. // wrapOutboundSettings embeds a settings payload the way the template stores
  155. // it: as the nested "settings" of an amneziawg outbound row.
  156. func wrapOutboundSettings(settings json.RawMessage) []byte {
  157. bs, err := json.Marshal(map[string]any{
  158. "protocol": "amneziawg",
  159. "tag": "t",
  160. "settings": settings,
  161. })
  162. if err != nil {
  163. panic(err)
  164. }
  165. return bs
  166. }
  167. func peer(m map[string]any) map[string]any {
  168. return m["peers"].([]any)[0].(map[string]any)
  169. }
  170. func validOutboundMapT(t *testing.T) map[string]any {
  171. t.Helper()
  172. var m map[string]any
  173. if err := json.Unmarshal(validOutboundJSON(t), &m); err != nil {
  174. t.Fatal(err)
  175. }
  176. return m
  177. }
  178. func TestIsAmneziaWGOutbound(t *testing.T) {
  179. yes := []byte(`{"protocol":"amneziawg","tag":"x"}`)
  180. if !IsAmneziaWGOutbound(yes) {
  181. t.Fatal("amneziawg protocol not detected")
  182. }
  183. no := []byte(`{"protocol":"freedom","tag":"x"}`)
  184. if IsAmneziaWGOutbound(no) {
  185. t.Fatal("freedom misdetected as amneziawg")
  186. }
  187. if IsAmneziaWGOutbound([]byte(`{broken`)) {
  188. t.Fatal("garbage misdetected as amneziawg")
  189. }
  190. }
  191. // A blank line terminates IpcSetOperation, silently truncating the peer set;
  192. // validation rejects a trailing newline, parsing normalizes it away.
  193. func TestValidateAmneziaWGOutbound_RejectsAllowedIPWithNewline(t *testing.T) {
  194. m := validOutboundMapT(t)
  195. peer(m)["allowedIPs"] = []string{"0.0.0.0/0\n", "::/0"}
  196. bs, err := json.Marshal(m)
  197. if err != nil {
  198. t.Fatal(err)
  199. }
  200. if err := ValidateAmneziaWGOutbound("t", wrapOutboundSettings(bs)); err == nil {
  201. t.Fatal("allowedIP with trailing newline: expected error, got nil")
  202. }
  203. }
  204. func TestInstanceFromOutbound_NormalizesAllowedIPs(t *testing.T) {
  205. m := validOutboundMapT(t)
  206. peer(m)["allowedIPs"] = []string{" 0.0.0.0/0\n", "::/0"}
  207. bs, err := json.Marshal(m)
  208. if err != nil {
  209. t.Fatal(err)
  210. }
  211. inst, ok := InstanceFromOutbound("t", wrapOutboundSettings(bs))
  212. if !ok {
  213. t.Fatal("InstanceFromOutbound returned false for trimmable allowedIPs")
  214. }
  215. got := inst.Peers[0].AllowedIPs
  216. want := []string{"0.0.0.0/0", "::/0"}
  217. if len(got) != len(want) {
  218. t.Fatalf("AllowedIPs = %v, want %v", got, want)
  219. }
  220. for i := range want {
  221. if got[i] != want[i] {
  222. t.Fatalf("AllowedIPs[%d] = %q, want %q (newline must not survive)", i, got[i], want[i])
  223. }
  224. }
  225. }
  226. func TestInstanceFromOutbound_RejectsUnparseableAllowedIP(t *testing.T) {
  227. m := validOutboundMapT(t)
  228. peer(m)["allowedIPs"] = []string{"not-a-prefix"}
  229. bs, err := json.Marshal(m)
  230. if err != nil {
  231. t.Fatal(err)
  232. }
  233. if _, ok := InstanceFromOutbound("t", wrapOutboundSettings(bs)); ok {
  234. t.Fatal("unparseable allowedIP must make InstanceFromOutbound return false")
  235. }
  236. }
  237. func TestValidateAmneziaWGOutbound_RejectsControlCharInIParams(t *testing.T) {
  238. for _, field := range []string{"i1", "i2", "i3", "i4", "i5"} {
  239. t.Run(field, func(t *testing.T) {
  240. m := validOutboundMapT(t)
  241. m[field] = "<r 64>\nPostUp=x"
  242. bs, err := json.Marshal(m)
  243. if err != nil {
  244. t.Fatal(err)
  245. }
  246. if err := ValidateAmneziaWGOutbound("t", wrapOutboundSettings(bs)); err == nil {
  247. t.Fatalf("%s with embedded newline: expected error, got nil", field)
  248. }
  249. })
  250. }
  251. }
  252. func TestValidateAmneziaWGOutbound_RejectsEmptyTag(t *testing.T) {
  253. raw := []byte(`{"protocol":"amneziawg","tag":"","settings":{"secretKey":"x"}}`)
  254. for _, tag := range []string{"", " "} {
  255. if err := ValidateAmneziaWGOutbound(tag, raw); err == nil {
  256. t.Fatalf("tag %q accepted", tag)
  257. }
  258. }
  259. }
  260. func TestValidateAmneziaWGOutbound_DNSField(t *testing.T) {
  261. valid := map[string]string{
  262. "": "",
  263. "1.1.1.1": "1.1.1.1:53",
  264. "8.8.8.8:53": "8.8.8.8:53",
  265. "2606:4700:4700::1111": "[2606:4700:4700::1111]:53",
  266. "[2606:4700:4700::1111]:53": "[2606:4700:4700::1111]:53",
  267. }
  268. for d, expected := range valid {
  269. m := validOutboundMapT(t)
  270. if d != "" {
  271. m["dns"] = d
  272. }
  273. bs, _ := json.Marshal(m)
  274. if err := ValidateAmneziaWGOutbound("t", wrapOutboundSettings(bs)); err != nil {
  275. t.Fatalf("valid dns %q rejected: %v", d, err)
  276. }
  277. inst, ok := InstanceFromOutbound("t", wrapOutboundSettings(bs))
  278. if !ok || inst.DNS != expected {
  279. t.Fatalf("InstanceFromOutbound dns=%q, want %q", inst.DNS, expected)
  280. }
  281. }
  282. invalid := []string{"not-an-ip", "1.1.1.1\nPostUp=x", "999.999.999.999"}
  283. for _, d := range invalid {
  284. m := validOutboundMapT(t)
  285. m["dns"] = d
  286. bs, _ := json.Marshal(m)
  287. if err := ValidateAmneziaWGOutbound("t", wrapOutboundSettings(bs)); err == nil {
  288. t.Fatalf("invalid dns %q accepted", d)
  289. }
  290. }
  291. }