mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 23:27:14 +00:00
fix(xray): place the freedom domain strategy where the core reads it (#6515)
* fix(xray): place the freedom domain strategy where the core reads it freedom resolves through the socket layer, so xray-core reads sockopt.domainStrategy and treats both other placements as legacy: it warns on every config load for the outbound-root targetStrategy it migrates itself, and again for the settings-level domainStrategy it deprecates. The panel wrote exactly those two keys from its Freedom Protocol Strategy select, the outbound form card, and the IPv4 routing helper, so any install that had configured a strategy logged a deprecation warning on every start. The strategy now travels in streamSettings.sockopt everywhere the panel emits it: the Basics select, the outbound form (including the JSON tab, which shares the same adapter), the shipped default template, and the IPv4 outbound the routing helper injects. Reading mirrors the loader's own order — root targetStrategy, then the settings keys, then sockopt — so the card keeps showing the value the core would actually run with, and saving drops the legacy keys instead of leaving them behind. A seeder moves the keys for configs already stored in the database, following OutboundRemovedKeysFix. The shared outbound-root Target Strategy field is hidden for freedom, since the core migrates that key into the very sockopt value the card writes and two knobs for one value would race. Tests: placement round-trips and the migration table run through the real vendored core (a captured log handler proves the warning is gone after the rewrite and present before it), and the modal asserts freedom offers a single strategy field. * test(database): seed the template row the seeder test needs A fresh InitDB creates no xrayTemplateConfig row — the panel's setting defaults live in the service layer — so the test has to insert the legacy template itself and then assert the seeder's history gate stops a second pass from rewriting it. * fix(xray): keep one strategy control per outbound, seed the row in tests Review findings: the Transport tab's Sockopts block renders for freedom too, so its Domain Strategy select and the freedom card wrote one sockopt value between them and the card won on save — the field is hidden for freedom now, leaving the card as the single control. The seeder is also pre-marked on a fresh install so it does not run on the second start, and the seeder test seeds the template row itself (a fresh InitDB has none) and asserts the rewrite structurally instead of grepping for a key name that sockopt also uses.
This commit is contained in:
@@ -25,6 +25,7 @@ import {
|
||||
MASK_ADDRESS,
|
||||
ROUTING_DOMAIN_STRATEGIES,
|
||||
} from './constants';
|
||||
import { directFreedomStrategy, setDirectFreedomStrategy } from './helpers';
|
||||
|
||||
interface BasicsTabProps {
|
||||
templateSettings: XraySettingsValue | null;
|
||||
@@ -109,11 +110,7 @@ export default function BasicsTab({
|
||||
});
|
||||
}
|
||||
|
||||
const freedomStrategy =
|
||||
(
|
||||
templateSettings?.outbounds?.find((o) => o?.protocol === 'freedom' && o?.tag === 'direct')
|
||||
?.settings as { domainStrategy?: string } | undefined
|
||||
)?.domainStrategy ?? 'AsIs';
|
||||
const freedomStrategy = directFreedomStrategy(templateSettings);
|
||||
|
||||
const directFreedomOutbound = templateSettings?.outbounds?.find(
|
||||
(o) => o?.protocol === 'freedom' && o?.tag === 'direct',
|
||||
@@ -186,25 +183,7 @@ export default function BasicsTab({
|
||||
value={freedomStrategy}
|
||||
style={{ width: '100%' }}
|
||||
options={OutboundDomainStrategies.map((s) => ({ value: s, label: s }))}
|
||||
onChange={(next) =>
|
||||
mutate((tt) => {
|
||||
if (!tt.outbounds) tt.outbounds = [];
|
||||
const idx = tt.outbounds.findIndex(
|
||||
(o) => o?.protocol === 'freedom' && o?.tag === 'direct',
|
||||
);
|
||||
if (idx < 0) {
|
||||
tt.outbounds.push({
|
||||
protocol: 'freedom',
|
||||
tag: 'direct',
|
||||
settings: { domainStrategy: next },
|
||||
});
|
||||
} else {
|
||||
const ob = tt.outbounds[idx];
|
||||
ob.settings = (ob.settings || {}) as Record<string, unknown>;
|
||||
(ob.settings as Record<string, unknown>).domainStrategy = next;
|
||||
}
|
||||
})
|
||||
}
|
||||
onChange={(next) => mutate((tt) => setDirectFreedomStrategy(tt, next))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -61,8 +61,10 @@ export const SERVICES_OPTIONS = [
|
||||
|
||||
export const directSettings = { tag: 'direct', protocol: 'freedom' };
|
||||
export const blockedSettings = { tag: 'blocked', protocol: 'blackhole', settings: {} };
|
||||
// The strategy rides on sockopt: freedom resolves through the socket layer, and
|
||||
// the settings-level alias makes the core warn on every config load.
|
||||
export const ipv4Settings = {
|
||||
tag: 'IPv4',
|
||||
protocol: 'freedom',
|
||||
settings: { domainStrategy: 'UseIPv4' },
|
||||
streamSettings: { sockopt: { domainStrategy: 'UseIPv4' } },
|
||||
};
|
||||
|
||||
@@ -1,6 +1,48 @@
|
||||
import type { XraySettingsValue } from '@/hooks/useXraySetting';
|
||||
import { freedomDomainStrategyFromWire } from '@/lib/xray/outbound-form-adapter';
|
||||
import { blockedSettings, directSettings } from './constants';
|
||||
|
||||
// Freedom resolves through the socket layer, so the outbound root and its own
|
||||
// settings only hold legacy aliases the core warns about (infra/conf/xray.go).
|
||||
const LEGACY_FREEDOM_STRATEGY_KEYS = ['domainStrategy', 'targetStrategy'] as const;
|
||||
|
||||
type Outbound = Record<string, unknown>;
|
||||
|
||||
function directFreedom(t: XraySettingsValue | null): Outbound | undefined {
|
||||
return t?.outbounds?.find((o) => o?.protocol === 'freedom' && o?.tag === 'direct') as
|
||||
| Outbound
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export function directFreedomStrategy(t: XraySettingsValue | null): string {
|
||||
const outbound = directFreedom(t);
|
||||
if (!outbound) return 'AsIs';
|
||||
return freedomDomainStrategyFromWire(outbound) || 'AsIs';
|
||||
}
|
||||
|
||||
export function setDirectFreedomStrategy(t: XraySettingsValue, next: string): void {
|
||||
if (!Array.isArray(t.outbounds)) t.outbounds = [];
|
||||
let idx = t.outbounds.findIndex((o) => o?.protocol === 'freedom' && o?.tag === 'direct');
|
||||
if (idx < 0) {
|
||||
t.outbounds.push({ protocol: 'freedom', tag: 'direct', settings: {} } as never);
|
||||
idx = t.outbounds.length - 1;
|
||||
}
|
||||
const ob = t.outbounds[idx] as Outbound;
|
||||
// Drop the legacy placements, or the loader keeps warning and the core keeps
|
||||
// preferring the root key it resets over the sockopt value set here.
|
||||
const settings = (ob.settings ?? {}) as Outbound;
|
||||
for (const key of LEGACY_FREEDOM_STRATEGY_KEYS) delete settings[key];
|
||||
ob.settings = settings;
|
||||
const stream = (ob.streamSettings ?? {}) as Outbound;
|
||||
const sockopt = (stream.sockopt ?? {}) as Outbound;
|
||||
if (next === 'AsIs') delete sockopt.domainStrategy;
|
||||
else sockopt.domainStrategy = next;
|
||||
if (Object.keys(sockopt).length === 0) delete stream.sockopt;
|
||||
else stream.sockopt = sockopt;
|
||||
if (Object.keys(stream).length === 0) delete ob.streamSettings;
|
||||
else ob.streamSettings = stream;
|
||||
}
|
||||
|
||||
export function ruleGetter(
|
||||
t: XraySettingsValue | null,
|
||||
outboundTag: string,
|
||||
|
||||
@@ -417,13 +417,17 @@ export default function OutboundFormModal({
|
||||
<Input placeholder={t('pages.xray.outboundForm.localIpPlaceholder')} />
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label={t('pages.xray.outbound.targetStrategy')}
|
||||
name="targetStrategy"
|
||||
tooltip={t('pages.xray.outboundForm.targetStrategyHint')}
|
||||
>
|
||||
<Select allowClear placeholder="AsIs" options={TARGET_STRATEGY_OPTIONS} />
|
||||
</FormField>
|
||||
{/* Freedom's own card owns the strategy — the core migrates this
|
||||
root key into the same sockopt value, so two knobs would race. */}
|
||||
{protocol !== 'freedom' && (
|
||||
<FormField
|
||||
label={t('pages.xray.outbound.targetStrategy')}
|
||||
name="targetStrategy"
|
||||
tooltip={t('pages.xray.outboundForm.targetStrategyHint')}
|
||||
>
|
||||
<Select allowClear placeholder="AsIs" options={TARGET_STRATEGY_OPTIONS} />
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
{SERVER_PROTOCOLS.has(protocol) && <ServerTarget />}
|
||||
{protocol === 'vmess' && <VmessFields />}
|
||||
@@ -541,7 +545,10 @@ export default function OutboundFormModal({
|
||||
{((streamAllowed && network) ||
|
||||
!streamAllowed ||
|
||||
protocol === 'wireguard') && (
|
||||
<SockoptForm outboundTags={dialerProxyTags ?? existingTags} />
|
||||
<SockoptForm
|
||||
outboundTags={dialerProxyTags ?? existingTags}
|
||||
showDomainStrategy={protocol !== 'freedom'}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
|
||||
@@ -12,7 +12,17 @@ import {
|
||||
|
||||
import { ADDRESS_PORT_STRATEGY_OPTIONS } from '../outbound-form-constants';
|
||||
|
||||
export default function SockoptForm({ outboundTags = [] }: { outboundTags?: string[] }) {
|
||||
interface SockoptFormProps {
|
||||
outboundTags?: string[];
|
||||
showDomainStrategy?: boolean;
|
||||
}
|
||||
|
||||
// Freedom's own card writes the strategy into this same sockopt key, so it hides
|
||||
// this field rather than letting two controls fight over one value.
|
||||
export default function SockoptForm({
|
||||
outboundTags = [],
|
||||
showDomainStrategy = true,
|
||||
}: SockoptFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const { control, setValue } = useFormContext();
|
||||
const sockopt = useWatch({ control, name: 'streamSettings.sockopt' });
|
||||
@@ -51,17 +61,19 @@ export default function SockoptForm({ outboundTags = [] }: { outboundTags?: stri
|
||||
options={dialerProxyOptions}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label={t('pages.xray.wireguard.domainStrategy')}
|
||||
name={['streamSettings', 'sockopt', 'domainStrategy']}
|
||||
>
|
||||
<Select
|
||||
options={Object.values(DOMAIN_STRATEGY_OPTION).map((v) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
}))}
|
||||
/>
|
||||
</FormField>
|
||||
{showDomainStrategy && (
|
||||
<FormField
|
||||
label={t('pages.xray.wireguard.domainStrategy')}
|
||||
name={['streamSettings', 'sockopt', 'domainStrategy']}
|
||||
>
|
||||
<Select
|
||||
options={Object.values(DOMAIN_STRATEGY_OPTION).map((v) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
}))}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField
|
||||
label={t('pages.inbounds.form.addressPortStrategy')}
|
||||
name={['streamSettings', 'sockopt', 'addressPortStrategy']}
|
||||
|
||||
Reference in New Issue
Block a user