diff --git a/frontend/src/pages/xray/dns/DnsServerModal.tsx b/frontend/src/pages/xray/dns/DnsServerModal.tsx index fb20231e6..f5cb64723 100644 --- a/frontend/src/pages/xray/dns/DnsServerModal.tsx +++ b/frontend/src/pages/xray/dns/DnsServerModal.tsx @@ -10,6 +10,7 @@ import { DnsQueryStrategySchema, DnsServerObjectInnerSchema, DnsServerObjectSchema, + isEncryptedDnsAddress, type DnsServerObject, } from '@/schemas/dns'; @@ -109,7 +110,6 @@ function valuesToWire(values: DnsServerForm): DnsServerValue { const out: Record = { address: values.address, - port: values.port, domains: values.domains.filter(Boolean), expectedIPs: values.expectedIPs.filter(Boolean), unexpectedIPs: values.unexpectedIPs.filter(Boolean), @@ -121,6 +121,7 @@ function valuesToWire(values: DnsServerForm): DnsServerValue { serveExpiredTTL: values.serveExpiredTTL, timeoutMs: values.timeoutMs, }; + if (!isEncryptedDnsAddress(values.address)) out.port = values.port; if (values.tag) out.tag = values.tag; if (values.clientIP) out.clientIP = values.clientIP; return out as DnsServerValue; diff --git a/frontend/src/schemas/dns.ts b/frontend/src/schemas/dns.ts index 3336414cd..12e3f6c21 100644 --- a/frontend/src/schemas/dns.ts +++ b/frontend/src/schemas/dns.ts @@ -14,9 +14,13 @@ const DnsHostValueSchema = z.union([z.string(), z.array(z.string())]); export const DnsHostsSchema = z.record(z.string(), DnsHostValueSchema); export type DnsHosts = z.infer; +export function isEncryptedDnsAddress(address: string): boolean { + return /^(https|https\+local|h2c|h2c\+local|quic\+local):\/\//i.test(address); +} + export const DnsServerObjectInnerSchema = z.object({ address: z.string(), - port: PortSchema.default(53), + port: PortSchema.optional(), domains: z.array(z.string()).optional(), expectedIPs: z.array(z.string()).optional(), unexpectedIPs: z.array(z.string()).optional(), @@ -41,7 +45,12 @@ export const DnsServerObjectSchema = z.preprocess( return val; }, DnsServerObjectInnerSchema, -); +).transform((v) => { + if (v.port === undefined && !isEncryptedDnsAddress(v.address)) { + return { ...v, port: 53 }; + } + return v; +}); export type DnsServerObject = z.infer; export const DnsServerEntrySchema = z.union([z.string(), DnsServerObjectSchema]); diff --git a/frontend/src/test/__snapshots__/dns.test.ts.snap b/frontend/src/test/__snapshots__/dns.test.ts.snap index 8ecafbb0c..38b116f8f 100644 --- a/frontend/src/test/__snapshots__/dns.test.ts.snap +++ b/frontend/src/test/__snapshots__/dns.test.ts.snap @@ -41,7 +41,6 @@ exports[`DnsObjectSchema fixtures > parses full byte-stably 1`] = ` "address": "quic+local://dns.adguard.com", "disableCache": true, "finalQuery": true, - "port": 53, "serveExpiredTTL": 60, "serveStale": false, "timeoutMs": 5000, diff --git a/frontend/src/test/dns.test.ts b/frontend/src/test/dns.test.ts index 6d9b894f3..c46737a93 100644 --- a/frontend/src/test/dns.test.ts +++ b/frontend/src/test/dns.test.ts @@ -41,3 +41,45 @@ describe('DnsServerObjectSchema fixtures', () => { }); } }); + +describe('DnsServerObjectSchema port defaulting', () => { + it('defaults port 53 for a plain address', () => { + const parsed = DnsServerObjectSchema.parse({ address: '8.8.8.8' }); + expect(parsed.port).toBe(53); + }); + + it('defaults port 53 for a tcp address', () => { + const parsed = DnsServerObjectSchema.parse({ address: 'tcp://1.1.1.1' }); + expect(parsed.port).toBe(53); + }); + + it('omits port for a DoH (https://) address', () => { + const parsed = DnsServerObjectSchema.parse({ address: 'https://cloudflare-dns.com/dns-query' }); + expect(parsed.port).toBeUndefined(); + }); + + it('omits port for a DoHL (https+local://) address', () => { + const parsed = DnsServerObjectSchema.parse({ address: 'https+local://dns.google/dns-query' }); + expect(parsed.port).toBeUndefined(); + }); + + it('omits port for a DoQ (quic+local://) address', () => { + const parsed = DnsServerObjectSchema.parse({ address: 'quic+local://dns.adguard.com' }); + expect(parsed.port).toBeUndefined(); + }); + + it('omits port for an h2c and h2c+local address', () => { + expect(DnsServerObjectSchema.parse({ address: 'h2c://dns.example.com/dns-query' }).port).toBeUndefined(); + expect(DnsServerObjectSchema.parse({ address: 'h2c+local://dns.example.com/dns-query' }).port).toBeUndefined(); + }); + + it('omits port for an uppercase encrypted scheme', () => { + const parsed = DnsServerObjectSchema.parse({ address: 'HTTPS://dns.google/dns-query' }); + expect(parsed.port).toBeUndefined(); + }); + + it('preserves an explicit port on an encrypted address', () => { + const parsed = DnsServerObjectSchema.parse({ address: 'https://dns.google/dns-query', port: 8443 }); + expect(parsed.port).toBe(8443); + }); +});