mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 18:24:10 +00:00
fix(outbounds): prevent freedom save crash, complete its fields (#4686)
Some checks are pending
CI / go-test (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
Some checks are pending
CI / go-test (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / frontend (push) Waiting to run
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
freedomToWire called Object.entries(s.fragment), but getFieldsValue(true) returns freedom settings without a fragment object when the Fragment switch is off (its sub-fields never register). That threw 'Cannot convert undefined or null to object' and silently killed the save. Guard fragment with a fallback so an unset value is treated as empty. While verifying against xray-core's freedom config, also: - add the missing userLevel field (schema, form schema, adapter, UI) - fix noise applyTo enum to ip/ipv4/ipv6 (xray rejects the old host/all) Closes #4686
This commit is contained in:
parent
c20ee00fa3
commit
f02018cfb7
5 changed files with 39 additions and 4 deletions
|
|
@ -266,6 +266,7 @@ function freedomFromWire(raw: Raw): FreedomOutboundFormSettings {
|
|||
return (allowed.includes(s) ? s : '') as FreedomOutboundFormSettings['domainStrategy'];
|
||||
})(),
|
||||
redirect: asString(raw.redirect),
|
||||
userLevel: asNumber(raw.userLevel, 0),
|
||||
proxyProtocol: ((): FreedomOutboundFormSettings['proxyProtocol'] => {
|
||||
const n = asNumber(raw.proxyProtocol, 0);
|
||||
return (n === 1 || n === 2) ? n : 0;
|
||||
|
|
@ -506,11 +507,15 @@ function freedomToWire(s: FreedomOutboundFormSettings) {
|
|||
// Legacy semantics: emit fragment only when the user actually populated
|
||||
// at least one of the four sub-fields. Defaults like packets='1-3' alone
|
||||
// are not enough — the modal's Fragment Switch sets all four together.
|
||||
const fragmentEntries = Object.entries(s.fragment).filter(([, v]) => v !== '' && v != null);
|
||||
const fragmentEnabled = !!s.fragment.length || !!s.fragment.interval || !!s.fragment.maxSplit;
|
||||
// getFieldsValue(true) may omit `fragment` when the switch is off, so the
|
||||
// fallback keeps Object.entries from throwing on undefined (issue #4686).
|
||||
const fragment: Partial<FreedomOutboundFormSettings['fragment']> = s.fragment ?? {};
|
||||
const fragmentEntries = Object.entries(fragment).filter(([, v]) => v !== '' && v != null);
|
||||
const fragmentEnabled = !!fragment.length || !!fragment.interval || !!fragment.maxSplit;
|
||||
return {
|
||||
domainStrategy: s.domainStrategy || undefined,
|
||||
redirect: s.redirect || undefined,
|
||||
userLevel: s.userLevel || undefined,
|
||||
proxyProtocol: s.proxyProtocol || undefined,
|
||||
fragment: fragmentEnabled ? Object.fromEntries(fragmentEntries) : undefined,
|
||||
noises: s.noises.length > 0 ? s.noises : undefined,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input, Select, Switch, type FormInstance } from 'antd';
|
||||
import { Button, Form, Input, InputNumber, Select, Switch, type FormInstance } from 'antd';
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
|
||||
import { OutboundDomainStrategies } from '@/schemas/primitives';
|
||||
|
|
@ -20,6 +20,9 @@ export default function FreedomFields({ form }: { form: FormInstance<OutboundFor
|
|||
<Form.Item label={t('pages.xray.outboundForm.redirect')} name={['settings', 'redirect']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('pages.xray.tun.userLevel')} name={['settings', 'userLevel']}>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t('pages.xray.outboundForm.proxyProtocol')} name={['settings', 'proxyProtocol']}>
|
||||
<Select
|
||||
options={[
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ export type FreedomFinalRuleForm = z.infer<typeof FreedomFinalRuleFormSchema>;
|
|||
export const FreedomOutboundFormSettingsSchema = z.object({
|
||||
domainStrategy: z.union([OutboundDomainStrategySchema, z.literal('')]).default(''),
|
||||
redirect: z.string().default(''),
|
||||
userLevel: z.number().int().min(0).default(0),
|
||||
proxyProtocol: z.number().int().min(0).max(2).default(0),
|
||||
fragment: FreedomFragmentSchema.default({
|
||||
packets: '1-3',
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const FreedomFragmentSchema = z.object({
|
|||
export type FreedomFragment = z.infer<typeof FreedomFragmentSchema>;
|
||||
|
||||
export const FreedomNoiseTypeSchema = z.enum(['rand', 'str', 'base64', 'hex']);
|
||||
export const FreedomNoiseApplyToSchema = z.enum(['ip', 'host', 'all']);
|
||||
export const FreedomNoiseApplyToSchema = z.enum(['ip', 'ipv4', 'ipv6']);
|
||||
|
||||
export const FreedomNoiseSchema = z.object({
|
||||
type: FreedomNoiseTypeSchema.default('rand'),
|
||||
|
|
@ -52,6 +52,7 @@ export type FreedomFinalRule = z.infer<typeof FreedomFinalRuleSchema>;
|
|||
export const FreedomOutboundSettingsSchema = z.object({
|
||||
domainStrategy: OutboundDomainStrategySchema.optional(),
|
||||
redirect: z.string().optional(),
|
||||
userLevel: z.number().int().min(0).optional(),
|
||||
proxyProtocol: z.number().optional(),
|
||||
fragment: FreedomFragmentSchema.optional(),
|
||||
noises: z.array(FreedomNoiseSchema).optional(),
|
||||
|
|
|
|||
|
|
@ -235,18 +235,43 @@ describe('outbound-form-adapter: round-trip', () => {
|
|||
settings: {
|
||||
domainStrategy: 'UseIPv4',
|
||||
redirect: '1.1.1.1',
|
||||
userLevel: 3,
|
||||
proxyProtocol: 2,
|
||||
fragment: { packets: 'tlshello', length: '100-200' },
|
||||
noises: [{ type: 'rand', packet: '10-20', delay: '10-16', applyTo: 'ipv4' }],
|
||||
},
|
||||
}));
|
||||
expect(filled.settings).toMatchObject({
|
||||
domainStrategy: 'UseIPv4',
|
||||
redirect: '1.1.1.1',
|
||||
userLevel: 3,
|
||||
proxyProtocol: 2,
|
||||
fragment: { packets: 'tlshello', length: '100-200' },
|
||||
noises: [{ type: 'rand', packet: '10-20', delay: '10-16', applyTo: 'ipv4' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('freedom tolerates settings without a fragment object (issue #4686)', () => {
|
||||
const values = {
|
||||
protocol: 'freedom',
|
||||
tag: 'direct',
|
||||
settings: {
|
||||
domainStrategy: '',
|
||||
redirect: '',
|
||||
proxyProtocol: 0,
|
||||
noises: [],
|
||||
finalRules: [
|
||||
{ action: 'block', network: '', port: '', ip: ['geoip:private'], blockDelay: '' },
|
||||
],
|
||||
},
|
||||
} as unknown as Parameters<typeof formValuesToWirePayload>[0];
|
||||
|
||||
expect(() => formValuesToWirePayload(values)).not.toThrow();
|
||||
const back = formValuesToWirePayload(values);
|
||||
expect((back.settings as { fragment?: unknown }).fragment).toBeUndefined();
|
||||
expect((back.settings as { finalRules?: unknown[] }).finalRules).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('freedom omits proxyProtocol when disabled (0)', () => {
|
||||
const round = formValuesToWirePayload(rawOutboundToFormValues({
|
||||
protocol: 'freedom',
|
||||
|
|
|
|||
Loading…
Reference in a new issue