xray_amneziawg_outbound_test.go 8.0 KB

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