mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 23:27:14 +00:00
feat(amneziawg): add AmneziaWG as an outbound protocol (#6320)
* feat(amneziawg): add AmneziaWG as an outbound protocol - AmneziaWG outbound protocol end-to-end: config schema, socks bridge, netstack, panel UI - Route amneziawg outbounds to HTTP probe in TCP mode (backend + frontend classifiers) with pinning test - Add 2-minute idle read deadline to pumpUDPEgress to reap idle egress sessions - Require SOCKS5 username/password auth on the egress server (reject NO-AUTH with 0xFF) with test - Bound the egress TCP tunnel dial with portForwardDialTimeout (10s), matching portfwd.go - Resolve UDP domain targets off the association's reader loop via deliverUDPDatagram; race-safe getOrDial starts the reply pump at session creation; client passed by value into resolver goroutines (pinned by TestEgressUDPDatagramDomainInterleavedClients) - Reconcile early-returns on an empty desired set and closes the egress listener; EgressBasePort (64900) is reserved against local inbound port conflicts like the internal API port, with pinning tests for both the port reservation (TestCheckPortConflict_EgressPortBlockedLocal) and the Reconcile empty-desired Close/Listen lifecycle (TestOutboundManagerReconcileEmptyDesiredClosesEgress) - Eliminate acceptLoop shutdown race by validating listener != nil and registering to tracked under s.mu before wg.Add; bound pre-auth handshake with deadline (pinned by TestEgressServerCloseDuringConcurrentAccepts) - Support AAAA and dual-stack domain resolution in tunnel DNS resolver with v6 default fallback (DefaultTunnelDNSServerV6); add DNS field to frontend protocol form; avoid unneeded cache flushes on unchanged SetStack ticks * fix(amneziawg): resolve IPv6-only DNS default fallback and validate required keys - Default to IPv6 tunnel DNS on IPv6-only outbounds with blank dns - Require non-empty secretKey and peer publicKey in ValidateAmneziaWGOutbound - Add end-to-end IPv6 tunnel domain resolution test and test empty key rejection - Trim comment blocks exceeding 2 lines across modified files - Fix Storybook test execution on environments with POSIX locale Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
VmessFields,
|
||||
WireguardFields,
|
||||
} from './protocols';
|
||||
import { AmneziawgFields } from './protocols';
|
||||
import {
|
||||
GrpcForm,
|
||||
HttpUpgradeForm,
|
||||
@@ -453,6 +454,7 @@ export default function OutboundFormModal({
|
||||
)}
|
||||
|
||||
{protocol === 'wireguard' && <WireguardFields />}
|
||||
{protocol === 'amneziawg' && <AmneziawgFields />}
|
||||
|
||||
{streamAllowed && network && (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input, InputNumber, Space, Switch } from 'antd';
|
||||
import { MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Wireguard } from '@/utils';
|
||||
import { activateOnKey } from '@/utils/a11y';
|
||||
import { InputAddon } from '@/components/ui';
|
||||
import { FormField } from '@/components/form/rhf';
|
||||
|
||||
// amneziawg outbound fields reuse the inbound i18n keys: identical protocol
|
||||
// parameters on both tunnel ends, so one label set serves both forms.
|
||||
export default function AmneziawgFields() {
|
||||
const { t } = useTranslation();
|
||||
const { control, setValue } = useFormContext();
|
||||
const {
|
||||
fields: peerFields,
|
||||
append: appendPeer,
|
||||
remove: removePeer,
|
||||
} = useFieldArray({ control, name: 'settings.peers' });
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField label={t('pages.xray.amneziawg.mtu')} name={['settings', 'mtu']}>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</FormField>
|
||||
<FormField
|
||||
label={t('pages.xray.amneziawg.listenPort')}
|
||||
name={['settings', 'listenPort']}
|
||||
extra={t('pages.xray.amneziawg.listenPortHint')}
|
||||
>
|
||||
<InputNumber min={0} max={65535} style={{ width: '100%' }} />
|
||||
</FormField>
|
||||
<Form.Item label={t('pages.inbounds.privatekey')}>
|
||||
<FormField name={['settings', 'secretKey']} noStyle>
|
||||
<Input
|
||||
aria-label={t('pages.inbounds.privatekey')}
|
||||
style={{ width: 'calc(100% - 32px)' }}
|
||||
/>
|
||||
</FormField>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
aria-label={t('regenerate')}
|
||||
onClick={() => {
|
||||
const pair = Wireguard.generateKeypair();
|
||||
setValue('settings.secretKey', pair.privateKey);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('pages.inbounds.address')} required>
|
||||
<FormField name={['settings', 'address', 0]} noStyle>
|
||||
<Input placeholder="10.8.0.2/32" aria-label={t('pages.inbounds.address')} />
|
||||
</FormField>
|
||||
</Form.Item>
|
||||
<FormField label={t('pages.inbounds.info.dns')} name={['settings', 'dns']}>
|
||||
<Input placeholder="1.1.1.1:53" />
|
||||
</FormField>
|
||||
<FormField
|
||||
name={['settings', 'headerProtectionKey']}
|
||||
label={t('pages.xray.amneziawg.headerProtectionKey')}
|
||||
extra={t('pages.xray.amneziawg.headerProtectionKeyHint')}
|
||||
>
|
||||
<Input />
|
||||
</FormField>
|
||||
|
||||
<Form.Item
|
||||
label={t('pages.xray.amneziawg.obfuscation')}
|
||||
extra={t('pages.xray.amneziawg.outboundObfuscationHint')}
|
||||
/>
|
||||
<ObfNumber name="jc" label={t('pages.xray.amneziawg.jc')} min={0} />
|
||||
<ObfNumber name="jmin" label={t('pages.xray.amneziawg.jmin')} min={0} />
|
||||
<ObfNumber name="jmax" label={t('pages.xray.amneziawg.jmax')} min={0} />
|
||||
<ObfNumber name="s1" label={t('pages.xray.amneziawg.s1')} min={0} />
|
||||
<ObfNumber name="s2" label={t('pages.xray.amneziawg.s2')} min={0} />
|
||||
<ObfNumber name="s3" label={t('pages.xray.amneziawg.s3')} min={0} max={64} />
|
||||
<ObfNumber name="s4" label={t('pages.xray.amneziawg.s4')} min={0} max={32} />
|
||||
<ObfText name="h1" label={t('pages.xray.amneziawg.h1')} placeholder="100-800" />
|
||||
<ObfText name="h2" label={t('pages.xray.amneziawg.h2')} placeholder="900-1600" />
|
||||
<ObfText name="h3" label={t('pages.xray.amneziawg.h3')} placeholder="1700-2400" />
|
||||
<ObfText name="h4" label={t('pages.xray.amneziawg.h4')} placeholder="2500-3200" />
|
||||
<ObfText name="i1" label={t('pages.xray.amneziawg.i1')} placeholder="<r 64>" />
|
||||
<ObfText
|
||||
name="contentPaddingAddition"
|
||||
label={t('pages.xray.amneziawg.contentPaddingAddition')}
|
||||
placeholder="8-64"
|
||||
/>
|
||||
|
||||
<Form.Item label={t('pages.inbounds.form.peers')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
aria-label={t('add')}
|
||||
onClick={() =>
|
||||
appendPeer({
|
||||
publicKey: '',
|
||||
presharedKey: '',
|
||||
allowedIPs: ['0.0.0.0/0', '::/0'],
|
||||
endpoint: '',
|
||||
keepAlive: 25,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
{peerFields.map((field, index) => (
|
||||
<div key={field.id}>
|
||||
<Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
|
||||
<div className="item-heading">
|
||||
<span>{t('pages.inbounds.info.peerNumber', { n: index + 1 })}</span>
|
||||
{peerFields.length > 1 && (
|
||||
<MinusOutlined
|
||||
className="danger-icon"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('remove')}
|
||||
onClick={() => removePeer(index)}
|
||||
onKeyDown={activateOnKey(() => removePeer(index))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
<FormField
|
||||
label={t('pages.xray.wireguard.endpoint')}
|
||||
name={['settings', 'peers', index, 'endpoint']}
|
||||
>
|
||||
<Input placeholder="203.0.113.7:51820" />
|
||||
</FormField>
|
||||
<FormField
|
||||
label={t('pages.inbounds.publicKey')}
|
||||
name={['settings', 'peers', index, 'publicKey']}
|
||||
>
|
||||
<Input />
|
||||
</FormField>
|
||||
<FormField label="PSK" name={['settings', 'peers', index, 'presharedKey']}>
|
||||
<Input />
|
||||
</FormField>
|
||||
<PeerAllowedIPs peerIndex={index} />
|
||||
<FormField
|
||||
label={t('pages.inbounds.info.keepAlive')}
|
||||
name={['settings', 'peers', index, 'keepAlive']}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</FormField>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<FormField
|
||||
name={['settings', 'randomTrailers']}
|
||||
label={t('pages.xray.amneziawg.randomTrailers')}
|
||||
valueProp="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
<FormField
|
||||
name={['settings', 'disableCookies']}
|
||||
label={t('pages.xray.amneziawg.disableCookies')}
|
||||
valueProp="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PeerAllowedIPs({ peerIndex }: { peerIndex: number }) {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: `settings.peers.${peerIndex}.allowedIPs`,
|
||||
});
|
||||
return (
|
||||
<Form.Item label={t('pages.xray.wireguard.allowedIPs')}>
|
||||
{fields.map((field, ipIdx) => (
|
||||
<Space.Compact key={field.id} block style={{ marginBottom: 4 }}>
|
||||
<FormField noStyle name={['settings', 'peers', peerIndex, 'allowedIPs', ipIdx]}>
|
||||
<Input aria-label={t('pages.xray.wireguard.allowedIPs')} />
|
||||
</FormField>
|
||||
{fields.length > 1 && (
|
||||
<InputAddon ariaLabel={t('remove')} onClick={() => remove(ipIdx)}>
|
||||
<MinusOutlined />
|
||||
</InputAddon>
|
||||
)}
|
||||
</Space.Compact>
|
||||
))}
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => append('')}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function ObfNumber({
|
||||
name,
|
||||
label,
|
||||
min,
|
||||
max,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}) {
|
||||
return (
|
||||
<FormField label={label} name={['settings', name] as never}>
|
||||
<InputNumber min={min} max={max} style={{ width: '100%' }} />
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
|
||||
function ObfText({
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
return (
|
||||
<FormField label={label} name={['settings', name] as never}>
|
||||
<Input placeholder={placeholder} />
|
||||
</FormField>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export { default as ShadowsocksFields } from './shadowsocks';
|
||||
export { default as HttpFields } from './http';
|
||||
export { default as SocksFields } from './socks';
|
||||
export { default as WireguardFields } from './wireguard';
|
||||
export { default as AmneziawgFields } from './amneziawg';
|
||||
export { default as FreedomFields } from './freedom';
|
||||
export { default as LoopbackFields } from './loopback';
|
||||
export { default as BlackholeFields } from './blackhole';
|
||||
|
||||
Reference in New Issue
Block a user