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
This commit is contained in:
MHSanaei
2026-05-31 19:50:50 +02:00
parent c20ee00fa3
commit f02018cfb7
5 changed files with 39 additions and 4 deletions
@@ -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',