mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-13 01:14:59 +03:00
feat(xray): edit Lua DNS and routing scripts
xray-core 26.10.10 can hand DNS and routing to Lua: dns.script and routing.script name a .lua file (absolute, or found next to the Xray binary) that defines HandleDNSQuery / HandleRoute, and a DNS server's id is how a DNS script picks it. The keys round-tripped through the JSON template but had no field. - DNS: a Lua Script field, and an ID field in the server modal; - Basics: a Routing Script field beside the routing strategy; - Routing: a warning above the rules while a script is set, since the core then routes only through the script and skips every rule, the panel's api rule included. The descriptions say the script must still send the api inbound to the api outbound, and that Xray will not start if the file is missing. An empty field drops the key, because the core rejects an empty path.
This commit is contained in:
@@ -160,6 +160,7 @@ export default function BasicsTab({
|
||||
);
|
||||
|
||||
const routingStrategy = templateSettings?.routing?.domainStrategy ?? 'AsIs';
|
||||
const routingScript = templateSettings?.routing?.script ?? '';
|
||||
const log = (templateSettings?.log || {}) as Record<string, unknown>;
|
||||
const policy = (templateSettings?.policy?.system || {}) as Record<string, boolean>;
|
||||
const level0 = (templateSettings?.policy?.levels?.['0'] || {}) as Record<string, unknown>;
|
||||
@@ -259,6 +260,25 @@ export default function BasicsTab({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingListItem
|
||||
title={t('pages.xray.routingScript')}
|
||||
description={t('pages.xray.routingScriptDesc')}
|
||||
paddings="small"
|
||||
control={
|
||||
<Input
|
||||
value={routingScript}
|
||||
placeholder="routing.lua"
|
||||
onChange={(e) => {
|
||||
const next = e.target.value;
|
||||
mutate((tt) => {
|
||||
if (!tt.routing) return;
|
||||
if (next.trim() === '') delete tt.routing.script;
|
||||
else tt.routing.script = next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingListItem
|
||||
title={t('pages.xray.outboundTestUrl')}
|
||||
description={t('pages.xray.outboundTestUrlDesc')}
|
||||
|
||||
@@ -188,6 +188,13 @@ export default function DnsServerModal({
|
||||
<InputNumber min={1} max={65535} />
|
||||
</FormField>
|
||||
)}
|
||||
<FormField
|
||||
label={t('pages.xray.dns.serverId')}
|
||||
tooltip={t('pages.xray.dns.serverIdDesc')}
|
||||
name="id"
|
||||
>
|
||||
<Input />
|
||||
</FormField>
|
||||
<FormField label={t('pages.xray.dns.tag')} name="tag">
|
||||
<Input />
|
||||
</FormField>
|
||||
|
||||
@@ -297,6 +297,18 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.dns.script')}
|
||||
description={t('pages.xray.dns.scriptDesc')}
|
||||
control={
|
||||
<Input
|
||||
value={dns?.script ?? ''}
|
||||
placeholder="dns.lua"
|
||||
onChange={(e) => setDnsField('script', e.target.value, true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{(
|
||||
[
|
||||
[
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Dropdown, Modal, Space, Table, Tabs, message } from 'antd';
|
||||
import { Alert, Button, Dropdown, Modal, Space, Table, Tabs, message } from 'antd';
|
||||
import {
|
||||
AimOutlined,
|
||||
ControlOutlined,
|
||||
@@ -64,6 +64,7 @@ export default function RoutingTab({
|
||||
moved: false,
|
||||
});
|
||||
|
||||
const routingScript = templateSettings?.routing?.script ?? '';
|
||||
const rules = useMemo(
|
||||
() => (templateSettings?.routing?.rules || []) as RoutingRule[],
|
||||
[templateSettings?.routing?.rules],
|
||||
@@ -349,6 +350,14 @@ export default function RoutingTab({
|
||||
<>
|
||||
{modalContextHolder}
|
||||
{messageContextHolder}
|
||||
{routingScript.trim() !== '' && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
title={t('pages.xray.routingScriptActive', { section: t('pages.xray.basicTemplate') })}
|
||||
/>
|
||||
)}
|
||||
<Tabs
|
||||
defaultActiveKey="basic"
|
||||
items={[
|
||||
|
||||
@@ -27,6 +27,7 @@ export const XraySettingsValueSchema = z
|
||||
rules: z.array(RuleObjectSchema).optional(),
|
||||
balancers: z.array(BalancerObjectSchema).optional(),
|
||||
domainStrategy: z.string().optional(),
|
||||
script: z.string().optional(),
|
||||
})
|
||||
.loose()
|
||||
.optional(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
import { act, fireEvent, screen } from '@testing-library/react';
|
||||
|
||||
import DnsServerModal from '@/pages/xray/dns/DnsServerModal';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
@@ -27,4 +27,19 @@ describe('DnsServerModal', () => {
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||
expect(onConfirm.mock.calls[0][0]).toMatchObject({ address: '1.1.1.1', id: 'cf' });
|
||||
});
|
||||
|
||||
it('saves an id typed for a plain server', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
renderWithProviders(
|
||||
<DnsServerModal open server="1.1.1.1" isEdit onClose={() => {}} onConfirm={onConfirm} />,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('ID'), { target: { value: 'cf' } });
|
||||
const ok = document.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement;
|
||||
await act(async () => {
|
||||
fireEvent.click(ok);
|
||||
});
|
||||
|
||||
expect(onConfirm.mock.calls[0][0]).toMatchObject({ address: '1.1.1.1', id: 'cf' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
|
||||
import DnsTab from '@/pages/xray/dns/DnsTab';
|
||||
@@ -127,4 +127,28 @@ describe('DnsTab', () => {
|
||||
|
||||
expect(screen.queryByLabelText('Domain (e.g. domain:example.com)')).toBeNull();
|
||||
});
|
||||
|
||||
// xray-core 26.10.10 dns.script is a file path; an empty one fails the core build.
|
||||
it('writes the Lua script path and drops the key once cleared', () => {
|
||||
const seen = vi.fn();
|
||||
const latest = () => seen.mock.lastCall?.[0] as { dns: Record<string, unknown> };
|
||||
function Harness() {
|
||||
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
|
||||
withHosts({}),
|
||||
);
|
||||
useEffect(() => seen(templateSettings), [templateSettings]);
|
||||
const updateTemplate: SetTemplate = (next) => {
|
||||
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
|
||||
};
|
||||
return <DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />;
|
||||
}
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
const script = screen.getByRole('textbox', { name: 'Lua Script' });
|
||||
fireEvent.change(script, { target: { value: '/etc/xray/dns.lua' } });
|
||||
expect(latest().dns.script).toBe('/etc/xray/dns.lua');
|
||||
|
||||
fireEvent.change(script, { target: { value: '' } });
|
||||
expect(latest().dns).not.toHaveProperty('script');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import BasicsTab from '@/pages/xray/basics/BasicsTab';
|
||||
import RoutingTab from '@/pages/xray/routing/RoutingTab';
|
||||
import type { SetTemplate, XraySettingsValue } from '@/hooks/useXraySetting';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
|
||||
const SCRIPT_BANNER = 'A routing script is set under Basics, so Xray ignores these rules.';
|
||||
|
||||
function renderRoutingRules(routing: Record<string, unknown>) {
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
renderWithProviders(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RoutingTab
|
||||
templateSettings={{ routing } as unknown as XraySettingsValue}
|
||||
setTemplateSettings={vi.fn()}
|
||||
inboundTags={[]}
|
||||
clientReverseTags={[]}
|
||||
isMobile={false}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Routing Rules/ }));
|
||||
}
|
||||
|
||||
// xray-core 26.10.10 routes only through routing.script once it is set, skipping
|
||||
// every rule, the panel's api rule included.
|
||||
describe('routing script', () => {
|
||||
it('writes the script path from Basics and drops the key once cleared', () => {
|
||||
const seen = vi.fn();
|
||||
const latest = () => seen.mock.lastCall?.[0] as { routing: Record<string, unknown> };
|
||||
function Harness() {
|
||||
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>({
|
||||
routing: { domainStrategy: 'AsIs', rules: [] },
|
||||
outbounds: [],
|
||||
} as unknown as XraySettingsValue);
|
||||
useEffect(() => seen(templateSettings), [templateSettings]);
|
||||
const updateTemplate: SetTemplate = (next) => {
|
||||
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
|
||||
};
|
||||
return (
|
||||
<BasicsTab
|
||||
templateSettings={templateSettings}
|
||||
setTemplateSettings={updateTemplate}
|
||||
outboundTestUrl=""
|
||||
onChangeOutboundTestUrl={vi.fn()}
|
||||
onResetDefault={vi.fn()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
const script = screen.getByRole('textbox', { name: 'Routing Script' });
|
||||
fireEvent.change(script, { target: { value: 'route.lua' } });
|
||||
expect(latest().routing.script).toBe('route.lua');
|
||||
|
||||
fireEvent.change(script, { target: { value: ' ' } });
|
||||
expect(latest().routing).not.toHaveProperty('script');
|
||||
});
|
||||
|
||||
it('warns above the rules while a script is set', () => {
|
||||
renderRoutingRules({ rules: [], script: 'route.lua' });
|
||||
|
||||
expect(screen.getByText(SCRIPT_BANNER)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows no warning without a script', () => {
|
||||
renderRoutingRules({ rules: [] });
|
||||
|
||||
expect(screen.queryByText(SCRIPT_BANNER)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user