xray_wireguard_config_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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. // The lower-id tunnel owns the shared record's peer fields: AmneziaWG first.
  89. if err := db.Create(awgInbound).Error; err != nil {
  90. t.Fatalf("create awg inbound: %v", err)
  91. }
  92. if err := db.Create(wgInbound).Error; err != nil {
  93. t.Fatalf("create wg inbound: %v", err)
  94. }
  95. svc := ClientService{}
  96. if err := svc.SyncInbound(nil, wgInbound.Id, []model.Client{wgClient}); err != nil {
  97. t.Fatalf("SyncInbound(wg): %v", err)
  98. }
  99. awgClient.Enable = enabled
  100. if err := svc.SyncInbound(nil, awgInbound.Id, []model.Client{awgClient}); err != nil {
  101. t.Fatalf("SyncInbound(awg): %v", err)
  102. }
  103. return email
  104. }
  105. func wgPeerList(t *testing.T, settings map[string]any) []map[string]any {
  106. t.Helper()
  107. if _, ok := settings["clients"]; ok {
  108. t.Fatalf("wireguard inbound must not emit a clients[] key: %v", settings["clients"])
  109. }
  110. rawPeers, ok := settings["peers"].([]any)
  111. if !ok {
  112. t.Fatalf("settings.peers is not an array: %T", settings["peers"])
  113. }
  114. out := make([]map[string]any, 0, len(rawPeers))
  115. for _, p := range rawPeers {
  116. m, ok := p.(map[string]any)
  117. if !ok {
  118. t.Fatalf("peer is not an object: %T", p)
  119. }
  120. out = append(out, m)
  121. }
  122. return out
  123. }
  124. func TestGetXrayConfigWireGuardPeers(t *testing.T) {
  125. clients := []model.Client{
  126. {Email: "[email protected]", Enable: true, PublicKey: "pub-alice", AllowedIPs: []string{"10.0.0.2/32"}, KeepAlive: model.KeepAlivePtr(25)},
  127. {Email: "[email protected]", Enable: true, PublicKey: "pub-bob", AllowedIPs: []string{"10.0.0.3/32"}},
  128. }
  129. seedWGInbound(t, "wg-multi", 51820, clients)
  130. settings := wgInboundEmittedSettings(t, "wg-multi")
  131. if settings["secretKey"] != wgTestSecretKey() {
  132. t.Errorf("secretKey not preserved: %v", settings["secretKey"])
  133. }
  134. if settings["mtu"] != float64(1420) {
  135. t.Errorf("mtu not preserved: %v", settings["mtu"])
  136. }
  137. peers := wgPeerList(t, settings)
  138. if len(peers) != 2 {
  139. t.Fatalf("expected 2 peers, got %d: %v", len(peers), peers)
  140. }
  141. ips := map[string]bool{}
  142. for _, p := range peers {
  143. if p["email"] == nil || p["email"] == "" {
  144. t.Errorf("peer missing email: %v", p)
  145. }
  146. if p["publicKey"] == nil || p["publicKey"] == "" {
  147. t.Errorf("peer missing publicKey: %v", p)
  148. }
  149. if p["level"] != float64(0) {
  150. t.Errorf("peer level = %v, want 0 (needed for per-user stats)", p["level"])
  151. }
  152. allowed, ok := p["allowedIPs"].([]any)
  153. if !ok || len(allowed) == 0 {
  154. t.Fatalf("peer missing allowedIPs: %v", p)
  155. }
  156. ips[allowed[0].(string)] = true
  157. }
  158. if len(ips) != 2 {
  159. t.Errorf("peers must have distinct allowedIPs, got %v", ips)
  160. }
  161. }
  162. func TestGetXrayConfigWireGuardDisabledClientExcluded(t *testing.T) {
  163. clients := []model.Client{
  164. {Email: "[email protected]", Enable: true, PublicKey: "pub-on", AllowedIPs: []string{"10.0.0.2/32"}},
  165. {Email: "[email protected]", Enable: true, PublicKey: "pub-off", AllowedIPs: []string{"10.0.0.3/32"}},
  166. }
  167. seedWGInbound(t, "wg-disabled", 51821, clients)
  168. if err := database.GetDB().Model(&model.ClientRecord{}).
  169. Where("email = ?", "[email protected]").Update("enable", false).Error; err != nil {
  170. t.Fatalf("disable client: %v", err)
  171. }
  172. peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-disabled"))
  173. if len(peers) != 1 {
  174. t.Fatalf("expected 1 enabled peer, got %d: %v", len(peers), peers)
  175. }
  176. if peers[0]["email"] != "[email protected]" {
  177. t.Errorf("wrong peer kept: %v", peers[0])
  178. }
  179. }
  180. func TestGetXrayConfigWireGuardUsesInboundLocalTunnelFields(t *testing.T) {
  181. email := seedDualTunnelClient(t, true, model.KeepAlivePtr(15))
  182. var shared model.ClientRecord
  183. if err := database.GetDB().Where("email = ?", email).First(&shared).Error; err != nil {
  184. t.Fatalf("read shared client: %v", err)
  185. }
  186. if shared.AllowedIPs != "10.8.1.5/32" || shared.PreSharedKey != "awg-psk" || shared.PublicKey != "pub-awg" || shared.KeepAlive != 25 {
  187. t.Fatalf("test setup did not persist AmneziaWG identity: allowedIPs=%q preSharedKey=%q publicKey=%q keepAlive=%d", shared.AllowedIPs, shared.PreSharedKey, shared.PublicKey, shared.KeepAlive)
  188. }
  189. peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-dual"))
  190. if len(peers) != 1 {
  191. t.Fatalf("expected 1 peer, got %d: %v", len(peers), peers)
  192. }
  193. allowed, ok := peers[0]["allowedIPs"].([]any)
  194. if !ok || len(allowed) != 1 || allowed[0] != "10.0.0.5/32" {
  195. t.Fatalf("WireGuard peer allowedIPs = %v, want [10.0.0.5/32]", peers[0]["allowedIPs"])
  196. }
  197. if peers[0]["preSharedKey"] != "wg-psk" {
  198. t.Fatalf("WireGuard peer preSharedKey = %v, want wg-psk", peers[0]["preSharedKey"])
  199. }
  200. if peers[0]["publicKey"] != "pub-wg" {
  201. t.Fatalf("WireGuard peer publicKey = %v, want pub-wg", peers[0]["publicKey"])
  202. }
  203. if peers[0]["keepAlive"] != float64(15) {
  204. t.Fatalf("WireGuard peer keepAlive = %v, want 15", peers[0]["keepAlive"])
  205. }
  206. }
  207. func TestGetXrayConfigWireGuardOmitsKeepAliveAbsentFromSettings(t *testing.T) {
  208. email := seedDualTunnelClient(t, true, nil)
  209. var shared model.ClientRecord
  210. if err := database.GetDB().Where("email = ?", email).First(&shared).Error; err != nil {
  211. t.Fatalf("read shared client: %v", err)
  212. }
  213. if shared.PublicKey != "pub-awg" || shared.KeepAlive != 25 {
  214. t.Fatalf("test setup did not persist AmneziaWG identity: publicKey=%q keepAlive=%d", shared.PublicKey, shared.KeepAlive)
  215. }
  216. peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-dual"))
  217. if len(peers) != 1 {
  218. t.Fatalf("expected 1 peer, got %d: %v", len(peers), peers)
  219. }
  220. if peers[0]["publicKey"] != "pub-wg" {
  221. t.Fatalf("WireGuard peer publicKey = %v, want pub-wg", peers[0]["publicKey"])
  222. }
  223. if _, ok := peers[0]["keepAlive"]; ok {
  224. t.Fatalf("WireGuard peer keepAlive = %v, want absent", peers[0]["keepAlive"])
  225. }
  226. }
  227. func TestGetXrayConfigWireGuardDisabledDualProtocolClientExcluded(t *testing.T) {
  228. seedDualTunnelClient(t, false, model.KeepAlivePtr(15))
  229. peers := wgPeerList(t, wgInboundEmittedSettings(t, "wg-dual"))
  230. if len(peers) != 0 {
  231. t.Fatalf("expected disabled dual-protocol client to be excluded, got %v", peers)
  232. }
  233. }
  234. func TestGetXrayConfigWireGuardNoClientsEmitsEmptyPeers(t *testing.T) {
  235. seedWGInbound(t, "wg-empty", 51822, nil)
  236. settings := wgInboundEmittedSettings(t, "wg-empty")
  237. if _, ok := settings["clients"]; ok {
  238. t.Fatalf("clients key must be absent")
  239. }
  240. peers, ok := settings["peers"].([]any)
  241. if !ok {
  242. t.Fatalf("peers must be an (empty) array, got %T", settings["peers"])
  243. }
  244. if len(peers) != 0 {
  245. t.Fatalf("expected empty peers, got %v", peers)
  246. }
  247. }
  248. // #6796: where the core would pick a kernel TUN it cannot set up (Docker, a sandboxed unit),
  249. // one WireGuard outbound aborts the whole start unless it is moved to the userspace TUN.
  250. func TestForceUserspaceWireGuardTun(t *testing.T) {
  251. cases := []struct {
  252. name, protocol, settings string
  253. }{
  254. {"kernel TUN requested", "wireguard", `{"noKernelTun":false,"mtu":1420}`},
  255. {"kernel TUN by default, protocol cased like the core accepts", "WireGuard", `{"mtu":1420}`},
  256. }
  257. for _, tc := range cases {
  258. t.Run(tc.name, func(t *testing.T) {
  259. raw := json_util.RawMessage(`[{"protocol":"freedom","tag":"direct","settings":{}},` +
  260. `{"protocol":"` + tc.protocol + `","tag":"warp","settings":` + tc.settings + `}]`)
  261. var outbounds []struct {
  262. Settings map[string]any `json:"settings"`
  263. }
  264. if err := json.Unmarshal(forceUserspaceWireGuardTun(raw), &outbounds); err != nil {
  265. t.Fatal(err)
  266. }
  267. if got := outbounds[1].Settings["noKernelTun"]; got != true {
  268. t.Fatalf("wireguard noKernelTun = %v, want true", got)
  269. }
  270. if got := outbounds[1].Settings["mtu"]; got != float64(1420) {
  271. t.Fatalf("wireguard mtu = %v, want the template's 1420 kept", got)
  272. }
  273. if _, ok := outbounds[0].Settings["noKernelTun"]; ok {
  274. t.Fatalf("freedom outbound gained noKernelTun: %v", outbounds[0].Settings)
  275. }
  276. })
  277. }
  278. }