From f02018cfb7d1681c5411849556388b95eae7eaa2 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 31 May 2026 19:50:50 +0200 Subject: [PATCH] fix(outbounds): prevent freedom save crash, complete its fields (#4686) 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 --- .../src/lib/xray/outbound-form-adapter.ts | 9 +++++-- .../xray/outbounds/protocols/freedom.tsx | 5 +++- frontend/src/schemas/forms/outbound-form.ts | 1 + .../src/schemas/protocols/outbound/freedom.ts | 3 ++- .../src/test/outbound-form-adapter.test.ts | 25 +++++++++++++++++++ 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/xray/outbound-form-adapter.ts b/frontend/src/lib/xray/outbound-form-adapter.ts index 310532fb..254880e0 100644 --- a/frontend/src/lib/xray/outbound-form-adapter.ts +++ b/frontend/src/lib/xray/outbound-form-adapter.ts @@ -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 = 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, diff --git a/frontend/src/pages/xray/outbounds/protocols/freedom.tsx b/frontend/src/pages/xray/outbounds/protocols/freedom.tsx index 818cbffe..30816202 100644 --- a/frontend/src/pages/xray/outbounds/protocols/freedom.tsx +++ b/frontend/src/pages/xray/outbounds/protocols/freedom.tsx @@ -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 + + +