feat: support latest Wireguard features from Xray-core (PRs #5643, #5833, #5850) (#5131)

* feat: support latest Wireguard features from Xray-core

Implements support for Xray-core PRs #5833, #5643, and #5850 for Wireguard Inbounds:
- Adds 'domainStrategy' and 'workers' to Wireguard inbound configuration.
- Enables the Stream Settings tab for Wireguard inbounds to configure 'sockopt' and 'finalmask', hiding the irrelevant 'network' transmission dropdown.
- Adds the 'randRange' field to the 'noise' UDP Finalmask obfuscation settings.

* fix

---------

Co-authored-by: Rqzbeh <Rqzbeh@example.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Rouzbeh†
2026-06-10 17:02:41 +02:00
committed by GitHub
parent f9b275dd23
commit 4002be4ade
8 changed files with 120 additions and 33 deletions
@@ -96,8 +96,12 @@ function defaultUdpHop(): Record<string, unknown> {
export default function FinalMaskForm({ name, network, protocol, form, showAll = false }: FinalMaskFormProps) {
const base = asPath(name);
const isHysteria = protocol === OutboundProtocols.Hysteria || protocol === 'hysteria';
const showTcp = showAll || TCP_NETWORKS.includes(network);
const showUdp = showAll || isHysteria || network === 'kcp';
// Wireguard carries no user-selectable transport (always a UDP listener/
// dialer), so only the UDP mask section applies — TCP masks would never
// wrap anything even though the leftover network value may be 'tcp'.
const isWireguard = protocol === 'wireguard';
const showTcp = showAll || (!isWireguard && TCP_NETWORKS.includes(network));
const showUdp = showAll || isHysteria || isWireguard || network === 'kcp';
const showQuic = showAll || isHysteria || network === 'xhttp';
const quicParams = Form.useWatch([...base, 'quicParams'], { form, preserve: true });
const hasQuicParams = quicParams != null;
@@ -107,7 +111,7 @@ export default function FinalMaskForm({ name, network, protocol, form, showAll =
return (
<>
{showTcp && <TcpMasksList base={base} form={form} />}
{showUdp && <UdpMasksList base={base} form={form} isHysteria={isHysteria} network={network} />}
{showUdp && <UdpMasksList base={base} form={form} isHysteria={isHysteria} isWireguard={isWireguard} network={network} />}
{showQuic && (
<>
<Form.Item label="QUIC Params">
@@ -275,6 +279,22 @@ function validateFragmentLength(_rule: unknown, value: unknown): Promise<void> {
return Promise.resolve();
}
// randRange bytes must sit in 0-255 — xray rejects the whole config with
// "invalid randRange" otherwise (reversed ranges like "200-100" are fine,
// xray reorders them).
function validateRandRange(_rule: unknown, value: unknown): Promise<void> {
const str = typeof value === 'string' ? value.trim() : String(value ?? '').trim();
if (str.length === 0) return Promise.resolve();
const m = /^(\d{1,3})(?:-(\d{1,3}))?$/.exec(str);
if (!m) return Promise.reject(new Error('Use a byte value or range like 0-255'));
const from = Number(m[1]);
const to = m[2] !== undefined ? Number(m[2]) : from;
if (from > 255 || to > 255) {
return Promise.reject(new Error('randRange bytes must be within 0-255'));
}
return Promise.resolve();
}
function getDeep(obj: unknown, path: (string | number)[]): unknown {
let cur: unknown = obj;
for (const key of path) {
@@ -345,8 +365,8 @@ function HeaderCustomGroups({
}
function UdpMasksList({
base, form, isHysteria, network,
}: { base: (string | number)[]; form: FormInstance; isHysteria: boolean; network: string }) {
base, form, isHysteria, isWireguard, network,
}: { base: (string | number)[]; form: FormInstance; isHysteria: boolean; isWireguard: boolean; network: string }) {
return (
<Form.List name={[...base, 'udp']}>
{(fields, { add, remove }) => (
@@ -357,7 +377,7 @@ function UdpMasksList({
size="small"
icon={<PlusOutlined />}
onClick={() => {
const def = isHysteria ? 'salamander' : 'mkcp-legacy';
const def = isHysteria || isWireguard ? 'salamander' : 'mkcp-legacy';
add({ type: def, settings: defaultUdpMaskSettings(def) });
}}
/>
@@ -370,6 +390,7 @@ function UdpMasksList({
form={form}
listPath={[...base, 'udp']}
isHysteria={isHysteria}
isWireguard={isWireguard}
network={network}
onRemove={() => remove(field.name)}
/>
@@ -381,13 +402,14 @@ function UdpMasksList({
}
function UdpMaskItem({
fieldName, displayIndex, form, listPath, isHysteria, network, onRemove,
fieldName, displayIndex, form, listPath, isHysteria, isWireguard, network, onRemove,
}: {
fieldName: number;
displayIndex: number;
form: FormInstance;
listPath: (string | number)[];
isHysteria: boolean;
isWireguard: boolean;
network: string;
onRemove: () => void;
}) {
@@ -404,6 +426,9 @@ function UdpMaskItem({
const options = isHysteria
? [{ value: 'salamander', label: 'Salamander (Hysteria2)' }]
: [
// Salamander is the mask xray-core's own wireguard finalmask example
// uses; it stays hysteria-only elsewhere to keep legacy parity.
...(isWireguard ? [{ value: 'salamander', label: 'Salamander' }] : []),
{ value: 'mkcp-legacy', label: 'mKCP Legacy' },
{ value: 'xdns', label: 'xDNS' },
{ value: 'xicmp', label: 'xICMP' },
@@ -674,7 +699,15 @@ function ItemEditor({
<InputNumber min={0} />
)}
</Form.Item>
<Form.Item label="Rand Range" name={[fieldName, 'randRange']}>
{/* Cleared must become undefined, not '': xray parses an
explicit "" as the range 0-0 (all-zero fill bytes), while
an omitted randRange falls back to the 0-255 default. */}
<Form.Item
label="Rand Range"
name={[fieldName, 'randRange']}
normalize={(v) => (v === '' ? undefined : v)}
rules={[{ validator: validateRandRange }]}
>
<Input placeholder="0-255" />
</Form.Item>
</>
@@ -9,7 +9,7 @@ const TLS_ELIGIBLE_PROTOCOLS = ['vmess', 'vless', 'trojan', 'shadowsocks'];
const TLS_NETWORKS = ['tcp', 'ws', 'http', 'grpc', 'httpupgrade', 'xhttp'];
const REALITY_ELIGIBLE_PROTOCOLS = ['vless', 'trojan'];
const REALITY_NETWORKS = ['tcp', 'http', 'grpc', 'xhttp'];
const STREAM_PROTOCOLS = ['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria'];
const STREAM_PROTOCOLS = ['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria', 'wireguard'];
const VISION_FLOW = 'xtls-rprx-vision';
const SS_2022_PREFIX = '2022';
const SS_BLAKE3_CHACHA20 = '2022-blake3-chacha20-poly1305';
@@ -372,9 +372,15 @@ export default function InboundFormModal({
}],
},
});
} else if (next === Protocols.WIREGUARD) {
// Wireguard has no user-selectable transport: the listener is always
// UDP and only finalmask/sockopt from streamSettings apply. Drop the
// leftover network/transport slices so the stream tab doesn't render
// a TCP sub-form and the wire payload carries no dead tcpSettings.
form.setFieldValue('streamSettings', { security: 'none' });
} else {
const current = form.getFieldValue('streamSettings') as { network?: string } | undefined;
if (current?.network === 'hysteria') {
if (current?.network === 'hysteria' || !current?.network) {
form.setFieldValue('streamSettings', { network: 'tcp', security: 'none', tcpSettings: {} });
}
}
@@ -645,7 +651,7 @@ export default function InboundFormModal({
const streamTab = (
<>
{protocol !== Protocols.HYSTERIA && (
{protocol !== Protocols.HYSTERIA && protocol !== Protocols.WIREGUARD && (
<Form.Item label={t('transmission')} name={['streamSettings', 'network']}>
<Select
style={{ width: '75%' }}
@@ -683,7 +689,10 @@ export default function InboundFormModal({
{network === 'kcp' && <KcpForm />}
<ExternalProxyForm toggleExternalProxy={toggleExternalProxy} />
{/* externalProxy only feeds client share links, and wireguard's
per-peer .conf fanout resolves its host elsewhere — the section
would be dead weight on a wireguard inbound. */}
{protocol !== Protocols.WIREGUARD && <ExternalProxyForm toggleExternalProxy={toggleExternalProxy} />}
<SockoptForm toggleSockopt={toggleSockopt} />
@@ -897,7 +906,11 @@ export default function InboundFormModal({
...(streamEnabled
? [
{ key: 'stream', label: t('pages.inbounds.streamTab'), children: streamTab, forceRender: true },
{ key: 'security', label: t('pages.inbounds.securityTab'), children: securityTab, forceRender: true },
// Wireguard can't do TLS/Reality (canEnableTls is false), so
// the security tab would only show a fully disabled radio.
...(protocol !== Protocols.WIREGUARD
? [{ key: 'security', label: t('pages.inbounds.securityTab'), children: securityTab, forceRender: true }]
: []),
]
: []),
...(sniffingSupported
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next';
import { Button, Divider, Form, Input, InputNumber, Space, Switch } from 'antd';
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import { MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import { Wireguard } from '@/utils';
@@ -62,6 +62,21 @@ export default function WireguardFields({ wgPubKey, regenInboundWg, regenWgPeerK
>
<Switch />
</Form.Item>
<Form.Item name={['settings', 'workers']} label='Workers'>
<InputNumber min={1} />
</Form.Item>
<Form.Item name={['settings', 'domainStrategy']} label={t('pages.xray.wireguard.domainStrategy')}>
<Select
allowClear
options={[
{ value: 'ForceIP', label: 'ForceIP' },
{ value: 'ForceIPv4', label: 'ForceIPv4' },
{ value: 'ForceIPv4v6', label: 'ForceIPv4v6' },
{ value: 'ForceIPv6', label: 'ForceIPv6' },
{ value: 'ForceIPv6v4', label: 'ForceIPv6v4' },
]}
/>
</Form.Item>
<Form.List name={['settings', 'peers']}>
{(fields, { add, remove }) => (
<>
@@ -156,10 +156,17 @@ export default function OutboundFormModal({
useEffect(() => {
if (!streamAllowed) return;
// Wireguard dials its own UDP — only finalmask/sockopt apply, never a
// transport. Don't seed network 'tcp'; clear a leftover one (from a
// protocol switch) so the transmission/security blocks stay hidden.
if (protocol === 'wireguard') {
if (network) form.setFieldValue('streamSettings', { security: 'none' });
return;
}
if (network) return;
form.setFieldValue('streamSettings', { ...newStreamSlice('tcp'), security: 'none' });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [streamAllowed, network]);
}, [streamAllowed, network, protocol]);
useEffect(() => {
if (protocol !== 'hysteria') return;
@@ -565,7 +572,7 @@ export default function OutboundFormModal({
{security === 'reality' && realityAllowed && <RealityForm />}
{((streamAllowed && network) || !streamAllowed) && (
{((streamAllowed && network) || !streamAllowed || protocol === 'wireguard') && (
<SockoptForm form={form} outboundTags={existingTags} />
)}
@@ -1,5 +1,20 @@
import { z } from 'zod';
export const WireguardDomainStrategySchema = z.enum([
'ForceIP',
'ForceIPv4',
'ForceIPv4v6',
'ForceIPv6',
'ForceIPv6v4',
]);
export type WireguardDomainStrategy = z.infer<typeof WireguardDomainStrategySchema>;
// AntD InputNumber emits null (not undefined) when the user clears it, and
// the form store hands that null straight to safeParse on submit — a bare
// .optional() would reject it and block the save.
const optionalClearedInt = (schema: z.ZodNumber) =>
z.preprocess((v) => (v == null ? undefined : v), schema.optional());
// Wireguard inbound is peer-based (no clients). Each peer is a client device
// the server accepts; secretKey is the server-side private key and pubKey is
// derived from it at runtime (not persisted on the wire). Inbound peers
@@ -10,14 +25,16 @@ export const WireguardInboundPeerSchema = z.object({
publicKey: z.string().min(1),
preSharedKey: z.string().optional(),
allowedIPs: z.array(z.string()).default([]),
keepAlive: z.number().int().min(0).optional(),
keepAlive: optionalClearedInt(z.number().int().min(0)),
});
export type WireguardInboundPeer = z.infer<typeof WireguardInboundPeerSchema>;
export const WireguardInboundSettingsSchema = z.object({
mtu: z.number().int().min(1).optional(),
mtu: optionalClearedInt(z.number().int().min(1)),
secretKey: z.string().min(1),
peers: z.array(WireguardInboundPeerSchema).default([]),
noKernelTun: z.boolean().default(false),
workers: optionalClearedInt(z.number().int().min(1)),
domainStrategy: WireguardDomainStrategySchema.optional(),
});
export type WireguardInboundSettings = z.infer<typeof WireguardInboundSettingsSchema>;
@@ -1683,7 +1683,7 @@ exports[`protocol capability predicates > vmess-basic :: xhttp/tls 1`] = `
exports[`protocol capability predicates > wireguard-basic :: grpc/none 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1695,7 +1695,7 @@ exports[`protocol capability predicates > wireguard-basic :: grpc/none 1`] = `
exports[`protocol capability predicates > wireguard-basic :: grpc/reality 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1707,7 +1707,7 @@ exports[`protocol capability predicates > wireguard-basic :: grpc/reality 1`] =
exports[`protocol capability predicates > wireguard-basic :: grpc/tls 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1719,7 +1719,7 @@ exports[`protocol capability predicates > wireguard-basic :: grpc/tls 1`] = `
exports[`protocol capability predicates > wireguard-basic :: httpupgrade/none 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1731,7 +1731,7 @@ exports[`protocol capability predicates > wireguard-basic :: httpupgrade/none 1`
exports[`protocol capability predicates > wireguard-basic :: httpupgrade/tls 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1743,7 +1743,7 @@ exports[`protocol capability predicates > wireguard-basic :: httpupgrade/tls 1`]
exports[`protocol capability predicates > wireguard-basic :: kcp/none 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1755,7 +1755,7 @@ exports[`protocol capability predicates > wireguard-basic :: kcp/none 1`] = `
exports[`protocol capability predicates > wireguard-basic :: tcp/none 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1767,7 +1767,7 @@ exports[`protocol capability predicates > wireguard-basic :: tcp/none 1`] = `
exports[`protocol capability predicates > wireguard-basic :: tcp/reality 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1779,7 +1779,7 @@ exports[`protocol capability predicates > wireguard-basic :: tcp/reality 1`] = `
exports[`protocol capability predicates > wireguard-basic :: tcp/tls 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1791,7 +1791,7 @@ exports[`protocol capability predicates > wireguard-basic :: tcp/tls 1`] = `
exports[`protocol capability predicates > wireguard-basic :: ws/none 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1803,7 +1803,7 @@ exports[`protocol capability predicates > wireguard-basic :: ws/none 1`] = `
exports[`protocol capability predicates > wireguard-basic :: ws/tls 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1815,7 +1815,7 @@ exports[`protocol capability predicates > wireguard-basic :: ws/tls 1`] = `
exports[`protocol capability predicates > wireguard-basic :: xhttp/none 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1827,7 +1827,7 @@ exports[`protocol capability predicates > wireguard-basic :: xhttp/none 1`] = `
exports[`protocol capability predicates > wireguard-basic :: xhttp/reality 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
@@ -1839,7 +1839,7 @@ exports[`protocol capability predicates > wireguard-basic :: xhttp/reality 1`] =
exports[`protocol capability predicates > wireguard-basic :: xhttp/tls 1`] = `
{
"canEnableReality": false,
"canEnableStream": false,
"canEnableStream": true,
"canEnableTls": false,
"canEnableTlsFlow": false,
"canEnableVisionSeed": false,
+3 -1
View File
@@ -289,7 +289,8 @@ func (s *InboundService) getAllEmailSubIDs() (map[string]string, error) {
}
// normalizeStreamSettings clears StreamSettings for protocols that don't use it.
// Only vmess, vless, trojan, shadowsocks, and hysteria protocols use streamSettings.
// Only vmess, vless, trojan, shadowsocks, hysteria, and wireguard protocols use
// streamSettings (wireguard for finalmask UDP masks and sockopt on its listener).
func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
protocolsWithStream := map[model.Protocol]bool{
model.VMESS: true,
@@ -297,6 +298,7 @@ func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
model.Trojan: true,
model.Shadowsocks: true,
model.Hysteria: true,
model.WireGuard: true,
}
if !protocolsWithStream[inbound.Protocol] {