xray_wireguard_config_test.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. package service
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  9. )
  10. func wgTestSecretKey() string {
  11. return base64.StdEncoding.EncodeToString(make([]byte, 32))
  12. }
  13. func wgInboundEmittedSettings(t *testing.T, tag string) map[string]any {
  14. t.Helper()
  15. svc := &XrayService{}
  16. cfg, err := svc.GetXrayConfig()
  17. if err != nil {
  18. t.Fatalf("GetXrayConfig: %v", err)
  19. }
  20. for i := range cfg.InboundConfigs {
  21. ic := cfg.InboundConfigs[i]
  22. if ic.Tag != tag {
  23. continue
  24. }
  25. var s map[string]any
  26. if err := json.Unmarshal([]byte(ic.Settings), &s); err != nil {
  27. t.Fatalf("unmarshal emitted settings: %v", err)
  28. }
  29. return s
  30. }
  31. t.Fatalf("inbound %q not found in generated config", tag)
  32. return nil
  33. }
  34. func seedWGInbound(t *testing.T, tag string, port int, clients []model.Client) {
  35. t.Helper()
  36. setupSettingTestDB(t)
  37. db := database.GetDB()
  38. in := &model.Inbound{
  39. Tag: tag,
  40. Enable: true,
  41. Port: port,
  42. Protocol: model.WireGuard,
  43. Settings: `{"secretKey":"` + wgTestSecretKey() + `","mtu":1420}`,
  44. }
  45. if err := db.Create(in).Error; err != nil {
  46. t.Fatalf("create wg inbound: %v", err)
  47. }
  48. svc := ClientService{}
  49. if err := svc.SyncInbound(nil, in.Id, clients); err != nil {
  50. t.Fatalf("SyncInbound: %v", err)
  51. }
  52. }
  53. func seedDualTunnelClient(t *testing.T, enabled bool, wgKeepAlive *int) string {
  54. t.Helper()
  55. setupSettingTestDB(t)
  56. db := database.GetDB()
  57. const email = "[email protected]"
  58. wgClient := model.Client{
  59. Email: email,
  60. Enable: true,
  61. PublicKey: "pub-wg",
  62. AllowedIPs: []string{"10.0.0.5/32"},
  63. PreSharedKey: "wg-psk",
  64. KeepAlive: wgKeepAlive,
  65. }
  66. awgClient := wgClient
  67. awgClient.PublicKey = "pub-awg"
  68. awgClient.AllowedIPs = []string{"10.8.1.5/32"}
  69. awgClient.PreSharedKey = "awg-psk"
  70. awgClient.KeepAlive = model.KeepAlivePtr(25)
  71. wgSettings, err := json.Marshal(map[string]any{
  72. "secretKey": wgTestSecretKey(),
  73. "mtu": 1420,
  74. "clients": []model.Client{wgClient},
  75. })
  76. if err != nil {
  77. t.Fatalf("marshal wg settings: %v", err)
  78. }
  79. awgSettings, err := json.Marshal(map[string]any{
  80. "server": map[string]any{"subnetIp": "10.8.1.0", "subnetCidr": 24},
  81. "clients": []model.Client{awgClient},
  82. })
  83. if err != nil {
  84. t.Fatalf("marshal awg settings: %v", err)
  85. }
  86. wgInbound := &model.Inbound{Tag: "wg-dual", Enable: true, Port: 51823, Protocol: model.WireGuard, Settings: string(wgSettings)}
  87. awgInbound := &model.Inbound{Tag: "awg-dual", Enable: true, Port: 51824, Protocol: model.AmneziaWG, Settings: string(awgSettings)}
  88. if err := db.Create(wgInbound).Error; err != nil {
  89. t.Fatalf("create wg inbound: %v", err)
  90. }
  91. if err := db.Create(awgInbound).Error; err != nil {
  92. t.Fatalf("create awg inbound: %v", err)
  93. }
  94. svc := ClientService{}
  95. if err := svc.SyncInbound(nil, wgInbound.Id, []model.Client{wgClient}); err != nil {
  96. t.Fatalf("SyncInbound(wg): %v", err)
  97. }
  98. awgClient.Enable = enabled
  99. if err := svc.SyncInbound(nil, awgInbound.Id, []model.Client{awgClient}); err != nil {
  100. t.Fatalf("SyncInbound(awg): %v", err)
  101. }
  102. return email
  103. }
  104. func wgPeerList(t *testing.T, settings map[string]any) []map[string]any {
  105. t.Helper()
  106. if _, ok := settings["clients"]; ok {
  107. t.Fatalf("wireguard inbound must not emit a clients[] key: %v", settings["clients"])
  108. }
  109. rawPeers, ok := settings["peers"].([]any)
  110. if !ok {
  111. t.Fatalf("settings.peers is not an array: %T", settings["peers"])
  112. }
  113. out := make([]map[string]any, 0, len(rawPeers))
  114. for _, p := range rawPeers {
  115. m, ok := p.(map[string]any)
  116. if !ok {
  117. t.Fatalf("peer is not an object: %T", p)
  118. }
  119. out = append(out, m)
  120. }
  121. return out
  122. }
  123. func TestGetXrayConfigWireGuardPeers(t *testing.T) {
  124. clients := []model.Client{
  125. {Email: "[email protected]", Enable: true, PublicKey: "pub-alice", AllowedIPs: []string{"10.0.0.2/32"}, KeepAlive: model.KeepAlivePtr(25)},
  126. {Email: "[email protected]", Enable: true, PublicKey: "pub-bob", AllowedIPs: []string{"10.0.0.3/32"}},
  127. }
  128. seedWGInbound(t, "wg-multi", 51820, clients)
  129. settings := wgInboundEmittedSettings(t, "wg-multi")
  130. if settings["secretKey"] != wgTestSecretKey() {
  131. t.Errorf("secretKey not preserved: %v", settings["secretKey"])
  132. }
  133. if settings["mtu"] != float64(1420) {
  134. t.Errorf("mtu not preserved: %v", settings["mtu"])
  135. }
  136. peers := wgPeerList(t, settings)
  137. if len(peers) != 2 {
  138. t.Fatalf("expected 2 peers, got %d: %v", len(peers), peers)
  139. }
  140. ips := map[string]bool{}
  141. for _, p := range peers {
  142. if p["email"] == nil || p["email"] == "" {
  143. t.Errorf("peer missing email: %v", p)
  144. }
  145. if p["publicKey"] == nil || p["publicKey"] == "" {
  146. t.Errorf("peer missing publicKey: %v", p)
  147. }
  148. if p["level"] != float64(0) {
  149. t.Errorf("peer level = %v, want 0 (needed for per-user stats)", p["level"])
  150. }
  151. allowed, ok := p["allowedIPs"].([]any)
  152. if !ok || len(allowed) == 0 {
  153. t.Fatalf("peer missing allowedIPs: %v", p)
  154. }
  155. ips[allowed[0].(string)] = true
  156. }
  157. if len(ips) != 2 {
  158. t.Errorf("peers must have distinct allowedIPs, got %v", ips)
  159. }
  160. }
  161. func TestGetXrayConfigWireGuardDisabledClientExcluded(t *testing.T) {
  162. clients := []model.Client{
  163. {Email: "[email protected]", Enable: true, PublicKey: "pub-on", AllowedIPs: []string{"10.0.0.2/32"}},
  164. {Email: "[email protected]", Enable: true, PublicKey: "pub-off", AllowedIPs: []string{"10.0.0.3/32"}},
  165. }
  166. seedWGInbound(t, "wg-disabled", 51821, clients)
  167. if err := database.GetDB().Model(&model.ClientRecord{}).
  168. Where("email = ?", "[email protected]").Update("enable", false).Error; err != nil {
  169. t.Fatalf("disable client: %v", err)
  170. }
  171. peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-disabled"))
  172. if len(peers) != 1 {
  173. t.Fatalf("expected 1 enabled peer, got %d: %v", len(peers), peers)
  174. }
  175. if peers[0]["email"] != "[email protected]" {
  176. t.Errorf("wrong peer kept: %v", peers[0])
  177. }
  178. }
  179. func TestGetXrayConfigWireGuardUsesInboundLocalTunnelFields(t *testing.T) {
  180. email := seedDualTunnelClient(t, true, model.KeepAlivePtr(15))
  181. var shared model.ClientRecord
  182. if err := database.GetDB().Where("email = ?", email).First(&shared).Error; err != nil {
  183. t.Fatalf("read shared client: %v", err)
  184. }
  185. if shared.AllowedIPs != "10.8.1.5/32" || shared.PreSharedKey != "awg-psk" || shared.PublicKey != "pub-awg" || shared.KeepAlive != 25 {
  186. t.Fatalf("test setup did not persist AmneziaWG last: allowedIPs=%q preSharedKey=%q publicKey=%q keepAlive=%d", shared.AllowedIPs, shared.PreSharedKey, shared.PublicKey, shared.KeepAlive)
  187. }
  188. peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-dual"))
  189. if len(peers) != 1 {
  190. t.Fatalf("expected 1 peer, got %d: %v", len(peers), peers)
  191. }
  192. allowed, ok := peers[0]["allowedIPs"].([]any)
  193. if !ok || len(allowed) != 1 || allowed[0] != "10.0.0.5/32" {
  194. t.Fatalf("WireGuard peer allowedIPs = %v, want [10.0.0.5/32]", peers[0]["allowedIPs"])
  195. }
  196. if peers[0]["preSharedKey"] != "wg-psk" {
  197. t.Fatalf("WireGuard peer preSharedKey = %v, want wg-psk", peers[0]["preSharedKey"])
  198. }
  199. if peers[0]["publicKey"] != "pub-wg" {
  200. t.Fatalf("WireGuard peer publicKey = %v, want pub-wg", peers[0]["publicKey"])
  201. }
  202. if peers[0]["keepAlive"] != float64(15) {
  203. t.Fatalf("WireGuard peer keepAlive = %v, want 15", peers[0]["keepAlive"])
  204. }
  205. }
  206. func TestGetXrayConfigWireGuardOmitsKeepAliveAbsentFromSettings(t *testing.T) {
  207. email := seedDualTunnelClient(t, true, nil)
  208. var shared model.ClientRecord
  209. if err := database.GetDB().Where("email = ?", email).First(&shared).Error; err != nil {
  210. t.Fatalf("read shared client: %v", err)
  211. }
  212. if shared.PublicKey != "pub-awg" || shared.KeepAlive != 25 {
  213. t.Fatalf("test setup did not persist AmneziaWG identity: publicKey=%q keepAlive=%d", shared.PublicKey, shared.KeepAlive)
  214. }
  215. peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-dual"))
  216. if len(peers) != 1 {
  217. t.Fatalf("expected 1 peer, got %d: %v", len(peers), peers)
  218. }
  219. if peers[0]["publicKey"] != "pub-wg" {
  220. t.Fatalf("WireGuard peer publicKey = %v, want pub-wg", peers[0]["publicKey"])
  221. }
  222. if _, ok := peers[0]["keepAlive"]; ok {
  223. t.Fatalf("WireGuard peer keepAlive = %v, want absent", peers[0]["keepAlive"])
  224. }
  225. }
  226. func TestGetXrayConfigWireGuardDisabledDualProtocolClientExcluded(t *testing.T) {
  227. seedDualTunnelClient(t, false, model.KeepAlivePtr(15))
  228. peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-dual"))
  229. if len(peers) != 0 {
  230. t.Fatalf("expected disabled dual-protocol client to be excluded, got %v", peers)
  231. }
  232. }
  233. func TestGetXrayConfigWireGuardNoClientsEmitsEmptyPeers(t *testing.T) {
  234. seedWGInbound(t, "wg-empty", 51822, nil)
  235. settings := wgInboundEmittedSettings(t, "wg-empty")
  236. if _, ok := settings["clients"]; ok {
  237. t.Fatalf("clients key must be absent")
  238. }
  239. peers, ok := settings["peers"].([]any)
  240. if !ok {
  241. t.Fatalf("peers must be an (empty) array, got %T", settings["peers"])
  242. }
  243. if len(peers) != 0 {
  244. t.Fatalf("expected empty peers, got %v", peers)
  245. }
  246. }
  247. // #6796: where the core would pick a kernel TUN it cannot set up (Docker, a sandboxed unit),
  248. // one WireGuard outbound aborts the whole start unless it is moved to the userspace TUN.
  249. func TestForceUserspaceWireGuardTun(t *testing.T) {
  250. cases := []struct {
  251. name, protocol, settings string
  252. }{
  253. {"kernel TUN requested", "wireguard", `{"noKernelTun":false,"mtu":1420}`},
  254. {"kernel TUN by default, protocol cased like the core accepts", "WireGuard", `{"mtu":1420}`},
  255. }
  256. for _, tc := range cases {
  257. t.Run(tc.name, func(t *testing.T) {
  258. raw := json_util.RawMessage(`[{"protocol":"freedom","tag":"direct","settings":{}},` +
  259. `{"protocol":"` + tc.protocol + `","tag":"warp","settings":` + tc.settings + `}]`)
  260. var outbounds []struct {
  261. Settings map[string]any `json:"settings"`
  262. }
  263. if err := json.Unmarshal(forceUserspaceWireGuardTun(raw), &outbounds); err != nil {
  264. t.Fatal(err)
  265. }
  266. if got := outbounds[1].Settings["noKernelTun"]; got != true {
  267. t.Fatalf("wireguard noKernelTun = %v, want true", got)
  268. }
  269. if got := outbounds[1].Settings["mtu"]; got != float64(1420) {
  270. t.Fatalf("wireguard mtu = %v, want the template's 1420 kept", got)
  271. }
  272. if _, ok := outbounds[0].Settings["noKernelTun"]; ok {
  273. t.Fatalf("freedom outbound gained noKernelTun: %v", outbounds[0].Settings)
  274. }
  275. })
  276. }
  277. }