diff --git a/frontend/src/lib/xray/outbound-form-adapter.ts b/frontend/src/lib/xray/outbound-form-adapter.ts
index 254880e0..200f6974 100644
--- a/frontend/src/lib/xray/outbound-form-adapter.ts
+++ b/frontend/src/lib/xray/outbound-form-adapter.ts
@@ -292,19 +292,20 @@ function blackholeFromWire(raw: Raw) {
function dnsRuleFromWire(raw: unknown): DnsRuleForm {
const r = asObject(raw);
- const qtype = Array.isArray(r.qtype)
- ? r.qtype.map((x) => String(x)).join(',')
- : typeof r.qtype === 'number'
- ? String(r.qtype)
- : asString(r.qtype);
+ const rawQType = r.qType ?? r.qtype;
+ const qType = Array.isArray(rawQType)
+ ? rawQType.map((x) => String(x)).join(',')
+ : typeof rawQType === 'number'
+ ? String(rawQType)
+ : asString(rawQType);
const domain = Array.isArray(r.domain)
? r.domain.map((x) => asString(x)).join(',')
: asString(r.domain);
const action = asString(r.action, 'direct');
- const validAction = ['direct', 'reject', 'rejectIPv4', 'rejectIPv6'].includes(action)
+ const validAction = ['direct', 'drop', 'return', 'hijack'].includes(action)
? action
: 'direct';
- return { action: validAction as DnsRuleForm['action'], qtype, domain };
+ return { action: validAction as DnsRuleForm['action'], qType, domain, rCode: asNumber(r.rCode, 0) };
}
function dnsFromWire(raw: Raw): DnsOutboundFormSettings {
@@ -536,16 +537,17 @@ function blackholeToWire(s: { type: '' | 'none' | 'http' }) {
}
function dnsRuleToWire(r: DnsRuleForm) {
- const action = ['direct', 'reject', 'rejectIPv4', 'rejectIPv6'].includes(r.action)
+ const action = ['direct', 'drop', 'return', 'hijack'].includes(r.action)
? r.action
: 'direct';
const result: Raw = { action };
- const qtype = r.qtype.trim();
- if (qtype) {
- result.qtype = /^\d+$/.test(qtype) ? Number(qtype) : qtype;
+ const qType = r.qType.trim();
+ if (qType) {
+ result.qType = /^\d+$/.test(qType) ? Number(qType) : qType;
}
const domains = r.domain.split(',').map((d) => d.trim()).filter(Boolean);
if (domains.length > 0) result.domain = domains;
+ if (r.rCode > 0) result.rCode = r.rCode;
return result;
}
diff --git a/frontend/src/pages/xray/outbounds/protocols/dns.tsx b/frontend/src/pages/xray/outbounds/protocols/dns.tsx
index 85940d15..3d3cc8c7 100644
--- a/frontend/src/pages/xray/outbounds/protocols/dns.tsx
+++ b/frontend/src/pages/xray/outbounds/protocols/dns.tsx
@@ -35,7 +35,7 @@ export default function DnsFields() {
size="small"
type="primary"
icon={}
- onClick={() => add({ action: 'direct', qtype: '', domain: '' })}
+ onClick={() => add({ action: 'direct', qType: '', domain: '', rCode: 0 })}
/>
{fields.map((field, index) => (
@@ -54,12 +54,15 @@ export default function DnsFields() {
options={DNSRuleActions.map((a) => ({ value: a, label: a }))}
/>
-
+
+
+
+
))}
>
diff --git a/frontend/src/schemas/forms/outbound-form.ts b/frontend/src/schemas/forms/outbound-form.ts
index 14e94dd9..4bff024d 100644
--- a/frontend/src/schemas/forms/outbound-form.ts
+++ b/frontend/src/schemas/forms/outbound-form.ts
@@ -29,7 +29,7 @@ import {
// the adapter wraps them as { reverse: { tag, sniffing } } on the wire.
// - blackhole `type` ('' | 'none' | 'http') is flat; the adapter wraps it
// as { response: { type } } on the wire (omitted when empty).
-// - DNS rules carry `qtype` and `domain` as comma-joined strings (matches
+// - DNS rules carry `qType` and `domain` as comma-joined strings (matches
// the legacy DNSRule UI). The adapter normalizes them on submit.
//
// All flat-form settings types are documented inline so the adapter has a
@@ -186,12 +186,13 @@ export const BlackholeOutboundFormSettingsSchema = z.object({
});
export type BlackholeOutboundFormSettings = z.infer;
-// DNS rules: form holds qtype + domain as joined strings (the legacy UI
+// DNS rules: form holds qType + domain as joined strings (the legacy UI
// binds to ). Adapter parses them on submit per the DNSRule class.
export const DnsRuleFormSchema = z.object({
action: DNSRuleActionSchema.default('direct'),
- qtype: z.string().default(''),
+ qType: z.string().default(''),
domain: z.string().default(''),
+ rCode: z.number().int().min(0).max(65535).default(0),
});
export type DnsRuleForm = z.infer;
diff --git a/frontend/src/schemas/primitives/options.ts b/frontend/src/schemas/primitives/options.ts
index 79cce3c5..bfd51ecb 100644
--- a/frontend/src/schemas/primitives/options.ts
+++ b/frontend/src/schemas/primitives/options.ts
@@ -59,7 +59,7 @@ export const Address_Port_Strategy = Object.freeze({
TXT_PORT_AND_ADDRESS: 'TxtPortAndAddress',
});
-export const DNSRuleActions = Object.freeze(['direct', 'drop', 'reject', 'hijack'] as const);
+export const DNSRuleActions = Object.freeze(['direct', 'drop', 'return', 'hijack'] as const);
export const TLS_VERSION_OPTION = Object.freeze({
TLS10: '1.0',
diff --git a/frontend/src/schemas/protocols/outbound/dns.ts b/frontend/src/schemas/protocols/outbound/dns.ts
index 58dcecb2..c241b788 100644
--- a/frontend/src/schemas/protocols/outbound/dns.ts
+++ b/frontend/src/schemas/protocols/outbound/dns.ts
@@ -2,15 +2,16 @@ import { z } from 'zod';
import { PortSchema } from '@/schemas/primitives';
-export const DNSRuleActionSchema = z.enum(['direct', 'reject', 'rejectIPv4', 'rejectIPv6']);
+export const DNSRuleActionSchema = z.enum(['direct', 'drop', 'return', 'hijack']);
-// On the wire `qtype` is either a number (DNS type code) or a string like
+// On the wire `qType` is either a number (DNS type code) or a string like
// "A"/"AAAA"/"TXT"; the panel normalizes numeric strings to numbers in
// toJson. `domain` is a string[] (split from a comma-joined input).
export const DNSRuleSchema = z.object({
action: DNSRuleActionSchema.default('direct'),
- qtype: z.union([z.string(), z.number().int()]).optional(),
+ qType: z.union([z.string(), z.number().int()]).optional(),
domain: z.array(z.string()).optional(),
+ rCode: z.number().int().min(0).max(65535).optional(),
});
export type DNSRule = z.infer;
diff --git a/frontend/src/test/outbound-form-adapter.test.ts b/frontend/src/test/outbound-form-adapter.test.ts
index 738ddfdc..8fe31f00 100644
--- a/frontend/src/test/outbound-form-adapter.test.ts
+++ b/frontend/src/test/outbound-form-adapter.test.ts
@@ -197,7 +197,7 @@ describe('outbound-form-adapter: round-trip', () => {
expect(withType.settings).toEqual({ response: { type: 'http' } });
});
- it('dns rules normalize qtype numeric strings and split domains', () => {
+ it('dns rules normalize qType numeric strings, split domains, carry rCode', () => {
const wire = {
protocol: 'dns',
settings: {
@@ -205,16 +205,26 @@ describe('outbound-form-adapter: round-trip', () => {
rewriteAddress: '1.1.1.1',
rewritePort: 53,
rules: [
- { action: 'direct', qtype: 'A,AAAA', domain: ['example.com', 'ext.org'] },
- { action: 'reject', qtype: 28, domain: 'blocked.com' },
+ { action: 'direct', qType: 'A,AAAA', domain: ['example.com', 'ext.org'] },
+ { action: 'return', qType: 28, domain: 'blocked.com', rCode: 3 },
],
},
};
const back = formValuesToWirePayload(rawOutboundToFormValues(wire));
const settings = back.settings as Record;
const rules = settings.rules as Array>;
- expect(rules[0]).toEqual({ action: 'direct', qtype: 'A,AAAA', domain: ['example.com', 'ext.org'] });
- expect(rules[1]).toEqual({ action: 'reject', qtype: 28, domain: ['blocked.com'] });
+ expect(rules[0]).toEqual({ action: 'direct', qType: 'A,AAAA', domain: ['example.com', 'ext.org'] });
+ expect(rules[1]).toEqual({ action: 'return', qType: 28, domain: ['blocked.com'], rCode: 3 });
+ });
+
+ it('dns rules read the legacy qtype wire key for back-compat', () => {
+ const wire = {
+ protocol: 'dns',
+ settings: { rules: [{ action: 'direct', qtype: 'TXT' }] },
+ };
+ const back = formValuesToWirePayload(rawOutboundToFormValues(wire));
+ const rules = (back.settings as Record).rules as Array>;
+ expect(rules[0]).toEqual({ action: 'direct', qType: 'TXT' });
});
it('freedom emits domainStrategy/redirect/fragment conditionally', () => {