mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 10:14:15 +00:00
fix(outbounds): lock hysteria to its QUIC transport + TLS, add version/masquerade
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
The hysteria protocol now offers only the Hysteria transport (other transports removed) and security is always TLS. This prevents the broken hysteria-over-tcp / security:none outbounds that made xray-core fail to start with 'Failed to build Hysteria config. > version != 2'. Show the fixed version field directly under Transmission, and expose the full masquerade sub-form on the outbound too. The masquerade UI was extracted into a shared HysteriaMasqueradeForm component used by both the inbound and outbound forms. Closes #4665
This commit is contained in:
parent
987a6dd1e5
commit
eee26e4788
3 changed files with 166 additions and 144 deletions
120
frontend/src/components/HysteriaMasqueradeForm.tsx
Normal file
120
frontend/src/components/HysteriaMasqueradeForm.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Form, Input, InputNumber, Select, Switch } from 'antd';
|
||||||
|
import type { FormInstance } from 'antd';
|
||||||
|
|
||||||
|
import HeaderMapEditor from '@/components/HeaderMapEditor';
|
||||||
|
|
||||||
|
const MASQ_PATH = ['streamSettings', 'hysteriaSettings', 'masquerade'];
|
||||||
|
|
||||||
|
interface HysteriaMasqueradeFormProps {
|
||||||
|
form: FormInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HysteriaMasqueradeForm({ form }: HysteriaMasqueradeFormProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item label={t('pages.inbounds.form.masquerade')}>
|
||||||
|
<Form.Item shouldUpdate noStyle>
|
||||||
|
{() => {
|
||||||
|
const m = form.getFieldValue(MASQ_PATH);
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={!!m}
|
||||||
|
onChange={(checked) =>
|
||||||
|
form.setFieldValue(
|
||||||
|
MASQ_PATH,
|
||||||
|
checked
|
||||||
|
? {
|
||||||
|
type: '', dir: '', url: '',
|
||||||
|
rewriteHost: false, insecure: false,
|
||||||
|
content: '', headers: {}, statusCode: 0,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate noStyle>
|
||||||
|
{() => {
|
||||||
|
const m = form.getFieldValue(MASQ_PATH) as { type?: string } | undefined;
|
||||||
|
if (!m) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.type')}
|
||||||
|
name={[...MASQ_PATH, 'type']}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'default (404 page)' },
|
||||||
|
{ value: 'proxy', label: 'proxy (reverse proxy)' },
|
||||||
|
{ value: 'file', label: 'file (serve directory)' },
|
||||||
|
{ value: 'string', label: 'string (fixed body)' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
{m.type === 'proxy' && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.upstreamUrl')}
|
||||||
|
name={[...MASQ_PATH, 'url']}
|
||||||
|
>
|
||||||
|
<Input placeholder="https://www.example.com" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.rewriteHost')}
|
||||||
|
name={[...MASQ_PATH, 'rewriteHost']}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.skipTlsVerify')}
|
||||||
|
name={[...MASQ_PATH, 'insecure']}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{m.type === 'file' && (
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.directory')}
|
||||||
|
name={[...MASQ_PATH, 'dir']}
|
||||||
|
>
|
||||||
|
<Input placeholder="/var/www/html" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{m.type === 'string' && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.statusCode')}
|
||||||
|
name={[...MASQ_PATH, 'statusCode']}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={599} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.body')}
|
||||||
|
name={[...MASQ_PATH, 'content']}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize={{ minRows: 3 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.headers')}
|
||||||
|
name={[...MASQ_PATH, 'headers']}
|
||||||
|
>
|
||||||
|
<HeaderMapEditor mode="v1" />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -83,6 +83,7 @@ import { XHttpStreamSettingsSchema } from '@/schemas/protocols/stream/xhttp';
|
||||||
import DateTimePicker from '@/components/DateTimePicker';
|
import DateTimePicker from '@/components/DateTimePicker';
|
||||||
import FinalMaskForm from '@/components/FinalMaskForm';
|
import FinalMaskForm from '@/components/FinalMaskForm';
|
||||||
import HeaderMapEditor from '@/components/HeaderMapEditor';
|
import HeaderMapEditor from '@/components/HeaderMapEditor';
|
||||||
|
import HysteriaMasqueradeForm from '@/components/HysteriaMasqueradeForm';
|
||||||
import InputAddon from '@/components/InputAddon';
|
import InputAddon from '@/components/InputAddon';
|
||||||
import JsonEditor from '@/components/JsonEditor';
|
import JsonEditor from '@/components/JsonEditor';
|
||||||
import './InboundFormModal.css';
|
import './InboundFormModal.css';
|
||||||
|
|
@ -1606,111 +1607,7 @@ export default function InboundFormModal({
|
||||||
<InputNumber min={1} style={{ width: '100%' }} />
|
<InputNumber min={1} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('pages.inbounds.form.masquerade')}>
|
<HysteriaMasqueradeForm form={form} />
|
||||||
<Form.Item shouldUpdate noStyle>
|
|
||||||
{() => {
|
|
||||||
const m = form.getFieldValue([
|
|
||||||
'streamSettings', 'hysteriaSettings', 'masquerade',
|
|
||||||
]);
|
|
||||||
return (
|
|
||||||
<Switch
|
|
||||||
checked={!!m}
|
|
||||||
onChange={(checked) =>
|
|
||||||
form.setFieldValue(
|
|
||||||
['streamSettings', 'hysteriaSettings', 'masquerade'],
|
|
||||||
checked
|
|
||||||
? {
|
|
||||||
type: '', dir: '', url: '',
|
|
||||||
rewriteHost: false, insecure: false,
|
|
||||||
content: '', headers: {}, statusCode: 0,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item shouldUpdate noStyle>
|
|
||||||
{() => {
|
|
||||||
const m = form.getFieldValue([
|
|
||||||
'streamSettings', 'hysteriaSettings', 'masquerade',
|
|
||||||
]) as { type?: string } | undefined;
|
|
||||||
if (!m) return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.type')}
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'type']}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
options={[
|
|
||||||
{ value: '', label: 'default (404 page)' },
|
|
||||||
{ value: 'proxy', label: 'proxy (reverse proxy)' },
|
|
||||||
{ value: 'file', label: 'file (serve directory)' },
|
|
||||||
{ value: 'string', label: 'string (fixed body)' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
{m.type === 'proxy' && (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.upstreamUrl')}
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'url']}
|
|
||||||
>
|
|
||||||
<Input placeholder="https://www.example.com" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.rewriteHost')}
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'rewriteHost']}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.skipTlsVerify')}
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'insecure']}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{m.type === 'file' && (
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.directory')}
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'dir']}
|
|
||||||
>
|
|
||||||
<Input placeholder="/var/www/html" />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
{m.type === 'string' && (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.statusCode')}
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'statusCode']}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} max={599} style={{ width: '100%' }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.body')}
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'content']}
|
|
||||||
>
|
|
||||||
<Input.TextArea autoSize={{ minRows: 3 }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.headers')}
|
|
||||||
name={['streamSettings', 'hysteriaSettings', 'masquerade', 'headers']}
|
|
||||||
>
|
|
||||||
<HeaderMapEditor mode="v1" />
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { DeleteOutlined, MinusOutlined, PlusOutlined, ReloadOutlined } from '@an
|
||||||
|
|
||||||
import FinalMaskForm from '@/components/FinalMaskForm';
|
import FinalMaskForm from '@/components/FinalMaskForm';
|
||||||
import HeaderMapEditor from '@/components/HeaderMapEditor';
|
import HeaderMapEditor from '@/components/HeaderMapEditor';
|
||||||
|
import HysteriaMasqueradeForm from '@/components/HysteriaMasqueradeForm';
|
||||||
import InputAddon from '@/components/InputAddon';
|
import InputAddon from '@/components/InputAddon';
|
||||||
import JsonEditor from '@/components/JsonEditor';
|
import JsonEditor from '@/components/JsonEditor';
|
||||||
import { Wireguard } from '@/utils';
|
import { Wireguard } from '@/utils';
|
||||||
|
|
@ -107,9 +108,8 @@ const NETWORK_OPTIONS: { value: string; label: string }[] = [
|
||||||
{ value: 'xhttp', label: 'XHTTP' },
|
{ value: 'xhttp', label: 'XHTTP' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Hysteria appends an extra `hysteria` network branch to the selector
|
// The hysteria protocol is locked to its own QUIC transport: the selector
|
||||||
// — only when the parent protocol is hysteria. Wire-side this matches
|
// shows only this option when the parent protocol is hysteria.
|
||||||
// the legacy modal's `isHysteria ? [...NETWORKS, 'hysteria'] : NETWORKS`.
|
|
||||||
const HYSTERIA_NETWORK_OPTION = { value: 'hysteria', label: 'Hysteria' };
|
const HYSTERIA_NETWORK_OPTION = { value: 'hysteria', label: 'Hysteria' };
|
||||||
|
|
||||||
// Per-network bootstrap. Mirrors the legacy class constructors so the
|
// Per-network bootstrap. Mirrors the legacy class constructors so the
|
||||||
|
|
@ -163,6 +163,19 @@ function newStreamSlice(network: string): Record<string, unknown> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hysteria2 always rides its own QUIC transport with TLS — the panel never
|
||||||
|
// offers another transport or 'none' security for it.
|
||||||
|
function hysteriaStreamSlice(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
...newStreamSlice('hysteria'),
|
||||||
|
security: 'tls',
|
||||||
|
tlsSettings: {
|
||||||
|
serverName: '', alpn: ['h3'], fingerprint: '',
|
||||||
|
echConfigList: '', verifyPeerCertByName: '', pinnedPeerCertSha256: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Protocols whose form schema carries a flat connect target — these all
|
// Protocols whose form schema carries a flat connect target — these all
|
||||||
// get the shared "server" sub-block (address + port) at the top of the
|
// get the shared "server" sub-block (address + port) at the top of the
|
||||||
// protocol section. Wireguard has an address but no port. DNS/freedom/
|
// protocol section. Wireguard has an address but no port. DNS/freedom/
|
||||||
|
|
@ -233,23 +246,13 @@ export default function OutboundFormModal({
|
||||||
|
|
||||||
const tag = Form.useWatch('tag', form) ?? '';
|
const tag = Form.useWatch('tag', form) ?? '';
|
||||||
const protocol = (Form.useWatch('protocol', form) ?? 'vless') as string;
|
const protocol = (Form.useWatch('protocol', form) ?? 'vless') as string;
|
||||||
// preserve: true — without it useWatch only reflects values whose
|
|
||||||
// Form.Item is currently mounted. The streamSettings selectors live
|
|
||||||
// INSIDE `{streamAllowed && network && (...)}`, so the moment that
|
|
||||||
// conditional gates them out, useWatch returns undefined, the gate
|
|
||||||
// keeps returning false, and the stream block never renders even
|
|
||||||
// though streamSettings is in the form store.
|
|
||||||
const network = (Form.useWatch(['streamSettings', 'network'], { form, preserve: true }) ?? '') as string;
|
const network = (Form.useWatch(['streamSettings', 'network'], { form, preserve: true }) ?? '') as string;
|
||||||
const security = (Form.useWatch(['streamSettings', 'security'], { form, preserve: true }) ?? 'none') as string;
|
const security = (Form.useWatch(['streamSettings', 'security'], { form, preserve: true }) ?? 'none') as string;
|
||||||
|
|
||||||
const streamAllowed = canEnableStream({ protocol });
|
const streamAllowed = canEnableStream({ protocol });
|
||||||
const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
|
const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
|
||||||
const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
|
const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
|
||||||
const tlsFlowAllowed = canEnableTlsFlow({ protocol, streamSettings: { network, security } });
|
const tlsFlowAllowed = canEnableTlsFlow({ protocol, streamSettings: { network, security } });
|
||||||
|
|
||||||
// Seed streamSettings when the user picks a protocol that supports
|
|
||||||
// streams but the form does not yet have a stream slice (new outbound,
|
|
||||||
// or wire payload arrived without streamSettings).
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!streamAllowed) return;
|
if (!streamAllowed) return;
|
||||||
if (network) return;
|
if (network) return;
|
||||||
|
|
@ -257,9 +260,16 @@ export default function OutboundFormModal({
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [streamAllowed, network]);
|
}, [streamAllowed, network]);
|
||||||
|
|
||||||
// Wireguard pubKey is a UI-only field derived from secretKey on every
|
useEffect(() => {
|
||||||
// edit. The legacy modal did the same on every keystroke. We re-derive
|
if (protocol !== 'hysteria') return;
|
||||||
// here so paste-in secret keys immediately surface the matching pub.
|
if (network === 'hysteria' && security === 'tls') return;
|
||||||
|
const existing = (form.getFieldValue('streamSettings') ?? {}) as Record<string, unknown>;
|
||||||
|
const slice = hysteriaStreamSlice();
|
||||||
|
if (existing.hysteriaSettings) slice.hysteriaSettings = existing.hysteriaSettings;
|
||||||
|
if (existing.tlsSettings) slice.tlsSettings = existing.tlsSettings;
|
||||||
|
form.setFieldValue('streamSettings', slice);
|
||||||
|
}, [protocol, network, security]);
|
||||||
|
|
||||||
const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form) as string | undefined;
|
const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form) as string | undefined;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (protocol !== 'wireguard') return;
|
if (protocol !== 'wireguard') return;
|
||||||
|
|
@ -277,21 +287,18 @@ export default function OutboundFormModal({
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [protocol, wgSecretKey]);
|
}, [protocol, wgSecretKey]);
|
||||||
|
|
||||||
// Switching protocol resets the settings sub-object to fresh defaults
|
|
||||||
// so leftover fields from the previous protocol do not bleed through.
|
|
||||||
// The adapter's rawOutboundToFormValues seeds whatever the new protocol
|
|
||||||
// expects (vless flat shape, vmess flat shape, wireguard with secretKey
|
|
||||||
// placeholder, etc.).
|
|
||||||
function onValuesChange(changed: Partial<OutboundFormValues>) {
|
function onValuesChange(changed: Partial<OutboundFormValues>) {
|
||||||
if ('protocol' in changed && changed.protocol) {
|
if ('protocol' in changed && changed.protocol) {
|
||||||
const next = rawOutboundToFormValues({ protocol: changed.protocol });
|
const next = rawOutboundToFormValues({ protocol: changed.protocol });
|
||||||
form.setFieldValue('settings', next.settings);
|
form.setFieldValue('settings', next.settings);
|
||||||
|
if (changed.protocol === 'hysteria') {
|
||||||
|
form.setFieldValue('streamSettings', hysteriaStreamSlice());
|
||||||
|
} else if ((form.getFieldValue(['streamSettings', 'network']) ?? '') === 'hysteria') {
|
||||||
|
form.setFieldValue('streamSettings', { ...newStreamSlice('tcp'), security: 'none' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Security change cascade: swap the security sub-key so the DU branch
|
|
||||||
// matches. Seed default field values when entering tls/reality so the
|
|
||||||
// sub-forms render without `undefined` field references.
|
|
||||||
function onSecurityChange(next: string) {
|
function onSecurityChange(next: string) {
|
||||||
const stream = form.getFieldValue('streamSettings') ?? {};
|
const stream = form.getFieldValue('streamSettings') ?? {};
|
||||||
const cleaned = { ...stream } as Record<string, unknown>;
|
const cleaned = { ...stream } as Record<string, unknown>;
|
||||||
|
|
@ -324,6 +331,10 @@ export default function OutboundFormModal({
|
||||||
// wsSettings, etc.) so the DU branch matches. Preserve security if
|
// wsSettings, etc.) so the DU branch matches. Preserve security if
|
||||||
// the new network supports it, otherwise force back to 'none'.
|
// the new network supports it, otherwise force back to 'none'.
|
||||||
function onNetworkChange(next: string) {
|
function onNetworkChange(next: string) {
|
||||||
|
if (next === 'hysteria') {
|
||||||
|
form.setFieldValue('streamSettings', hysteriaStreamSlice());
|
||||||
|
return;
|
||||||
|
}
|
||||||
const currentSecurity = form.getFieldValue(['streamSettings', 'security']) ?? 'none';
|
const currentSecurity = form.getFieldValue(['streamSettings', 'security']) ?? 'none';
|
||||||
const stillAllowed = canEnableTls({ protocol, streamSettings: { network: next, security: currentSecurity } });
|
const stillAllowed = canEnableTls({ protocol, streamSettings: { network: next, security: currentSecurity } });
|
||||||
const stillReality = canEnableReality({ protocol, streamSettings: { network: next, security: currentSecurity } });
|
const stillReality = canEnableReality({ protocol, streamSettings: { network: next, security: currentSecurity } });
|
||||||
|
|
@ -372,13 +383,6 @@ export default function OutboundFormModal({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap every tab switch with a blur of the active element. AntD marks
|
|
||||||
// the outgoing panel `aria-hidden="true"` synchronously when the
|
|
||||||
// controlled activeKey flips; if a focused input is still inside that
|
|
||||||
// panel (e.g. Input.Search on the JSON tab after user hits Enter to
|
|
||||||
// import), Chrome logs a WAI-ARIA warning. Doing the blur right
|
|
||||||
// before setActiveKey ensures the panel is unfocused by the time
|
|
||||||
// AntD applies the attribute.
|
|
||||||
function switchTab(key: string) {
|
function switchTab(key: string) {
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
(document.activeElement as HTMLElement | null)?.blur?.();
|
(document.activeElement as HTMLElement | null)?.blur?.();
|
||||||
|
|
@ -597,12 +601,6 @@ export default function OutboundFormModal({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{protocol === 'hysteria' && (
|
|
||||||
<Form.Item label={t('pages.inbounds.form.version')} name={['settings', 'version']}>
|
|
||||||
<InputNumber min={2} max={2} disabled />
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{protocol === 'loopback' && (
|
{protocol === 'loopback' && (
|
||||||
<Form.Item label={t('pages.xray.outboundForm.inboundTag')} name={['settings', 'inboundTag']}>
|
<Form.Item label={t('pages.xray.outboundForm.inboundTag')} name={['settings', 'inboundTag']}>
|
||||||
<Input placeholder={t('pages.xray.outboundForm.inboundTagPlaceholder')} />
|
<Input placeholder={t('pages.xray.outboundForm.inboundTagPlaceholder')} />
|
||||||
|
|
@ -1155,7 +1153,7 @@ export default function OutboundFormModal({
|
||||||
onChange={onNetworkChange}
|
onChange={onNetworkChange}
|
||||||
options={
|
options={
|
||||||
protocol === 'hysteria'
|
protocol === 'hysteria'
|
||||||
? [...NETWORK_OPTIONS, HYSTERIA_NETWORK_OPTION]
|
? [HYSTERIA_NETWORK_OPTION]
|
||||||
: NETWORK_OPTIONS
|
: NETWORK_OPTIONS
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1721,6 +1719,12 @@ export default function OutboundFormModal({
|
||||||
|
|
||||||
{network === 'hysteria' && (
|
{network === 'hysteria' && (
|
||||||
<>
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.version')}
|
||||||
|
name={['streamSettings', 'hysteriaSettings', 'version']}
|
||||||
|
>
|
||||||
|
<InputNumber min={2} max={2} disabled style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('pages.xray.outboundForm.authPassword')}
|
label={t('pages.xray.outboundForm.authPassword')}
|
||||||
name={['streamSettings', 'hysteriaSettings', 'auth']}
|
name={['streamSettings', 'hysteriaSettings', 'auth']}
|
||||||
|
|
@ -1733,6 +1737,7 @@ export default function OutboundFormModal({
|
||||||
>
|
>
|
||||||
<InputNumber min={1} style={{ width: '100%' }} />
|
<InputNumber min={1} style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<HysteriaMasqueradeForm form={form} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -1783,7 +1788,7 @@ export default function OutboundFormModal({
|
||||||
buttonStyle="solid"
|
buttonStyle="solid"
|
||||||
onChange={(e) => onSecurityChange(e.target.value as string)}
|
onChange={(e) => onSecurityChange(e.target.value as string)}
|
||||||
>
|
>
|
||||||
<Radio.Button value="none">{t('none')}</Radio.Button>
|
{network !== 'hysteria' && <Radio.Button value="none">{t('none')}</Radio.Button>}
|
||||||
{tlsAllowed && <Radio.Button value="tls">TLS</Radio.Button>}
|
{tlsAllowed && <Radio.Button value="tls">TLS</Radio.Button>}
|
||||||
{realityAllowed && <Radio.Button value="reality">Reality</Radio.Button>}
|
{realityAllowed && <Radio.Button value="reality">Reality</Radio.Button>}
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue