|
@@ -0,0 +1,136 @@
|
|
|
|
|
+package integration
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "crypto/ecdsa"
|
|
|
|
|
+ "crypto/elliptic"
|
|
|
|
|
+ "crypto/rand"
|
|
|
|
|
+ "crypto/x509"
|
|
|
|
|
+ "encoding/base64"
|
|
|
|
|
+ "encoding/json"
|
|
|
|
|
+ "encoding/pem"
|
|
|
|
|
+ "net/http"
|
|
|
|
|
+ "net/http/httptest"
|
|
|
|
|
+ "path/filepath"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/xtls/xray-core/infra/conf"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+func withWarpMasqueAPIBase(t *testing.T, base string) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ orig := warpMasqueAPIBase
|
|
|
|
|
+ warpMasqueAPIBase = base
|
|
|
|
|
+ t.Cleanup(func() { warpMasqueAPIBase = orig })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Enrolling an ECDSA key replaces a device's WireGuard key, so MASQUE gets a device of
|
|
|
|
|
+// its own; the returned warp block must pair the enrolled key and load in xray-core.
|
|
|
|
|
+func TestRegWarpMasqueEnrollsASeparateP256Device(t *testing.T) {
|
|
|
|
|
+ dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
|
|
|
|
|
+ seedWarp(t, "")
|
|
|
|
|
+
|
|
|
|
|
+ endpointKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("generate endpoint key: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ endpointDER, err := x509.MarshalPKIXPublicKey(&endpointKey.PublicKey)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("marshal endpoint key: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ endpointPEM := string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: endpointDER}))
|
|
|
|
|
+
|
|
|
|
|
+ var enrolledKey *ecdsa.PublicKey
|
|
|
|
|
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
+ switch {
|
|
|
|
|
+ case r.Method == http.MethodPost && r.URL.Path == "/reg":
|
|
|
|
|
+ _, _ = w.Write([]byte(`{"id":"masque-device","token":"masque-token","account":{"license":""}}`))
|
|
|
|
|
+ case r.Method == http.MethodPatch && r.URL.Path == "/reg/masque-device":
|
|
|
|
|
+ if got := r.Header.Get("Authorization"); got != "Bearer masque-token" {
|
|
|
|
|
+ t.Errorf("enroll Authorization = %q, want the new device's token", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ var body map[string]string
|
|
|
|
|
+ _ = json.NewDecoder(r.Body).Decode(&body)
|
|
|
|
|
+ if body["key_type"] != "secp256r1" || body["tunnel_type"] != "masque" {
|
|
|
|
|
+ t.Errorf("enroll body = %v, want a secp256r1 key for the masque tunnel", body)
|
|
|
|
|
+ }
|
|
|
|
|
+ der, err := base64.StdEncoding.DecodeString(body["key"])
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Errorf("enroll key is not base64: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ parsed, err := x509.ParsePKIXPublicKey(der)
|
|
|
|
|
+ key, ok := parsed.(*ecdsa.PublicKey)
|
|
|
|
|
+ if err != nil || !ok || key.Curve != elliptic.P256() {
|
|
|
|
|
+ t.Errorf("enroll key is not a PKIX P-256 public key: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ enrolledKey = key
|
|
|
|
|
+ resp, _ := json.Marshal(map[string]any{
|
|
|
|
|
+ "id": "masque-device",
|
|
|
|
|
+ "config": map[string]any{
|
|
|
|
|
+ "interface": map[string]any{"addresses": map[string]any{"v4": "172.16.0.2", "v6": "2606:4700:110:8a36::2"}},
|
|
|
|
|
+ "peers": []any{map[string]any{
|
|
|
|
|
+ "public_key": endpointPEM,
|
|
|
|
|
+ "endpoint": map[string]any{"v4": "162.159.198.1:0", "v6": "[2606:4700:103::1]:0"},
|
|
|
|
|
+ }},
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ _, _ = w.Write(resp)
|
|
|
|
|
+ default:
|
|
|
|
|
+ t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
|
|
|
+ w.WriteHeader(http.StatusNotFound)
|
|
|
|
|
+ }
|
|
|
|
|
+ }))
|
|
|
|
|
+ t.Cleanup(srv.Close)
|
|
|
|
|
+ withWarpMasqueAPIBase(t, srv.URL)
|
|
|
|
|
+
|
|
|
|
|
+ out, err := (&WarpService{}).RegWarpMasque()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("RegWarpMasque: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ var result struct {
|
|
|
|
|
+ PrivateKey string `json:"privateKey"`
|
|
|
|
|
+ PublicKey string `json:"publicKey"`
|
|
|
|
|
+ Address []string `json:"address"`
|
|
|
|
|
+ Endpoint string `json:"endpoint"`
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := json.Unmarshal([]byte(out), &result); err != nil {
|
|
|
|
|
+ t.Fatalf("decode result: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if result.Endpoint != "162.159.198.1" || result.PublicKey != endpointPEM {
|
|
|
|
|
+ t.Fatalf("endpoint = %q, publicKey = %q; want the enrolled peer's", result.Endpoint, result.PublicKey)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ block, _ := pem.Decode([]byte(result.PrivateKey))
|
|
|
|
|
+ if block == nil {
|
|
|
|
|
+ t.Fatalf("privateKey %q is not PEM", result.PrivateKey)
|
|
|
|
|
+ }
|
|
|
|
|
+ private, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
|
|
|
+ ecPrivate, ok := private.(*ecdsa.PrivateKey)
|
|
|
|
|
+ if err != nil || !ok || enrolledKey == nil || !ecPrivate.PublicKey.Equal(enrolledKey) {
|
|
|
|
|
+ t.Fatal("the returned private key does not match the enrolled public key")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ raw, _ := json.Marshal(map[string]any{"warp": map[string]any{
|
|
|
|
|
+ "privateKey": result.PrivateKey, "publicKey": result.PublicKey, "address": result.Address,
|
|
|
|
|
+ }})
|
|
|
|
|
+ var masque conf.MasqueConfig
|
|
|
|
|
+ if err := json.Unmarshal(raw, &masque); err != nil {
|
|
|
|
|
+ t.Fatalf("decode masque config: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, err := masque.Build(); err != nil {
|
|
|
|
|
+ t.Fatalf("xray-core refuses the returned warp block: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var stored model.Setting
|
|
|
|
|
+ if err := database.GetDB().Where("key = ?", "warp").First(&stored).Error; err != nil {
|
|
|
|
|
+ t.Fatalf("reload warp setting: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ var data map[string]string
|
|
|
|
|
+ _ = json.Unmarshal([]byte(stored.Value), &data)
|
|
|
|
|
+ if data["device_id"] != "old-device" {
|
|
|
|
|
+ t.Fatalf("stored WireGuard registration = %v, want it untouched", data)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|