feat(frontend): add targetStrategy field to the outbound editor

Xray-core added a top-level targetStrategy to OutboundObject that
controls how the destination domain is resolved before dialing
(AsIs/UseIP*/ForceIP*, any protocol). The panel neither offered a
control for it nor preserved the key across the modal's JSON round
trip, so hand-written values were silently dropped on save.

The form now carries targetStrategy next to sendThrough as a select
of the 11 canonical values; the adapter normalizes wire values to
canonical case (the core matches case-insensitively) and omits the
key when unset. Freedom settings additionally read the new
settings-level targetStrategy with domainStrategy as fallback,
mirroring the core, while still emitting the legacy domainStrategy
key so configs keep working on older cores.
This commit is contained in:
MHSanaei
2026-07-02 23:03:43 +02:00
parent 9f760cf0fa
commit 258d8b7344
18 changed files with 113 additions and 10 deletions
@@ -420,6 +420,54 @@ describe('outbound-form-adapter: round-trip', () => {
});
});
describe('outbound-form-adapter: targetStrategy', () => {
it('round-trips a top-level targetStrategy', () => {
const back = formValuesToWirePayload(rawOutboundToFormValues({
protocol: 'vless',
settings: { address: 's', port: 443, id: '11111111-2222-4333-8444-555555555555', flow: '', encryption: 'none' },
targetStrategy: 'ForceIPv6v4',
}));
expect(back.targetStrategy).toBe('ForceIPv6v4');
});
it('normalizes wire case to the canonical spelling (core matches case-insensitively)', () => {
const form = rawOutboundToFormValues({
protocol: 'freedom',
settings: {},
targetStrategy: 'useipv4v6',
});
expect(form.targetStrategy).toBe('UseIPv4v6');
});
it('omits targetStrategy when unset and drops unknown values', () => {
const unset = formValuesToWirePayload(rawOutboundToFormValues({
protocol: 'freedom',
settings: {},
}));
expect(unset).not.toHaveProperty('targetStrategy');
const invalid = formValuesToWirePayload(rawOutboundToFormValues({
protocol: 'freedom',
settings: {},
targetStrategy: 'UseIPv5',
}));
expect(invalid).not.toHaveProperty('targetStrategy');
});
it('freedom prefers settings.targetStrategy over domainStrategy and emits the legacy key', () => {
const form = rawOutboundToFormValues({
protocol: 'freedom',
settings: { targetStrategy: 'UseIPv6', domainStrategy: 'UseIPv4' },
});
if (form.protocol === 'freedom') {
expect(form.settings.domainStrategy).toBe('UseIPv6');
}
const back = formValuesToWirePayload(form);
expect(back.settings).toMatchObject({ domainStrategy: 'UseIPv6' });
expect(back.settings).not.toHaveProperty('targetStrategy');
});
});
describe('outbound-form-adapter: xhttp xmux toggle', () => {
const xmuxWire = {
protocol: 'vless',