Kaynağa Gözat

Add encrypted DNS presets (#5837)

Rouzbeh† 15 saat önce
ebeveyn
işleme
b8a654967f

+ 0 - 1
.gitignore

@@ -44,4 +44,3 @@ docker-compose.override.yml
 
 # Ignore .env (Environment Variables) file
 .env
-

+ 19 - 0
docs/xray-dns.md

@@ -0,0 +1,19 @@
+# Xray DNS presets and leak notes
+
+The panel DNS preset menu supports plain DNS, DoH, and DoQ server entries accepted by Xray's built-in DNS config. These notes follow Xray's DNS server address forms documented in <https://xtls.github.io/en/config/dns.html>.
+
+- Plain DNS uses UDP/53 and follows routing unless a local-mode scheme is used. It is easy to observe on the server network path.
+- DoH uses `https://.../dns-query` and goes through Xray routing.
+- DoQ uses `quic+local://...` in Xray. Local mode bypasses Xray routing and connects directly through Freedom, which avoids DNS routing loops but can reveal resolver traffic from the server IP.
+- Xray DNS does not have a direct DoT URL scheme. Use DoH/DoQ, or wrap DNS-over-TCP/TLS outside this DNS config if DoT is required.
+
+Leak-prone settings:
+
+- `localhost` uses the host resolver and is outside Xray control.
+- `tcp+local://`, `https+local://`, and `quic+local://` bypass Xray routing by design.
+- Domain resolver names can need system DNS in local mode unless pinned in `hosts`.
+- `disableFallback: false` or `enableParallelQuery: true` can query fallback servers and reveal domains to more than one provider.
+- `clientIp` sends EDNS Client Subnet data upstream.
+- Plain IP/UDP DNS presets are not encrypted.
+
+For privacy-sensitive setups, prefer DoH through routed outbounds, add `hosts` pins for resolver hostnames, set `disableFallback` or `skipFallback` where fallback is not wanted, and keep `clientIp` empty.

+ 4 - 0
frontend/src/pages/xray/dns/DnsPresetsModal.css

@@ -1,3 +1,7 @@
+.preset-warning {
+  margin-bottom: 12px;
+}
+
 .preset-list {
   border: 1px solid var(--ant-color-border-secondary);
   border-radius: 8px;

+ 53 - 10
frontend/src/pages/xray/dns/DnsPresetsModal.tsx

@@ -1,5 +1,5 @@
 import { useTranslation } from 'react-i18next';
-import { Button, Modal, Space, Tag } from 'antd';
+import { Alert, Button, Modal, Space, Tag } from 'antd';
 import './DnsPresetsModal.css';
 
 interface DnsPresetsModalProps {
@@ -8,34 +8,69 @@ interface DnsPresetsModalProps {
   onInstall: (servers: string[]) => void;
 }
 
-const PRESETS: { name: string; family: boolean; data: string[] }[] = [
+export const PRESETS: { name: string; tags: string[]; data: string[] }[] = [
   {
     name: 'Google DNS',
-    family: false,
+    tags: ['UDP'],
     data: ['8.8.8.8', '8.8.4.4', '2001:4860:4860::8888', '2001:4860:4860::8844'],
   },
   {
     name: 'Cloudflare DNS',
-    family: false,
+    tags: ['UDP'],
     data: ['1.1.1.1', '1.0.0.1', '2606:4700:4700::1111', '2606:4700:4700::1001'],
   },
   {
     name: 'AdGuard DNS',
-    family: false,
+    tags: ['UDP'],
     data: ['94.140.14.14', '94.140.15.15', '2a10:50c0::ad1:ff', '2a10:50c0::ad2:ff'],
   },
   {
     name: 'AdGuard Family DNS',
-    family: true,
+    tags: ['UDP', 'Family'],
     data: ['94.140.14.15', '94.140.15.16', '2a10:50c0::bad1:ff', '2a10:50c0::bad2:ff'],
   },
   {
     name: 'Cloudflare Family DNS',
-    family: true,
+    tags: ['UDP', 'Family'],
     data: ['1.1.1.3', '1.0.0.3', '2606:4700:4700::1113', '2606:4700:4700::1003'],
   },
+  {
+    name: 'Cloudflare DoH',
+    tags: ['DoH'],
+    data: ['https://cloudflare-dns.com/dns-query'],
+  },
+  {
+    name: 'Google DoH',
+    tags: ['DoH'],
+    data: ['https://dns.google/dns-query'],
+  },
+  {
+    name: 'Quad9 Secure DoH',
+    tags: ['DoH', 'Malware'],
+    data: ['https://dns.quad9.net/dns-query'],
+  },
+  {
+    name: 'AdGuard DoH + DoQ',
+    tags: ['DoH', 'DoQ', 'Ads'],
+    data: ['https://dns.adguard-dns.com/dns-query', 'quic+local://dns.adguard-dns.com'],
+  },
+  {
+    name: 'Control D Ads DoH + DoQ',
+    tags: ['DoH', 'DoQ', 'Ads'],
+    data: ['https://freedns.controld.com/p2', 'quic+local://p2.freedns.controld.com'],
+  },
+  {
+    name: 'Control D Family DoH + DoQ',
+    tags: ['DoH', 'DoQ', 'Family'],
+    data: ['https://freedns.controld.com/p4', 'quic+local://p4.freedns.controld.com'],
+  },
 ];
 
+function tagLabel(tag: string, t: (key: string) => string): string {
+  if (tag === 'Family') return t('pages.xray.dns.dnsPresetFamily');
+  return tag;
+}
+
 export default function DnsPresetsModal({ open, onClose, onInstall }: DnsPresetsModalProps) {
   const { t } = useTranslation();
 
@@ -47,13 +82,21 @@ export default function DnsPresetsModal({ open, onClose, onInstall }: DnsPresets
       mask={{ closable: false }}
       onCancel={onClose}
     >
+      <Alert
+        type="warning"
+        showIcon
+        className="preset-warning"
+        message={t('pages.xray.dns.dnsLeakWarning')}
+      />
       <div className="preset-list">
         {PRESETS.map((preset) => (
           <div key={preset.name} className="preset-row">
             <Space size="small" align="center">
-              <Tag color={preset.family ? 'purple' : 'green'}>
-                {preset.family ? t('pages.xray.dns.dnsPresetFamily') : 'DNS'}
-              </Tag>
+              {preset.tags.map((tag) => (
+                <Tag key={tag} color={tag === 'Family' ? 'purple' : tag === 'UDP' ? 'orange' : 'green'}>
+                  {tagLabel(tag, t)}
+                </Tag>
+              ))}
               <span className="preset-name">{preset.name}</span>
             </Space>
             <Button type="primary" size="small" onClick={() => onInstall([...preset.data])}>

+ 7 - 1
frontend/src/pages/xray/dns/DnsTab.tsx

@@ -1,6 +1,6 @@
 import { useCallback, useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Button, Empty, Input, InputNumber, Modal, Select, Space, Switch, Table, Tabs } from 'antd';
+import { Alert, Button, Empty, Input, InputNumber, Modal, Select, Space, Switch, Table, Tabs } from 'antd';
 import {
   DatabaseOutlined,
   DeleteOutlined,
@@ -237,6 +237,12 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
             />
             {dnsEnabled && (
               <>
+                <Alert
+                  type="warning"
+                  showIcon
+                  message={t('pages.xray.dns.dnsLeakWarning')}
+                  style={{ marginBottom: 12 }}
+                />
                 <SettingListItem
                   paddings="small"
                   title={t('pages.xray.dns.tag')}

+ 19 - 0
frontend/src/test/dns-presets.test.ts

@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'vitest';
+
+import { PRESETS } from '@/pages/xray/dns/DnsPresetsModal';
+import { DnsObjectSchema, DnsServerObjectSchema } from '@/schemas/dns';
+
+describe('DNS presets', () => {
+  it('include encrypted presets accepted by xray DNS config', () => {
+    const servers = PRESETS.flatMap((preset) => preset.data);
+
+    expect(servers).toContain('https://freedns.controld.com/p2');
+    expect(servers).toContain('quic+local://p2.freedns.controld.com');
+    expect(servers).toContain('https://dns.google/dns-query');
+    expect(servers.every((server) => !server.startsWith('tls://'))).toBe(true);
+    expect(DnsObjectSchema.parse({ servers }).servers).toEqual(servers);
+    for (const server of servers) {
+      expect(DnsServerObjectSchema.parse({ address: server }).address).toBe(server);
+    }
+  });
+});

+ 2 - 1
internal/web/translation/ar-EG.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "العائلي",
         "clearAll": "حذف الكل",
         "clearAllTitle": "حذف جميع خوادم DNS؟",
-        "clearAllConfirm": "سيؤدي هذا إلى إزالة جميع خوادم DNS من القائمة. لا يمكن التراجع عن هذا الإجراء."
+        "clearAllConfirm": "سيؤدي هذا إلى إزالة جميع خوادم DNS من القائمة. لا يمكن التراجع عن هذا الإجراء.",
+        "dnsLeakWarning": "قد يتسرب DNS عبر localhost أو UDP/TCP غير المشفر أو وضع DoH/DoQ المحلي أو استعلامات الرجوع أو EDNS client IP. استخدم DoH عبر التوجيه، وثبت أسماء المحللات في hosts، وعطل الرجوع عندما تكون الخصوصية مهمة."
       },
       "fakedns": {
         "add": "أضف Fake DNS",

+ 2 - 1
internal/web/translation/en-US.json

@@ -1975,7 +1975,8 @@
         "dnsPresetFamily": "Family",
         "clearAll": "Delete All",
         "clearAllTitle": "Delete all DNS servers?",
-        "clearAllConfirm": "This removes every DNS server from the list. This cannot be undone."
+        "clearAllConfirm": "This removes every DNS server from the list. This cannot be undone.",
+        "dnsLeakWarning": "DNS can leak through localhost, plain UDP/TCP, local-mode DoH/DoQ, fallback queries, or EDNS client IP. Use routed DoH, hosts pins, and disable fallback when privacy matters."
       },
       "fakedns": {
         "add": "Add Fake DNS",

+ 2 - 1
internal/web/translation/es-ES.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "Familiar",
         "clearAll": "Eliminar todos",
         "clearAllTitle": "¿Eliminar todos los servidores DNS?",
-        "clearAllConfirm": "Esto eliminará todos los servidores DNS de la lista. No se puede deshacer."
+        "clearAllConfirm": "Esto eliminará todos los servidores DNS de la lista. No se puede deshacer.",
+        "dnsLeakWarning": "El DNS puede filtrarse por localhost, UDP/TCP sin cifrar, DoH/DoQ en modo local, consultas de respaldo o EDNS client IP. Usa DoH enrutado, fija los resolutores en hosts y desactiva el respaldo cuando la privacidad importe."
       },
       "fakedns": {
         "add": "Agregar DNS Falso",

+ 2 - 1
internal/web/translation/fa-IR.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "خانوادگی",
         "clearAll": "حذف همه",
         "clearAllTitle": "حذف همه سرورهای DNS؟",
-        "clearAllConfirm": "این کار همه سرورهای DNS را از لیست حذف می‌کند و قابل بازگشت نیست."
+        "clearAllConfirm": "این کار همه سرورهای DNS را از لیست حذف می‌کند و قابل بازگشت نیست.",
+        "dnsLeakWarning": "DNS ممکن است از طریق localhost، UDP/TCP ساده، حالت محلی DoH/DoQ، درخواست‌های fallback یا EDNS client IP نشت کند. برای حریم خصوصی از DoH مسیریابی‌شده، مقدارهای hosts ثابت و غیرفعال‌سازی fallback استفاده کنید."
       },
       "fakedns": {
         "add": "افزودن دی‌ان‌اس جعلی",

+ 2 - 1
internal/web/translation/id-ID.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "Keluarga",
         "clearAll": "Hapus Semua",
         "clearAllTitle": "Hapus semua server DNS?",
-        "clearAllConfirm": "Ini akan menghapus semua server DNS dari daftar. Tidak dapat dibatalkan."
+        "clearAllConfirm": "Ini akan menghapus semua server DNS dari daftar. Tidak dapat dibatalkan.",
+        "dnsLeakWarning": "DNS dapat bocor lewat localhost, UDP/TCP polos, DoH/DoQ mode lokal, kueri fallback, atau EDNS client IP. Gunakan DoH yang dirutekan, pin resolver di hosts, dan nonaktifkan fallback saat privasi penting."
       },
       "fakedns": {
         "add": "Tambahkan DNS Palsu",

+ 2 - 1
internal/web/translation/ja-JP.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "ファミリー",
         "clearAll": "すべて削除",
         "clearAllTitle": "すべての DNS サーバを削除しますか?",
-        "clearAllConfirm": "リストからすべての DNS サーバが削除されます。この操作は元に戻せません。"
+        "clearAllConfirm": "リストからすべての DNS サーバが削除されます。この操作は元に戻せません。",
+        "dnsLeakWarning": "DNS は localhost、暗号化なしの UDP/TCP、ローカルモードの DoH/DoQ、フォールバック問い合わせ、EDNS client IP から漏れる可能性があります。プライバシー重視ではルーティングされた DoH、hosts 固定、フォールバック無効化を使ってください。"
       },
       "fakedns": {
         "add": "フェイクDNS追加",

+ 2 - 1
internal/web/translation/pt-BR.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "Familiar",
         "clearAll": "Remover Todos",
         "clearAllTitle": "Remover todos os servidores DNS?",
-        "clearAllConfirm": "Isso remove todos os servidores DNS da lista. Não pode ser desfeito."
+        "clearAllConfirm": "Isso remove todos os servidores DNS da lista. Não pode ser desfeito.",
+        "dnsLeakWarning": "DNS pode vazar por localhost, UDP/TCP sem criptografia, DoH/DoQ em modo local, consultas de fallback ou EDNS client IP. Use DoH roteado, fixe resolvedores em hosts e desative fallback quando privacidade importar."
       },
       "fakedns": {
         "add": "Adicionar Fake DNS",

+ 2 - 1
internal/web/translation/ru-RU.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "Семейный",
         "clearAll": "Удалить все",
         "clearAllTitle": "Удалить все DNS-серверы?",
-        "clearAllConfirm": "Все DNS-серверы будут удалены из списка. Это действие нельзя отменить."
+        "clearAllConfirm": "Все DNS-серверы будут удалены из списка. Это действие нельзя отменить.",
+        "dnsLeakWarning": "DNS может утекать через localhost, обычный UDP/TCP, локальный режим DoH/DoQ, fallback-запросы или EDNS client IP. Для приватности используйте маршрутизируемый DoH, закрепляйте резолверы в hosts и отключайте fallback."
       },
       "fakedns": {
         "add": "Создать Fake DNS",

+ 2 - 1
internal/web/translation/tr-TR.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "Aile",
         "clearAll": "Tümünü Sil",
         "clearAllTitle": "Tüm DNS sunucularını sil?",
-        "clearAllConfirm": "Bu işlem tüm DNS sunucularını listeden kaldırır. Geri alınamaz."
+        "clearAllConfirm": "Bu işlem tüm DNS sunucularını listeden kaldırır. Geri alınamaz.",
+        "dnsLeakWarning": "DNS; localhost, düz UDP/TCP, yerel mod DoH/DoQ, fallback sorguları veya EDNS client IP üzerinden sızabilir. Gizlilik önemliyse yönlendirilmiş DoH, hosts sabitlemeleri ve kapalı fallback kullanın."
       },
       "fakedns": {
         "add": "Sahte DNS Ekle",

+ 2 - 1
internal/web/translation/uk-UA.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "Сімейний",
         "clearAll": "Видалити всі",
         "clearAllTitle": "Видалити всі DNS-сервери?",
-        "clearAllConfirm": "Усі DNS-сервери буде видалено зі списку. Дію не можна скасувати."
+        "clearAllConfirm": "Усі DNS-сервери буде видалено зі списку. Дію не можна скасувати.",
+        "dnsLeakWarning": "DNS може витікати через localhost, звичайний UDP/TCP, локальний режим DoH/DoQ, fallback-запити або EDNS client IP. Для приватності використовуйте маршрутизований DoH, фіксацію hosts і вимикайте fallback."
       },
       "fakedns": {
         "add": "Додати підроблений DNS",

+ 2 - 1
internal/web/translation/vi-VN.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "Gia đình",
         "clearAll": "Xóa tất cả",
         "clearAllTitle": "Xóa tất cả máy chủ DNS?",
-        "clearAllConfirm": "Thao tác này sẽ xóa toàn bộ máy chủ DNS khỏi danh sách. Không thể hoàn tác."
+        "clearAllConfirm": "Thao tác này sẽ xóa toàn bộ máy chủ DNS khỏi danh sách. Không thể hoàn tác.",
+        "dnsLeakWarning": "DNS có thể rò rỉ qua localhost, UDP/TCP không mã hóa, DoH/DoQ chế độ cục bộ, truy vấn fallback hoặc EDNS client IP. Hãy dùng DoH được định tuyến, ghim resolver trong hosts và tắt fallback khi cần riêng tư."
       },
       "fakedns": {
         "add": "Thêm DNS giả",

+ 2 - 1
internal/web/translation/zh-CN.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "家庭",
         "clearAll": "删除全部",
         "clearAllTitle": "删除所有 DNS 服务器?",
-        "clearAllConfirm": "此操作将从列表中删除所有 DNS 服务器,且无法撤销。"
+        "clearAllConfirm": "此操作将从列表中删除所有 DNS 服务器,且无法撤销。",
+        "dnsLeakWarning": "DNS 可能通过 localhost、明文 UDP/TCP、本地模式 DoH/DoQ、回退查询或 EDNS client IP 泄漏。重视隐私时请使用经路由的 DoH、在 hosts 中固定解析器,并禁用回退。"
       },
       "fakedns": {
         "add": "添加假 DNS",

+ 2 - 1
internal/web/translation/zh-TW.json

@@ -1858,7 +1858,8 @@
         "dnsPresetFamily": "家庭",
         "clearAll": "全部刪除",
         "clearAllTitle": "刪除所有 DNS 伺服器?",
-        "clearAllConfirm": "此操作將從清單中刪除所有 DNS 伺服器,無法復原。"
+        "clearAllConfirm": "此操作將從清單中刪除所有 DNS 伺服器,無法復原。",
+        "dnsLeakWarning": "DNS 可能透過 localhost、明文 UDP/TCP、本機模式 DoH/DoQ、回退查詢或 EDNS client IP 洩漏。重視隱私時請使用經路由的 DoH、在 hosts 固定解析器,並停用回退。"
       },
       "fakedns": {
         "add": "新增假 DNS",