xray_amneziawg_outbound_test.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. package service
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
  6. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  7. wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  8. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  9. )
  10. func amneziawgnetEgressPortForTest() int { return amneziawgnet.EgressBasePort }
  11. func wgKeypairForTest() (priv, pub string, err error) {
  12. return wgutil.GenerateWireguardKeypair()
  13. }
  14. func makeAWGOutboundConfig(t *testing.T) *xray.Config {
  15. t.Helper()
  16. cfg := &xray.Config{}
  17. err := json.Unmarshal([]byte(`{
  18. "outbounds": [
  19. {"protocol": "freedom", "tag": "direct"},
  20. {"protocol": "amneziawg", "tag": "awg-hop", "settings": {"secretKey": "x"}}
  21. ]
  22. }`), cfg)
  23. if err != nil {
  24. t.Fatal(err)
  25. }
  26. return cfg
  27. }
  28. // The core folds the protocol id's case before resolving it, so a mixed-case
  29. // spelling must bridge here too or the raw pseudo-protocol reaches the core.
  30. func TestTransformAmneziaWGOutbounds_ReadsTheProtocolIDLikeTheCore(t *testing.T) {
  31. for _, protocol := range []string{"amneziawg", "AmneziaWG", "AMNEZIAWG"} {
  32. t.Run(protocol, func(t *testing.T) {
  33. cfg := &xray.Config{}
  34. raw := `{"outbounds":[
  35. {"protocol":"freedom","tag":"direct"},
  36. {"protocol":"` + protocol + `","tag":"awg-hop","settings":{"secretKey":"x"}}
  37. ]}`
  38. if err := json.Unmarshal([]byte(raw), cfg); err != nil {
  39. t.Fatal(err)
  40. }
  41. if err := transformAmneziaWGOutbounds(cfg); err != nil {
  42. t.Fatal(err)
  43. }
  44. var outbounds []struct {
  45. Protocol string `json:"protocol"`
  46. Tag string `json:"tag"`
  47. Settings struct {
  48. Address string `json:"address"`
  49. Port int `json:"port"`
  50. User string `json:"user"`
  51. } `json:"settings"`
  52. }
  53. if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
  54. t.Fatal(err)
  55. }
  56. if len(outbounds) != 2 {
  57. t.Fatalf("outbound count = %d, want 2 (no additions or drops)", len(outbounds))
  58. }
  59. got := outbounds[1]
  60. if got.Protocol != "socks" {
  61. t.Errorf("protocol = %q, want %q: the bridge never ran, so the raw pseudo-protocol reaches the core", got.Protocol, "socks")
  62. }
  63. if got.Tag != "awg-hop" {
  64. t.Errorf("tag = %q, want %q", got.Tag, "awg-hop")
  65. }
  66. if got.Settings.Address != "127.0.0.1" || got.Settings.Port != amneziawgnetEgressPortForTest() || got.Settings.User != "awg-hop" {
  67. t.Errorf("settings = %+v, want the socks bridge for tag %q on port %d", got.Settings, "awg-hop", amneziawgnetEgressPortForTest())
  68. }
  69. })
  70. }
  71. }
  72. func TestTransformAmneziaWGOutbounds(t *testing.T) {
  73. cfg := makeAWGOutboundConfig(t)
  74. if err := transformAmneziaWGOutbounds(cfg); err != nil {
  75. t.Fatal(err)
  76. }
  77. var outbounds []struct {
  78. Protocol string `json:"protocol"`
  79. Tag string `json:"tag"`
  80. Settings struct {
  81. Address string `json:"address"`
  82. Port int `json:"port"`
  83. User string `json:"user"`
  84. Pass string `json:"pass"`
  85. } `json:"settings"`
  86. }
  87. if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
  88. t.Fatal(err)
  89. }
  90. if len(outbounds) != 2 {
  91. t.Fatalf("outbound count = %d, want 2 (no additions or drops)", len(outbounds))
  92. }
  93. if outbounds[0].Protocol != "freedom" || outbounds[0].Tag != "direct" {
  94. t.Fatalf("first outbound disturbed: %+v", outbounds[0])
  95. }
  96. got := outbounds[1]
  97. if got.Protocol != "socks" {
  98. t.Fatalf("amneziawg outbound not swapped to socks: %q", got.Protocol)
  99. }
  100. if got.Tag != "awg-hop" {
  101. t.Fatalf("tag not preserved: %q", got.Tag)
  102. }
  103. if got.Settings.Address != "127.0.0.1" {
  104. t.Fatalf("bridge address = %q, want 127.0.0.1", got.Settings.Address)
  105. }
  106. if got.Settings.Port != amneziawgnetEgressPortForTest() {
  107. t.Fatalf("bridge port = %d", got.Settings.Port)
  108. }
  109. if got.Settings.User != "awg-hop" {
  110. t.Fatalf("SOCKS username = %q, want the outbound tag", got.Settings.User)
  111. }
  112. if got.Settings.Pass == "" {
  113. t.Fatal("SOCKS password must be set (egress server enforces it)")
  114. }
  115. }
  116. func TestTransformAmneziaWGOutbounds_NoopWithoutAWG(t *testing.T) {
  117. before := &xray.Config{}
  118. if err := json.Unmarshal([]byte(`{"outbounds":[{"protocol":"freedom","tag":"direct"}]}`), before); err != nil {
  119. t.Fatal(err)
  120. }
  121. cfg := &xray.Config{}
  122. if err := json.Unmarshal(before.OutboundConfigs, &cfg.OutboundConfigs); err != nil {
  123. t.Fatal(err)
  124. }
  125. orig := json_util.RawMessage(append([]byte(nil), cfg.OutboundConfigs...))
  126. if err := transformAmneziaWGOutbounds(cfg); err != nil {
  127. t.Fatal(err)
  128. }
  129. if string(cfg.OutboundConfigs) != string(orig) {
  130. t.Fatalf("config without amneziawg outbounds must stay byte-identical:\nbefore=%s\nafter=%s", orig, cfg.OutboundConfigs)
  131. }
  132. }
  133. func TestCheckXrayConfig_AcceptsValidAWGOutbound(t *testing.T) {
  134. // A syntactically valid AWG outbound must pass panel-side validation --
  135. // the Xray-core loader would reject the unknown protocol outright.
  136. priv, pub, err := wgKeypairForTest()
  137. if err != nil {
  138. t.Fatal(err)
  139. }
  140. template := `{
  141. "outbounds": [{
  142. "protocol": "amneziawg",
  143. "tag": "awg-hop",
  144. "settings": {
  145. "mtu": 1420,
  146. "secretKey": "` + priv + `",
  147. "address": ["10.8.0.2/32"],
  148. "jc": 4, "jmin": 40, "jmax": 100, "s1": 15, "s2": 80, "s3": 12, "s4": 12,
  149. "h1": "100-800", "h2": "900-1600", "h3": "1700-2400", "h4": "2500-3200",
  150. "peers": [{
  151. "publicKey": "` + pub + `",
  152. "allowedIPs": ["0.0.0.0/0"],
  153. "endpoint": "203.0.113.7:51820",
  154. "keepAlive": 25
  155. }]
  156. }
  157. }]
  158. }`
  159. svc := &XraySettingService{}
  160. if err := svc.CheckXrayConfig(template); err != nil {
  161. t.Fatalf("valid amneziawg outbound rejected: %v", err)
  162. }
  163. }
  164. func TestCheckXrayConfig_RejectsBrokenAWGOutbound(t *testing.T) {
  165. // The emptied field's partner must be a real key, or the case is decided
  166. // by that partner and stays green with the empty-key guard removed.
  167. priv, pub, err := wgKeypairForTest()
  168. if err != nil {
  169. t.Fatal(err)
  170. }
  171. cases := []struct {
  172. name string
  173. template string
  174. }{
  175. {
  176. name: "not a key",
  177. template: `{
  178. "outbounds": [{
  179. "protocol": "amneziawg",
  180. "tag": "awg-bad",
  181. "settings": {
  182. "secretKey": "not-a-key",
  183. "address": ["10.8.0.2/32"],
  184. "peers": [{"publicKey": "alsobad", "allowedIPs": ["0.0.0.0/0"], "endpoint": "203.0.113.7:51820"}]
  185. }
  186. }]
  187. }`,
  188. },
  189. {
  190. name: "empty secretKey",
  191. template: `{
  192. "outbounds": [{
  193. "protocol": "amneziawg",
  194. "tag": "awg-empty-sec",
  195. "settings": {
  196. "secretKey": "",
  197. "address": ["10.8.0.2/32"],
  198. "peers": [{"publicKey": "` + pub + `", "allowedIPs": ["0.0.0.0/0"], "endpoint": "203.0.113.7:51820"}]
  199. }
  200. }]
  201. }`,
  202. },
  203. {
  204. name: "empty peer publicKey",
  205. template: `{
  206. "outbounds": [{
  207. "protocol": "amneziawg",
  208. "tag": "awg-empty-pub",
  209. "settings": {
  210. "secretKey": "` + priv + `",
  211. "address": ["10.8.0.2/32"],
  212. "peers": [{"publicKey": "", "allowedIPs": ["0.0.0.0/0"], "endpoint": "203.0.113.7:51820"}]
  213. }
  214. }]
  215. }`,
  216. },
  217. }
  218. svc := &XraySettingService{}
  219. for _, tc := range cases {
  220. t.Run(tc.name, func(t *testing.T) {
  221. if err := svc.CheckXrayConfig(tc.template); err == nil {
  222. t.Fatalf("%s: expected error, got nil", tc.name)
  223. }
  224. })
  225. }
  226. }
  227. func TestTransformAmneziaWGOutbounds_PreservesSiblingKeys(t *testing.T) {
  228. cfg := &xray.Config{}
  229. err := json.Unmarshal([]byte(`{
  230. "outbounds": [
  231. {"protocol": "amneziawg", "tag": "awg-hop", "sendThrough": "0.0.0.0",
  232. "targetStrategy": "UseIPv4",
  233. "mux": {"enabled": false},
  234. "streamSettings": {"sockopt": {"tcpFastOpen": true}},
  235. "settings": {"secretKey": "x"}}
  236. ]
  237. }`), cfg)
  238. if err != nil {
  239. t.Fatal(err)
  240. }
  241. if err := transformAmneziaWGOutbounds(cfg); err != nil {
  242. t.Fatal(err)
  243. }
  244. var outbounds []struct {
  245. Protocol string `json:"protocol"`
  246. Tag string `json:"tag"`
  247. SendThrough string `json:"sendThrough"`
  248. TargetStrategy string `json:"targetStrategy"`
  249. Mux map[string]any `json:"mux"`
  250. StreamSettings map[string]any `json:"streamSettings"`
  251. }
  252. if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
  253. t.Fatal(err)
  254. }
  255. if len(outbounds) != 1 {
  256. t.Fatalf("outbound count = %d, want 1", len(outbounds))
  257. }
  258. got := outbounds[0]
  259. if got.SendThrough != "0.0.0.0" {
  260. t.Fatalf("sendThrough dropped: %q", got.SendThrough)
  261. }
  262. if got.TargetStrategy != "UseIPv4" {
  263. t.Fatalf("targetStrategy dropped: %q", got.TargetStrategy)
  264. }
  265. if got.Mux == nil {
  266. t.Fatal("mux dropped")
  267. }
  268. if got.StreamSettings == nil {
  269. t.Fatal("streamSettings.sockopt dropped")
  270. }
  271. }
  272. func TestTransformAmneziaWGOutbounds_EmptyTagIsAnError(t *testing.T) {
  273. cfg := &xray.Config{}
  274. if err := json.Unmarshal([]byte(`{
  275. "outbounds": [
  276. {"protocol": "freedom", "tag": "direct"},
  277. {"protocol": "amneziawg", "tag": "", "settings": {"secretKey": "x"}}
  278. ]
  279. }`), cfg); err != nil {
  280. t.Fatal(err)
  281. }
  282. if err := transformAmneziaWGOutbounds(cfg); err == nil {
  283. t.Fatal("empty-tag amneziawg outbound must fail config generation, not silently pass through")
  284. }
  285. }
  286. func TestCheckXrayConfig_RejectsEmptyTagAWGOutbound(t *testing.T) {
  287. priv, pub, err := wgKeypairForTest()
  288. if err != nil {
  289. t.Fatal(err)
  290. }
  291. template := `{
  292. "outbounds": [{
  293. "protocol": "amneziawg",
  294. "tag": "",
  295. "settings": {
  296. "secretKey": "` + priv + `",
  297. "address": ["10.8.0.2/32"],
  298. "peers": [{"publicKey": "` + pub + `", "allowedIPs": ["0.0.0.0/0"], "endpoint": "203.0.113.7:51820"}]
  299. }
  300. }]
  301. }`
  302. svc := &XraySettingService{}
  303. if err := svc.CheckXrayConfig(template); err == nil {
  304. t.Fatal("empty-tag amneziawg outbound accepted by CheckXrayConfig")
  305. }
  306. }
  307. func TestCheckXrayConfig_RejectsNonStringTagAWGOutbound(t *testing.T) {
  308. template := `{
  309. "outbounds": [{
  310. "protocol": "amneziawg",
  311. "tag": 123,
  312. "settings": {"secretKey": "x"}
  313. }]
  314. }`
  315. svc := &XraySettingService{}
  316. if err := svc.CheckXrayConfig(template); err == nil {
  317. t.Fatal("non-string tag amneziawg outbound accepted by CheckXrayConfig")
  318. }
  319. }