Files
3x-ui/frontend/src/schemas/dns.ts
T
Matt Van Horn ae0da4c51f fix: stop forcing port 53 on DoH/DoQ DNS server entries (#5950)
Object-form DNS server entries always received port: 53, because
DnsServerObjectInnerSchema defaulted the port unconditionally and the
DnsServerModal wire adapter always wrote it. Per Xray-core, encrypted
schemes must not carry a port field; a non-standard port is embedded in
the URL instead.

Default the port to 53 only for non-encrypted addresses and omit it for
the encrypted DNS schemes Xray dispatches without a port - https,
https+local, h2c, h2c+local and quic+local - both in the Zod schema and
in the modal's valuesToWire adapter. Schemes are matched
case-insensitively to mirror Xray-core's EqualFold comparison. A shared
isEncryptedDnsAddress helper backs both paths.

Fixes #5920

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-14 12:55:10 +02:00

74 lines
2.5 KiB
TypeScript

import { z } from 'zod';
import { PortSchema } from '@/schemas/primitives';
export const DnsQueryStrategySchema = z.enum([
'UseIP',
'UseIPv4',
'UseIPv6',
'UseSystem',
]);
export type DnsQueryStrategy = z.infer<typeof DnsQueryStrategySchema>;
const DnsHostValueSchema = z.union([z.string(), z.array(z.string())]);
export const DnsHostsSchema = z.record(z.string(), DnsHostValueSchema);
export type DnsHosts = z.infer<typeof DnsHostsSchema>;
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.optional(),
domains: z.array(z.string()).optional(),
expectedIPs: z.array(z.string()).optional(),
unexpectedIPs: z.array(z.string()).optional(),
skipFallback: z.boolean().optional(),
finalQuery: z.boolean().optional(),
tag: z.string().optional(),
clientIP: z.string().optional(),
queryStrategy: DnsQueryStrategySchema.optional(),
disableCache: z.boolean().optional(),
timeoutMs: z.number().int().min(0).default(4000),
serveStale: z.boolean().optional(),
serveExpiredTTL: z.number().int().min(0).optional(),
});
export const DnsServerObjectSchema = z.preprocess(
(val) => {
if (typeof val !== 'object' || val === null || Array.isArray(val)) return val;
const v = val as Record<string, unknown>;
if (v.expectIPs && !v.expectedIPs) {
return { ...v, expectedIPs: v.expectIPs };
}
return val;
},
DnsServerObjectInnerSchema,
).transform((v) => {
if (v.port === undefined && !isEncryptedDnsAddress(v.address)) {
return { ...v, port: 53 };
}
return v;
});
export type DnsServerObject = z.infer<typeof DnsServerObjectSchema>;
export const DnsServerEntrySchema = z.union([z.string(), DnsServerObjectSchema]);
export type DnsServerEntry = z.infer<typeof DnsServerEntrySchema>;
export const DnsObjectSchema = z.object({
tag: z.string().optional(),
hosts: DnsHostsSchema.optional(),
servers: z.array(DnsServerEntrySchema).optional(),
clientIp: z.string().optional(),
queryStrategy: DnsQueryStrategySchema.default('UseIP'),
disableCache: z.boolean().default(false),
disableFallback: z.boolean().default(false),
disableFallbackIfMatch: z.boolean().default(false),
enableParallelQuery: z.boolean().default(false),
useSystemHosts: z.boolean().default(false),
serveStale: z.boolean().default(false),
serveExpiredTTL: z.number().int().min(0).default(0),
});
export type DnsObject = z.infer<typeof DnsObjectSchema>;