mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-24 06:45:01 +03:00
feat(sub): opt-in dns and sniffer from remote Clash rules (#6868)
Public Mihomo templates ship rules, rule-providers and proxy-groups together with a dns block those rules depend on (fake-ip ranges, nameserver policy). Pasting a template inline keeps its dns, but pointing the global routing rules at its URL dropped it, so URL users had to give up auto-refresh to keep it. A new switch, subClashRemoteDNS (off by default), also takes the remote dns and sniffer maps. It is opt-in because a remote dns block decides where clients send every lookup. A malformed dns or sniffer value is skipped, not the whole template. The component-test teardown now outlasts AntD Form's 10ms error debounce, which otherwise fired after jsdom was gone.
This commit is contained in:
@@ -242,6 +242,9 @@
|
||||
"subClashPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"subClashRemoteDNS": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subClashRules": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -662,6 +665,7 @@
|
||||
"subClashEnable",
|
||||
"subClashEnableRouting",
|
||||
"subClashPath",
|
||||
"subClashRemoteDNS",
|
||||
"subClashRules",
|
||||
"subClashURI",
|
||||
"subClashUserAgentRegex",
|
||||
@@ -1018,6 +1022,9 @@
|
||||
"subClashPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"subClashRemoteDNS": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subClashRules": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1446,6 +1453,7 @@
|
||||
"subClashEnable",
|
||||
"subClashEnableRouting",
|
||||
"subClashPath",
|
||||
"subClashRemoteDNS",
|
||||
"subClashRules",
|
||||
"subClashURI",
|
||||
"subClashUserAgentRegex",
|
||||
|
||||
@@ -65,6 +65,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"subClashEnable": false,
|
||||
"subClashEnableRouting": false,
|
||||
"subClashPath": "",
|
||||
"subClashRemoteDNS": false,
|
||||
"subClashRules": "",
|
||||
"subClashURI": "",
|
||||
"subClashUserAgentRegex": "",
|
||||
@@ -252,6 +253,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
||||
"subClashEnable": false,
|
||||
"subClashEnableRouting": false,
|
||||
"subClashPath": "",
|
||||
"subClashRemoteDNS": false,
|
||||
"subClashRules": "",
|
||||
"subClashURI": "",
|
||||
"subClashUserAgentRegex": "",
|
||||
|
||||
@@ -216,6 +216,9 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subClashPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"subClashRemoteDNS": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subClashRules": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -636,6 +639,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subClashEnable",
|
||||
"subClashEnableRouting",
|
||||
"subClashPath",
|
||||
"subClashRemoteDNS",
|
||||
"subClashRules",
|
||||
"subClashURI",
|
||||
"subClashUserAgentRegex",
|
||||
@@ -992,6 +996,9 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subClashPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"subClashRemoteDNS": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"subClashRules": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1420,6 +1427,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"subClashEnable",
|
||||
"subClashEnableRouting",
|
||||
"subClashPath",
|
||||
"subClashRemoteDNS",
|
||||
"subClashRules",
|
||||
"subClashURI",
|
||||
"subClashUserAgentRegex",
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface AllSetting {
|
||||
subClashEnable: boolean;
|
||||
subClashEnableRouting: boolean;
|
||||
subClashPath: string;
|
||||
subClashRemoteDNS: boolean;
|
||||
subClashRules: string;
|
||||
subClashURI: string;
|
||||
subClashUserAgentRegex: string;
|
||||
@@ -261,6 +262,7 @@ export interface AllSettingView {
|
||||
subClashEnable: boolean;
|
||||
subClashEnableRouting: boolean;
|
||||
subClashPath: string;
|
||||
subClashRemoteDNS: boolean;
|
||||
subClashRules: string;
|
||||
subClashURI: string;
|
||||
subClashUserAgentRegex: string;
|
||||
|
||||
@@ -89,6 +89,7 @@ export const AllSettingSchema = z.object({
|
||||
subClashEnable: z.boolean(),
|
||||
subClashEnableRouting: z.boolean(),
|
||||
subClashPath: z.string(),
|
||||
subClashRemoteDNS: z.boolean(),
|
||||
subClashRules: z.string(),
|
||||
subClashURI: z.string(),
|
||||
subClashUserAgentRegex: z.string(),
|
||||
@@ -278,6 +279,7 @@ export const AllSettingViewSchema = z.object({
|
||||
subClashEnable: z.boolean(),
|
||||
subClashEnableRouting: z.boolean(),
|
||||
subClashPath: z.string(),
|
||||
subClashRemoteDNS: z.boolean(),
|
||||
subClashRules: z.string(),
|
||||
subClashURI: z.string(),
|
||||
subClashUserAgentRegex: z.string(),
|
||||
|
||||
@@ -74,6 +74,7 @@ export class AllSetting {
|
||||
subClashURI = '';
|
||||
subClashEnableRouting = false;
|
||||
subClashRules = '';
|
||||
subClashRemoteDNS = false;
|
||||
subJsonMux = '';
|
||||
subJsonRules = '';
|
||||
subJsonRoutingRules = '';
|
||||
|
||||
@@ -459,6 +459,16 @@ export default function SubscriptionGeneralTab({
|
||||
onChange={(e) => updateSetting({ subClashRules: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.subClashRemoteDNS')}
|
||||
description={t('pages.settings.subClashRemoteDNSDesc')}
|
||||
>
|
||||
<Switch
|
||||
checked={allSetting.subClashRemoteDNS}
|
||||
onChange={(v) => updateSetting({ subClashRemoteDNS: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -80,6 +80,7 @@ export const AllSettingSchema = z
|
||||
subClashURI: z.string().optional(),
|
||||
subClashEnableRouting: z.boolean().optional(),
|
||||
subClashRules: z.string().optional(),
|
||||
subClashRemoteDNS: z.boolean().optional(),
|
||||
subJsonMux: z.string().optional(),
|
||||
subJsonRules: z.string().optional(),
|
||||
subJsonRoutingRules: z.string().optional(),
|
||||
|
||||
@@ -95,6 +95,12 @@ afterEach(async () => {
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
/*
|
||||
* AntD Form debounces clearing its error list by 10ms (useDebounce via
|
||||
* rc-util useDelayState), and that timer survives unmount. Wait past it so
|
||||
* it fires while `window` still exists.
|
||||
*/
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
});
|
||||
|
||||
import { HttpUtil, Msg } from '@/utils';
|
||||
|
||||
@@ -167,6 +167,29 @@ describe('SubscriptionGeneralTab', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
'updates the Clash remote DNS opt-in, not the routing switch, when stored as %s',
|
||||
(enabled) => {
|
||||
const updateSetting = vi.fn();
|
||||
|
||||
renderWithProviders(
|
||||
<MemoryRouter initialEntries={['/settings#subscription']}>
|
||||
<SubscriptionGeneralTab
|
||||
allSetting={new AllSetting({ subClashEnableRouting: true, subClashRemoteDNS: enabled })}
|
||||
updateSetting={updateSetting}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Clash/ }));
|
||||
const remoteDNSSwitch = screen.getByRole('switch', { name: 'Use DNS from remote rules' });
|
||||
expect(remoteDNSSwitch.getAttribute('aria-checked')).toBe(String(enabled));
|
||||
fireEvent.click(remoteDNSSwitch);
|
||||
|
||||
expect(updateSetting).toHaveBeenCalledExactlyOnceWith({ subClashRemoteDNS: !enabled });
|
||||
},
|
||||
);
|
||||
|
||||
it('shows the Happ link gate from the QR settings deep link without enabling generation', () => {
|
||||
const updateSetting = vi.fn();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user