feat(inbound-form): salamander auto-seed for Hysteria + modernize random buttons

Picking Hysteria from the protocol select used to leave finalmask.udp
empty, so the listener went out without obfs unless the admin added
the salamander wrapper by hand. Hook into onValuesChange so switching
to Hysteria seeds finalmask.udp with
{type: 'salamander', settings: {password: <random>}} alongside the
hysteriaSettings / tlsSettings reset already happening there.

Also modernise the SyncOutlined-in-label "random" affordances on
Shadowsocks password, WireGuard secret key (server + per-peer), and
Reality target / SNI / shortIds into proper icon buttons inside a
Space.Compact next to the field. The old pattern dropped a tiny
clickable icon into the form-item label, which was easy to miss and
inconsistent with the other action buttons in the modal.
This commit is contained in:
MHSanaei 2026-05-27 13:43:21 +02:00
parent 222e000b3b
commit 9d2a4f217e
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
2 changed files with 86 additions and 75 deletions

View file

@ -424,8 +424,19 @@ function UdpMaskItem({
const type = getFieldValue([...absolutePath, 'type']) as string | undefined; const type = getFieldValue([...absolutePath, 'type']) as string | undefined;
if (type === 'mkcp-aes128gcm' || type === 'salamander') { if (type === 'mkcp-aes128gcm' || type === 'salamander') {
return ( return (
<Form.Item label="Password" name={[fieldName, 'settings', 'password']}> <Form.Item label="Password">
<Input placeholder="Obfuscation password" /> <Space.Compact block>
<Form.Item name={[fieldName, 'settings', 'password']} noStyle>
<Input placeholder="Obfuscation password" style={{ width: 'calc(100% - 32px)' }} />
</Form.Item>
<Button
icon={<ReloadOutlined />}
onClick={() => form.setFieldValue(
[...absolutePath, 'settings', 'password'],
RandomUtil.randomLowerAndNum(16),
)}
/>
</Space.Compact>
</Form.Item> </Form.Item>
); );
} }

View file

@ -26,7 +26,7 @@ import {
DeleteOutlined, DeleteOutlined,
MinusOutlined, MinusOutlined,
PlusOutlined, PlusOutlined,
SyncOutlined, ReloadOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils'; import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
@ -762,6 +762,18 @@ export default function InboundFormModal({
security: 'tls', security: 'tls',
hysteriaSettings: HysteriaStreamSettingsSchema.parse({}), hysteriaSettings: HysteriaStreamSettingsSchema.parse({}),
tlsSettings: tls, tlsSettings: tls,
// Hysteria2 needs an obfs wrapper on the FinalMask side; seed
// it with salamander + a random password so the listener boots
// with a usable default. Re-selecting Hysteria from another
// protocol re-runs this and refreshes the password — that's
// intentional, the form was already being reset.
finalmask: {
tcp: [],
udp: [{
type: 'salamander',
settings: { password: RandomUtil.randomLowerAndNum(16) },
}],
},
}); });
} else { } else {
const current = form.getFieldValue('streamSettings') as { network?: string } | undefined; const current = form.getFieldValue('streamSettings') as { network?: string } | undefined;
@ -1048,16 +1060,13 @@ export default function InboundFormModal({
<> <>
{protocol === Protocols.WIREGUARD && ( {protocol === Protocols.WIREGUARD && (
<> <>
<Form.Item <Form.Item label="Secret key">
name={['settings', 'secretKey']} <Space.Compact block>
label={ <Form.Item name={['settings', 'secretKey']} noStyle>
<> <Input style={{ width: 'calc(100% - 32px)' }} />
Secret key{' '} </Form.Item>
<SyncOutlined className="random-icon" onClick={regenInboundWg} /> <Button icon={<ReloadOutlined />} onClick={regenInboundWg} />
</> </Space.Compact>
}
>
<Input />
</Form.Item> </Form.Item>
<Form.Item label="Public key"> <Form.Item label="Public key">
<Input value={wgPubKey} disabled /> <Input value={wgPubKey} disabled />
@ -1106,19 +1115,16 @@ export default function InboundFormModal({
)} )}
</Space> </Space>
</Divider> </Divider>
<Form.Item <Form.Item label="Secret key">
name={[field.name, 'privateKey']} <Space.Compact block>
label={ <Form.Item name={[field.name, 'privateKey']} noStyle>
<> <Input style={{ width: 'calc(100% - 32px)' }} />
Secret key{' '} </Form.Item>
<SyncOutlined <Button
className="random-icon" icon={<ReloadOutlined />}
onClick={() => regenWgPeerKeypair(field.name)} onClick={() => regenWgPeerKeypair(field.name)}
/> />
</> </Space.Compact>
}
>
<Input />
</Form.Item> </Form.Item>
<Form.Item name={[field.name, 'publicKey']} label="Public key"> <Form.Item name={[field.name, 'publicKey']} label="Public key">
<Input /> <Input />
@ -1362,25 +1368,22 @@ export default function InboundFormModal({
/> />
</Form.Item> </Form.Item>
{isSSWith2022 && ( {isSSWith2022 && (
<Form.Item <Form.Item label="Password">
name={['settings', 'password']} <Space.Compact block>
label={ <Form.Item name={['settings', 'password']} noStyle>
<> <Input style={{ width: 'calc(100% - 32px)' }} />
Password{' '} </Form.Item>
<SyncOutlined <Button
className="random-icon" icon={<ReloadOutlined />}
onClick={() => { onClick={() => {
const method = form.getFieldValue(['settings', 'method']); const method = form.getFieldValue(['settings', 'method']);
form.setFieldValue( form.setFieldValue(
['settings', 'password'], ['settings', 'password'],
RandomUtil.randomShadowsocksPassword(method as string), RandomUtil.randomShadowsocksPassword(method as string),
); );
}} }}
/> />
</> </Space.Compact>
}
>
<Input />
</Form.Item> </Form.Item>
)} )}
<Form.Item name={['settings', 'network']} label="Network"> <Form.Item name={['settings', 'network']} label="Network">
@ -2759,27 +2762,24 @@ export default function InboundFormModal({
options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))} options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label="Target">
name={['streamSettings', 'realitySettings', 'target']} <Space.Compact block>
label={ <Form.Item name={['streamSettings', 'realitySettings', 'target']} noStyle>
<> <Input style={{ width: 'calc(100% - 32px)' }} />
Target{' '} </Form.Item>
<SyncOutlined className="random-icon" onClick={randomizeRealityTarget} /> <Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
</> </Space.Compact>
}
>
<Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label="SNI">
name={['streamSettings', 'realitySettings', 'serverNames']} <Space.Compact block style={{ display: 'flex' }}>
label={ <Form.Item
<> name={['streamSettings', 'realitySettings', 'serverNames']}
SNI{' '} noStyle
<SyncOutlined className="random-icon" onClick={randomizeRealityTarget} /> >
</> <Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
} </Form.Item>
> <Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
<Select mode="tags" tokenSeparators={[',']} style={{ width: '100%' }} /> </Space.Compact>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={['streamSettings', 'realitySettings', 'maxTimediff']} name={['streamSettings', 'realitySettings', 'maxTimediff']}
@ -2799,16 +2799,16 @@ export default function InboundFormModal({
> >
<Input placeholder="25.9.11" /> <Input placeholder="25.9.11" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label="Short IDs">
name={['streamSettings', 'realitySettings', 'shortIds']} <Space.Compact block style={{ display: 'flex' }}>
label={ <Form.Item
<> name={['streamSettings', 'realitySettings', 'shortIds']}
Short IDs{' '} noStyle
<SyncOutlined className="random-icon" onClick={randomizeShortIds} /> >
</> <Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
} </Form.Item>
> <Button icon={<ReloadOutlined />} onClick={randomizeShortIds} />
<Select mode="tags" tokenSeparators={[',']} style={{ width: '100%' }} /> </Space.Compact>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name={['streamSettings', 'realitySettings', 'settings', 'spiderX']} name={['streamSettings', 'realitySettings', 'settings', 'spiderX']}