feat(warp): register WARP over MASQUE from the WARP modal

xray-core's masque transport reaches Cloudflare WARP with an enrolled
ECDSA P-256 key, the endpoint's public key and the assigned tunnel
addresses, which only a WARP enrollment hands out.

A new regMasque action on /panel/api/xray/warp/:action registers a
separate WARP device and enrolls a secp256r1 key for the masque tunnel
(the API revision and client version the WARP apps enroll MASQUE keys
with). Enrolling replaces a device's WireGuard key, so it never touches
the stored WireGuard registration. It returns the PEM key pair, the
addresses and the endpoint, and the WARP modal turns them into a
warp-masque outbound (HTTP/3, Cloudflare's MASQUE SNI), or refreshes
the one already there.
This commit is contained in:
MHSanaei
2026-10-11 10:31:01 +02:00
parent b04039c2bd
commit 04cccef311
25 changed files with 477 additions and 16 deletions
+5
View File
@@ -40,6 +40,11 @@ Pick **masque** as the outbound protocol, then set:
## WARP over MASQUE
The quickest way is **Xray → Outbounds → WARP → Add WARP over MASQUE outbound**: the panel
registers a separate WARP device, enrolls a MASQUE key for it and adds a ready `warp-masque`
outbound (or refreshes the existing one). Your WireGuard WARP registration is not touched.
To set it up by hand instead:
Turn on **WARP** in the masque transport to reach Cloudflare WARP through its MASQUE
endpoint instead of WireGuard. It takes a WARP registration enrolled for MASQUE:
+5
View File
@@ -40,6 +40,11 @@ icon: Waypoints
## WARP از طریق MASQUE
ساده‌ترین راه **Xray ← Outbounds ← WARP ← افزودن outbound برای WARP روی MASQUE** است: پنل یک
دستگاه WARP جداگانه ثبت می‌کند، برای آن کلید MASQUE ثبت می‌کند و یک outbound آمادهٔ `warp-masque`
اضافه می‌کند (یا outbound موجود را تازه می‌کند). ثبت WARP وایرگارد شما دست نمی‌خورد.
برای راه‌اندازی دستی:
**WARP** را در transport مربوط به masque روشن کنید تا به‌جای WireGuard از طریق endpoint
مخصوص MASQUE به Cloudflare WARP برسید. این کار به یک ثبت‌نام WARP نیاز دارد که برای MASQUE
ثبت شده باشد:
+5
View File
@@ -40,6 +40,11 @@ icon: Waypoints
## WARP через MASQUE
Проще всего: **Xray → Аутбаунды → WARP → Добавить аутбаунд WARP через MASQUE** — панель
регистрирует отдельное устройство WARP, привязывает к нему ключ MASQUE и добавляет готовый
аутбаунд `warp-masque` (или обновляет существующий). Регистрация WARP для WireGuard не меняется.
Для ручной настройки:
Включите **WARP** в транспорте masque, чтобы подключаться к Cloudflare WARP через его
MASQUE-эндпоинт вместо WireGuard. Для этого нужна регистрация WARP, привязанная к MASQUE:
+4
View File
@@ -38,6 +38,10 @@ HTTPS 流量无异的完整三层隧道。xray-core 原生支持它;3x-ui 将
## 通过 MASQUE 使用 WARP
最简单的方式是 **Xray → 出站 → WARP → 添加 WARP over MASQUE 出站**:面板会单独注册一个 WARP 设备、为其注册 MASQUE 密钥,
并添加一个可直接使用的 `warp-masque` 出站(若已存在则更新)。你的 WireGuard WARP 注册不受影响。
如需手动设置:
在 masque 传输中开启 **WARP**,即可通过 Cloudflare 的 MASQUE 端点而非 WireGuard 连接 WARP。这需要一个已为
MASQUE 注册的 WARP 账户:
+1 -1
View File
@@ -13649,7 +13649,7 @@
"name": "action",
"in": "path",
"required": true,
"description": "data — return Warp stats. del — delete Warp data. config — return current config. reg — register (sends keys). changeIp — rotate the endpoint. license — set a Warp+ key. interval — set automatic rotation in hours.",
"description": "data — return Warp stats. del — delete Warp data. config — return current config. reg — register (sends keys). regMasque — register a separate device enrolled for MASQUE and return its warp key pair, addresses and endpoint. changeIp — rotate the endpoint. license — set a Warp+ key. interval — set automatic rotation in hours.",
"schema": {
"type": "string"
}
+1 -1
View File
@@ -13649,7 +13649,7 @@
"name": "action",
"in": "path",
"required": true,
"description": "data — return Warp stats. del — delete Warp data. config — return current config. reg — register (sends keys). changeIp — rotate the endpoint. license — set a Warp+ key. interval — set automatic rotation in hours.",
"description": "data — return Warp stats. del — delete Warp data. config — return current config. reg — register (sends keys). regMasque — register a separate device enrolled for MASQUE and return its warp key pair, addresses and endpoint. changeIp — rotate the endpoint. license — set a Warp+ key. interval — set automatic rotation in hours.",
"schema": {
"type": "string"
}
+1 -1
View File
@@ -2138,7 +2138,7 @@ export const sections: readonly Section[] = [
name: 'action',
in: 'path',
type: 'string',
desc: 'data — return Warp stats. del — delete Warp data. config — return current config. reg — register (sends keys). changeIp — rotate the endpoint. license — set a Warp+ key. interval — set automatic rotation in hours.',
desc: 'data — return Warp stats. del — delete Warp data. config — return current config. reg — register (sends keys). regMasque — register a separate device enrolled for MASQUE and return its warp key pair, addresses and endpoint. changeIp — rotate the endpoint. license — set a Warp+ key. interval — set automatic rotation in hours.',
},
{
name: 'privateKey',
@@ -93,6 +93,31 @@ export function buildWarpOutbound(
};
}
interface WarpMasqueRegistration {
privateKey: string;
publicKey: string;
address: string[];
endpoint: string;
}
// Cloudflare serves WARP over MASQUE on HTTP/3 behind this SNI; the enrolled key and
// endpoint key replace user/pass, which xray-core refuses next to a warp block.
export function buildWarpMasqueOutbound(reg: WarpMasqueRegistration): Record<string, unknown> {
return {
tag: 'warp-masque',
protocol: 'masque',
settings: { address: reg.endpoint, port: 443 },
streamSettings: {
network: 'masque',
security: 'tls',
tlsSettings: { serverName: 'consumer-masque.cloudflareclient.com', alpn: ['h3'] },
masqueSettings: {
warp: { privateKey: reg.privateKey, publicKey: reg.publicKey, address: reg.address },
},
},
};
}
export function mergeWarpRotation(
existing: Record<string, unknown> | undefined,
data: WarpData | null,
@@ -310,6 +335,21 @@ export default function WarpModal({
}
}
async function addMasqueOutbound() {
setLoading(true);
try {
const msg = await HttpUtil.post<string>('/panel/api/xray/warp/regMasque');
if (!msg?.success || !msg.obj) return;
const outbound = buildWarpMasqueOutbound(JSON.parse(msg.obj) as WarpMasqueRegistration);
const index = templateSettings?.outbounds?.findIndex((o) => o?.tag === 'warp-masque') ?? -1;
if (index >= 0) onResetOutbound({ index, outbound });
else onAddOutbound(outbound);
onClose();
} finally {
setLoading(false);
}
}
function addOutbound() {
if (!stagedOutbound) {
messageApi.warning(t('pages.xray.warp.fetchFirst'));
@@ -544,6 +584,18 @@ export default function WarpModal({
)}
</>
)}
<Divider className="my-10">MASQUE</Divider>
<Alert type="info" showIcon title={t('pages.xray.warp.masqueDesc')} />
<Button
type="primary"
className="mt-8"
loading={loading}
icon={<PlusOutlined />}
onClick={addMasqueOutbound}
>
{t('pages.xray.warp.addMasqueOutbound')}
</Button>
</FormProvider>
</Modal>
</>
@@ -0,0 +1,100 @@
import { describe, expect, it, vi, afterEach } from 'vitest';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import WarpModal from '@/pages/xray/overrides/WarpModal';
import { HttpUtil, Msg } from '@/utils';
import { renderWithProviders } from './test-utils';
const registration = {
privateKey: '-----BEGIN PRIVATE KEY-----\nAAA\n-----END PRIVATE KEY-----\n',
publicKey: '-----BEGIN PUBLIC KEY-----\nBBB\n-----END PUBLIC KEY-----\n',
address: ['172.16.0.2', '2606:4700:110:8a36::2'],
endpoint: '162.159.198.1',
};
function mockApi() {
vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
if (url === '/panel/api/xray/warp/regMasque') {
return new Msg(true, '', JSON.stringify(registration));
}
if (url === '/panel/api/setting/all') return new Msg(true, '', {});
return new Msg(true, '', '');
});
}
async function clickAddMasque() {
const button = await screen.findByRole('button', { name: /Add WARP over MASQUE outbound/ });
// The modal loads its WARP data on open; a loading button swallows the click.
await waitFor(() => expect(button.className).not.toContain('ant-btn-loading'));
await act(async () => {
fireEvent.click(button);
});
}
afterEach(() => {
vi.restoreAllMocks();
});
// WARP over MASQUE dials Cloudflare's MASQUE endpoint with the enrolled key, its SNI
// and HTTP/3; an outbound missing any of them fails the handshake at runtime.
describe('WarpModal WARP over MASQUE', () => {
const expected = {
tag: 'warp-masque',
protocol: 'masque',
settings: { address: '162.159.198.1', port: 443 },
streamSettings: {
network: 'masque',
security: 'tls',
tlsSettings: { serverName: 'consumer-masque.cloudflareclient.com', alpn: ['h3'] },
masqueSettings: {
warp: {
privateKey: registration.privateKey,
publicKey: registration.publicKey,
address: registration.address,
},
},
},
};
it('adds a MASQUE outbound built from a fresh enrollment', async () => {
mockApi();
const onAddOutbound = vi.fn();
renderWithProviders(
<WarpModal
open
templateSettings={{ outbounds: [{ tag: 'direct' }] }}
onClose={() => {}}
onAddOutbound={onAddOutbound}
onResetOutbound={() => {}}
onRemoveOutbound={() => {}}
/>,
);
await clickAddMasque();
await waitFor(() => expect(onAddOutbound).toHaveBeenCalledWith(expected));
});
it('replaces an existing warp-masque outbound instead of adding a second one', async () => {
mockApi();
const onAddOutbound = vi.fn();
const onResetOutbound = vi.fn();
renderWithProviders(
<WarpModal
open
templateSettings={{ outbounds: [{ tag: 'direct' }, { tag: 'warp-masque' }] }}
onClose={() => {}}
onAddOutbound={onAddOutbound}
onResetOutbound={onResetOutbound}
onRemoveOutbound={() => {}}
/>,
);
await clickAddMasque();
await waitFor(() =>
expect(onResetOutbound).toHaveBeenCalledWith({ index: 1, outbound: expected }),
);
expect(onAddOutbound).not.toHaveBeenCalled();
});
});
+2
View File
@@ -195,6 +195,8 @@ func (a *XraySettingController) warp(c *gin.Context) {
skey := c.PostForm("privateKey")
pkey := c.PostForm("publicKey")
resp, err = a.WarpService.RegWarp(skey, pkey)
case "regMasque":
resp, err = a.WarpService.RegWarpMasque()
case "changeIp":
resp, err = a.WarpService.ChangeWarpIP()
if err == nil {
+126
View File
@@ -3,9 +3,16 @@ package integration
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net"
"net/http"
"os"
"time"
@@ -30,6 +37,12 @@ const (
// (not a const) so integration tests can point it at a mock server.
var warpAPIBase = "https://api.cloudflareclient.com/v0a4005"
// warpMasqueAPIBase and warpMasqueClientVer are the API revision and client the WARP
// apps enroll secp256r1 MASQUE keys with; a var so tests can point it at a mock.
var warpMasqueAPIBase = "https://api.cloudflareclient.com/v0a4471"
const warpMasqueClientVer = "a-6.35-4471"
func (s *WarpService) GetWarpData() (string, error) {
return s.GetWarp()
}
@@ -134,6 +147,119 @@ func (s *WarpService) RegWarp(secretKey string, publicKey string) (string, error
return string(result), nil
}
// RegWarpMasque registers a WARP device of its own and enrolls an ECDSA P-256 key for
// MASQUE: enrolling replaces a device's WireGuard key, so the stored one stays untouched.
func (s *WarpService) RegWarpMasque() (string, error) {
_, wgPublicKey, err := wireguard.GenerateWireguardKeypair()
if err != nil {
return "", err
}
hostName, _ := os.Hostname()
regBody, err := json.Marshal(map[string]any{
"key": wgPublicKey,
"tos": time.Now().UTC().Format("2006-01-02T15:04:05.000Z"),
"type": "PC",
"model": "x-ui",
"name": hostName,
})
if err != nil {
return "", err
}
var registration struct {
ID string `json:"id"`
Token string `json:"token"`
}
if err := s.doWarpMasqueRequest(http.MethodPost, warpMasqueAPIBase+"/reg", "", regBody, &registration); err != nil {
return "", err
}
if registration.ID == "" || registration.Token == "" {
return "", common.NewError("warp masque register: missing 'id' or 'token' in response")
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return "", err
}
publicDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
if err != nil {
return "", err
}
enrollBody, err := json.Marshal(map[string]string{
"key": base64.StdEncoding.EncodeToString(publicDER),
"key_type": "secp256r1",
"tunnel_type": "masque",
"name": hostName,
})
if err != nil {
return "", err
}
var enrolled struct {
Config struct {
Interface struct {
Addresses struct {
V4 string `json:"v4"`
V6 string `json:"v6"`
} `json:"addresses"`
} `json:"interface"`
Peers []struct {
PublicKey string `json:"public_key"`
Endpoint struct {
V4 string `json:"v4"`
} `json:"endpoint"`
} `json:"peers"`
} `json:"config"`
}
if err := s.doWarpMasqueRequest(http.MethodPatch, warpMasqueAPIBase+"/reg/"+registration.ID, registration.Token, enrollBody, &enrolled); err != nil {
return "", err
}
if len(enrolled.Config.Peers) == 0 || enrolled.Config.Peers[0].PublicKey == "" {
return "", common.NewError("warp masque enroll: missing endpoint public key in response")
}
peer := enrolled.Config.Peers[0]
endpoint, _, err := net.SplitHostPort(peer.Endpoint.V4)
if err != nil {
endpoint = peer.Endpoint.V4
}
privateDER, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return "", err
}
var address []string
for _, a := range []string{enrolled.Config.Interface.Addresses.V4, enrolled.Config.Interface.Addresses.V6} {
if a != "" {
address = append(address, a)
}
}
result, err := json.MarshalIndent(map[string]any{
"privateKey": string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDER})),
"publicKey": peer.PublicKey,
"address": address,
"endpoint": endpoint,
}, "", " ")
if err != nil {
return "", err
}
return string(result), nil
}
func (s *WarpService) doWarpMasqueRequest(method, url, token string, body []byte, out any) error {
req, err := http.NewRequestWithContext(context.Background(), method, url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("CF-Client-Version", warpMasqueClientVer)
req.Header.Set("User-Agent", "WARP for Android")
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
respBody, err := s.doWarpRequest(req)
if err != nil {
return err
}
return json.Unmarshal(respBody, out)
}
func (s *WarpService) SetWarpLicense(license string) (string, error) {
warpData, err := s.loadWarpCreds()
if err != nil {
@@ -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)
}
}
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "بيانات WARP+",
"quota": "الحصة",
"usage": "الاستخدام",
"addOutbound": "إضافة صادر"
"addOutbound": "إضافة صادر",
"masqueDesc": "يسجّل جهاز WARP منفصلًا مخصصًا لـ MASQUE؛ ولا يتأثر تسجيل WireGuard أعلاه.",
"addMasqueOutbound": "إضافة outbound لـ WARP عبر MASQUE"
},
"dns": {
"enable": "فعل DNS",
+3 -1
View File
@@ -2347,7 +2347,9 @@
"warpPlusData": "WARP+ data",
"quota": "Quota",
"usage": "Usage",
"addOutbound": "Add outbound"
"addOutbound": "Add outbound",
"masqueDesc": "Registers a separate WARP device enrolled for MASQUE; the WireGuard registration above is not affected.",
"addMasqueOutbound": "Add WARP over MASQUE outbound"
},
"dns": {
"enable": "Enable DNS",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "Datos WARP+",
"quota": "Cuota",
"usage": "Uso",
"addOutbound": "Añadir salida"
"addOutbound": "Añadir salida",
"masqueDesc": "Registra un dispositivo WARP aparte inscrito para MASQUE; el registro de WireGuard de arriba no se ve afectado.",
"addMasqueOutbound": "Añadir outbound de WARP sobre MASQUE"
},
"dns": {
"enable": "Habilitar DNS",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "داده WARP+",
"quota": "سهمیه",
"usage": "مصرف",
"addOutbound": "افزودن خروجی"
"addOutbound": "افزودن خروجی",
"masqueDesc": "یک دستگاه WARP جداگانه برای MASQUE ثبت می‌کند؛ ثبت WireGuard بالا تغییری نمی‌کند.",
"addMasqueOutbound": "افزودن outbound برای WARP روی MASQUE"
},
"dns": {
"enable": "فعال کردن حل دامنه",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "Data WARP+",
"quota": "Kuota",
"usage": "Penggunaan",
"addOutbound": "Tambah outbound"
"addOutbound": "Tambah outbound",
"masqueDesc": "Mendaftarkan perangkat WARP terpisah untuk MASQUE; pendaftaran WireGuard di atas tidak terpengaruh.",
"addMasqueOutbound": "Tambah outbound WARP melalui MASQUE"
},
"dns": {
"enable": "Aktifkan DNS",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "WARP+ データ",
"quota": "クォータ",
"usage": "使用量",
"addOutbound": "アウトバウンドを追加"
"addOutbound": "アウトバウンドを追加",
"masqueDesc": "MASQUE 用に別の WARP デバイスを登録します。上の WireGuard の登録には影響しません。",
"addMasqueOutbound": "WARP over MASQUE のアウトバウンドを追加"
},
"dns": {
"enable": "DNSを有効にする",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "Dados WARP+",
"quota": "Quota",
"usage": "Uso",
"addOutbound": "Adicionar saída"
"addOutbound": "Adicionar saída",
"masqueDesc": "Registra um dispositivo WARP separado inscrito para MASQUE; o registro WireGuard acima não é afetado.",
"addMasqueOutbound": "Adicionar outbound WARP via MASQUE"
},
"dns": {
"enable": "Ativar DNS",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "WARP+ data",
"quota": "Квота",
"usage": "Использование",
"addOutbound": "Добавить исходящий"
"addOutbound": "Добавить исходящий",
"masqueDesc": "Регистрирует отдельное устройство WARP для MASQUE; регистрация WireGuard выше не затрагивается.",
"addMasqueOutbound": "Добавить аутбаунд WARP через MASQUE"
},
"dns": {
"enable": "Включить DNS",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "WARP+ Veri",
"quota": "Kota",
"usage": "Kullanım",
"addOutbound": "Giden Bağlantı Ekle"
"addOutbound": "Giden Bağlantı Ekle",
"masqueDesc": "MASQUE için ayrı bir WARP cihazı kaydeder; yukarıdaki WireGuard kaydı etkilenmez.",
"addMasqueOutbound": "MASQUE üzerinden WARP outbound ekle"
},
"dns": {
"enable": "DNS'yi Etkinleştir",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "WARP+ data",
"quota": "Квота",
"usage": "Використання",
"addOutbound": "Додати вихідний"
"addOutbound": "Додати вихідний",
"masqueDesc": "Реєструє окремий пристрій WARP для MASQUE; реєстрація WireGuard вище не змінюється.",
"addMasqueOutbound": "Додати аутбаунд WARP через MASQUE"
},
"dns": {
"enable": "Увімкнути DNS",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "Dữ liệu WARP+",
"quota": "Hạn ngạch",
"usage": "Sử dụng",
"addOutbound": "Thêm outbound"
"addOutbound": "Thêm outbound",
"masqueDesc": "Đăng ký một thiết bị WARP riêng cho MASQUE; đăng ký WireGuard ở trên không bị ảnh hưởng.",
"addMasqueOutbound": "Thêm outbound WARP qua MASQUE"
},
"dns": {
"enable": "Kích hoạt DNS",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "WARP+ 数据",
"quota": "配额",
"usage": "使用",
"addOutbound": "添加出站"
"addOutbound": "添加出站",
"masqueDesc": "为 MASQUE 单独注册一个 WARP 设备;上方的 WireGuard 注册不受影响。",
"addMasqueOutbound": "添加 WARP over MASQUE 出站"
},
"dns": {
"enable": "启用 DNS",
+3 -1
View File
@@ -2229,7 +2229,9 @@
"warpPlusData": "WARP+ 資料",
"quota": "配額",
"usage": "使用",
"addOutbound": "新增出站"
"addOutbound": "新增出站",
"masqueDesc": "為 MASQUE 另外註冊一個 WARP 裝置;上方的 WireGuard 註冊不受影響。",
"addMasqueOutbound": "新增 WARP over MASQUE 出站"
},
"dns": {
"enable": "啟用 DNS",