feat(frontend): OutboundFormModal.new.tsx socks/http/hysteria/loopback/blackhole/wireguard sections

- SOCKS / HTTP: user + pass at settings root.
- Hysteria: read-only version=2 (the actual transport knobs live on
  stream.hysteria, added with the stream tab).
- Loopback: inboundTag.
- Blackhole: response type Select with empty/none/http options.
- Wireguard: address (csv) + secretKey (with regenerate icon) + derived
  pubKey + domain strategy + MTU + workers + no-kernel-tun + reserved
  (csv) + peers Form.List with nested allowedIPs sub-list.

Wireguard regenerate icon uses Wireguard.generateKeypair() and writes
both keys to the form via setFieldValue — preserves the legacy UX of
the SyncOutlined inline-icon next to the privateKey label.
This commit is contained in:
MHSanaei 2026-05-26 12:06:52 +02:00
parent a3857cff6a
commit b6d996d1b1
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A

View file

@ -1,8 +1,22 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Form, Input, InputNumber, Modal, Select, Space, Switch, Tabs, message } from 'antd'; import {
Button,
Form,
Input,
InputNumber,
Modal,
Select,
Space,
Switch,
Tabs,
message,
} from 'antd';
import { DeleteOutlined, MinusOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons';
import InputAddon from '@/components/InputAddon';
import JsonEditor from '@/components/JsonEditor'; import JsonEditor from '@/components/JsonEditor';
import { Wireguard } from '@/utils';
import { import {
formValuesToWirePayload, formValuesToWirePayload,
rawOutboundToFormValues, rawOutboundToFormValues,
@ -19,6 +33,7 @@ import {
OutboundProtocols as Protocols, OutboundProtocols as Protocols,
TLS_FLOW_CONTROL, TLS_FLOW_CONTROL,
USERS_SECURITY, USERS_SECURITY,
WireguardDomainStrategy,
} from '@/schemas/primitives'; } from '@/schemas/primitives';
import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks'; import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
import { antdRule } from '@/utils/zodForm'; import { antdRule } from '@/utils/zodForm';
@ -315,6 +330,175 @@ export default function OutboundFormModalNew({
</Form.Item> </Form.Item>
</> </>
)} )}
{(protocol === 'socks' || protocol === 'http') && (
<>
<Form.Item label={t('username')} name={['settings', 'user']}>
<Input />
</Form.Item>
<Form.Item label={t('password')} name={['settings', 'pass']}>
<Input />
</Form.Item>
</>
)}
{protocol === 'hysteria' && (
<Form.Item label="Version" name={['settings', 'version']}>
<InputNumber min={2} max={2} disabled />
</Form.Item>
)}
{protocol === 'loopback' && (
<Form.Item label="Inbound tag" name={['settings', 'inboundTag']}>
<Input placeholder="inbound tag used in routing rules" />
</Form.Item>
)}
{protocol === 'blackhole' && (
<Form.Item label="Response type" name={['settings', 'type']}>
<Select
options={[
{ value: '', label: '(empty)' },
{ value: 'none', label: 'none' },
{ value: 'http', label: 'http' },
]}
/>
</Form.Item>
)}
{protocol === 'wireguard' && (
<>
<Form.Item label={t('pages.inbounds.address')} name={['settings', 'address']}>
<Input placeholder="comma-separated, e.g. 10.0.0.1,fd00::1" />
</Form.Item>
<Form.Item
label={
<>
{t('pages.inbounds.privatekey')}
<SyncOutlined
className="random-icon"
onClick={() => {
const pair = Wireguard.generateKeypair();
form.setFieldValue(['settings', 'secretKey'], pair.privateKey);
form.setFieldValue(['settings', 'pubKey'], pair.publicKey);
}}
/>
</>
}
name={['settings', 'secretKey']}
>
<Input />
</Form.Item>
<Form.Item label={t('pages.inbounds.publicKey')} name={['settings', 'pubKey']}>
<Input disabled />
</Form.Item>
<Form.Item label="Domain strategy" name={['settings', 'domainStrategy']}>
<Select
options={[
{ value: '', label: `(${t('none')})` },
...WireguardDomainStrategy.map((s) => ({ value: s, label: s })),
]}
/>
</Form.Item>
<Form.Item label="MTU" name={['settings', 'mtu']}>
<InputNumber min={0} />
</Form.Item>
<Form.Item label="Workers" name={['settings', 'workers']}>
<InputNumber min={0} />
</Form.Item>
<Form.Item
label="No-kernel TUN"
name={['settings', 'noKernelTun']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item label="Reserved" name={['settings', 'reserved']}>
<Input placeholder="comma-separated bytes, e.g. 1,2,3" />
</Form.Item>
<Form.List name={['settings', 'peers']}>
{(fields, { add, remove }) => (
<>
<Form.Item label="Peers">
<Button
size="small"
type="primary"
icon={<PlusOutlined />}
onClick={() =>
add({
publicKey: '',
psk: '',
allowedIPs: ['0.0.0.0/0', '::/0'],
endpoint: '',
keepAlive: 0,
})
}
/>
</Form.Item>
{fields.map((field, index) => (
<div key={field.key}>
<Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
<div className="item-heading">
<span>Peer {index + 1}</span>
{fields.length > 1 && (
<DeleteOutlined
className="danger-icon"
onClick={() => remove(field.name)}
/>
)}
</div>
</Form.Item>
<Form.Item label="Endpoint" name={[field.name, 'endpoint']}>
<Input />
</Form.Item>
<Form.Item
label={t('pages.inbounds.publicKey')}
name={[field.name, 'publicKey']}
>
<Input />
</Form.Item>
<Form.Item label="PSK" name={[field.name, 'psk']}>
<Input />
</Form.Item>
<Form.Item label="Allowed IPs">
<Form.List name={[field.name, 'allowedIPs']}>
{(ipFields, { add: addIp, remove: removeIp }) => (
<>
{ipFields.map((ipField, ipIdx) => (
<Space.Compact
key={ipField.key}
block
style={{ marginBottom: 4 }}
>
<Form.Item noStyle name={ipField.name}>
<Input />
</Form.Item>
{ipFields.length > 1 && (
<InputAddon onClick={() => removeIp(ipIdx)}>
<MinusOutlined />
</InputAddon>
)}
</Space.Compact>
))}
<Button
size="small"
icon={<PlusOutlined />}
onClick={() => addIp('')}
/>
</>
)}
</Form.List>
</Form.Item>
<Form.Item label="Keep alive" name={[field.name, 'keepAlive']}>
<InputNumber min={0} />
</Form.Item>
</div>
))}
</>
)}
</Form.List>
</>
)}
</> </>
), ),
}, },