warp_masque_test.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. package integration
  2. import (
  3. "crypto/ecdsa"
  4. "crypto/elliptic"
  5. "crypto/rand"
  6. "crypto/x509"
  7. "encoding/base64"
  8. "encoding/json"
  9. "encoding/pem"
  10. "net/http"
  11. "net/http/httptest"
  12. "path/filepath"
  13. "testing"
  14. "github.com/xtls/xray-core/infra/conf"
  15. "github.com/mhsanaei/3x-ui/v3/internal/database"
  16. "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
  17. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  18. )
  19. func withWarpMasqueAPIBase(t *testing.T, base string) {
  20. t.Helper()
  21. orig := warpMasqueAPIBase
  22. warpMasqueAPIBase = base
  23. t.Cleanup(func() { warpMasqueAPIBase = orig })
  24. }
  25. // Enrolling an ECDSA key replaces a device's WireGuard key, so MASQUE gets a device of
  26. // its own; the returned warp block must pair the enrolled key and load in xray-core.
  27. func TestRegWarpMasqueEnrollsASeparateP256Device(t *testing.T) {
  28. dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
  29. seedWarp(t, "")
  30. endpointKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  31. if err != nil {
  32. t.Fatalf("generate endpoint key: %v", err)
  33. }
  34. endpointDER, err := x509.MarshalPKIXPublicKey(&endpointKey.PublicKey)
  35. if err != nil {
  36. t.Fatalf("marshal endpoint key: %v", err)
  37. }
  38. endpointPEM := string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: endpointDER}))
  39. var enrolledKey *ecdsa.PublicKey
  40. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  41. switch {
  42. case r.Method == http.MethodPost && r.URL.Path == "/reg":
  43. _, _ = w.Write([]byte(`{"id":"masque-device","token":"masque-token","account":{"license":""}}`))
  44. case r.Method == http.MethodPatch && r.URL.Path == "/reg/masque-device":
  45. if got := r.Header.Get("Authorization"); got != "Bearer masque-token" {
  46. t.Errorf("enroll Authorization = %q, want the new device's token", got)
  47. }
  48. var body map[string]string
  49. _ = json.NewDecoder(r.Body).Decode(&body)
  50. if body["key_type"] != "secp256r1" || body["tunnel_type"] != "masque" {
  51. t.Errorf("enroll body = %v, want a secp256r1 key for the masque tunnel", body)
  52. }
  53. der, err := base64.StdEncoding.DecodeString(body["key"])
  54. if err != nil {
  55. t.Errorf("enroll key is not base64: %v", err)
  56. }
  57. parsed, err := x509.ParsePKIXPublicKey(der)
  58. key, ok := parsed.(*ecdsa.PublicKey)
  59. if err != nil || !ok || key.Curve != elliptic.P256() {
  60. t.Errorf("enroll key is not a PKIX P-256 public key: %v", err)
  61. }
  62. enrolledKey = key
  63. resp, _ := json.Marshal(map[string]any{
  64. "id": "masque-device",
  65. "config": map[string]any{
  66. "interface": map[string]any{"addresses": map[string]any{"v4": "172.16.0.2", "v6": "2606:4700:110:8a36::2"}},
  67. "peers": []any{map[string]any{
  68. "public_key": endpointPEM,
  69. "endpoint": map[string]any{"v4": "162.159.198.1:0", "v6": "[2606:4700:103::1]:0"},
  70. }},
  71. },
  72. })
  73. _, _ = w.Write(resp)
  74. default:
  75. t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
  76. w.WriteHeader(http.StatusNotFound)
  77. }
  78. }))
  79. t.Cleanup(srv.Close)
  80. withWarpMasqueAPIBase(t, srv.URL)
  81. out, err := (&WarpService{}).RegWarpMasque()
  82. if err != nil {
  83. t.Fatalf("RegWarpMasque: %v", err)
  84. }
  85. var result struct {
  86. PrivateKey string `json:"privateKey"`
  87. PublicKey string `json:"publicKey"`
  88. Address []string `json:"address"`
  89. Endpoint string `json:"endpoint"`
  90. }
  91. if err := json.Unmarshal([]byte(out), &result); err != nil {
  92. t.Fatalf("decode result: %v", err)
  93. }
  94. if result.Endpoint != "162.159.198.1" || result.PublicKey != endpointPEM {
  95. t.Fatalf("endpoint = %q, publicKey = %q; want the enrolled peer's", result.Endpoint, result.PublicKey)
  96. }
  97. block, _ := pem.Decode([]byte(result.PrivateKey))
  98. if block == nil {
  99. t.Fatalf("privateKey %q is not PEM", result.PrivateKey)
  100. }
  101. private, err := x509.ParsePKCS8PrivateKey(block.Bytes)
  102. ecPrivate, ok := private.(*ecdsa.PrivateKey)
  103. if err != nil || !ok || enrolledKey == nil || !ecPrivate.PublicKey.Equal(enrolledKey) {
  104. t.Fatal("the returned private key does not match the enrolled public key")
  105. }
  106. raw, _ := json.Marshal(map[string]any{"warp": map[string]any{
  107. "privateKey": result.PrivateKey, "publicKey": result.PublicKey, "address": result.Address,
  108. }})
  109. var masque conf.MasqueConfig
  110. if err := json.Unmarshal(raw, &masque); err != nil {
  111. t.Fatalf("decode masque config: %v", err)
  112. }
  113. if _, err := masque.Build(); err != nil {
  114. t.Fatalf("xray-core refuses the returned warp block: %v", err)
  115. }
  116. var stored model.Setting
  117. if err := database.GetDB().Where("key = ?", "warp").First(&stored).Error; err != nil {
  118. t.Fatalf("reload warp setting: %v", err)
  119. }
  120. var data map[string]string
  121. _ = json.Unmarshal([]byte(stored.Value), &data)
  122. if data["device_id"] != "old-device" {
  123. t.Fatalf("stored WireGuard registration = %v, want it untouched", data)
  124. }
  125. }