feat(xray): update xray-core to v26.7.11 and adapt panel

Bump xtls/xray-core to 50231eaf (v26.7.11) and the three binary pins
(DockerInit.sh, release.yml x2) in lockstep.

Adapt the panel to the upstream changes:

- Shadowsocks "none"/"plain" and VMess "none"/"zero" were removed from
  the core. A migration rewrites stored none/plain SS methods to a
  supported cipher and none/zero VMess security to "auto" (on both the
  clients column and inbound settings JSON); the SS build-time heal does
  the same so a row injected after boot cannot brick startup. The removed
  values are dropped from every frontend option list, schema and adapter,
  and coerced to "auto" at the Go link/sub/Clash emit sites and both link
  importers. Fix the CipherType_NONE sentinel that no longer compiles.

- Unencrypted vless/trojan outbounds to a public address are now refused
  by the core. Validate outbounds through the vendored config loader when
  saving the xray template and when storing/merging outbound
  subscriptions, so one such outbound cannot keep the core from starting.

- New TCP finalmask type "xmc" (Minecraft mimicry): add it to the sub
  link allowlist, the frontend enum and the FinalMask form (hostname,
  usernames, required password), and document it.

- streamSettings gained a "method" alias for "network"; canonicalize it
  to "network" at inbound save time and in the form adapters/schema so a
  method-keyed config keeps its transport.

- New root "env" config key is passed through xray.Config, compared in
  Equals, and forces a restart in the hot diff.

- REALITY now defaults minClientVer to 26.3.27; update the form
  placeholder.
This commit is contained in:
MHSanaei
2026-07-12 00:30:47 +02:00
parent affcf6c422
commit 814cda3fb4
39 changed files with 709 additions and 69 deletions
@@ -81,6 +81,8 @@ function defaultTcpMaskSettings(type: string): Record<string, unknown> {
};
case 'header-custom':
return { clients: [], servers: [] };
case 'xmc':
return { hostname: '', usernames: [], password: RandomUtil.randomLowerAndNum(16) };
default:
return {};
}
@@ -294,6 +296,7 @@ function TcpMaskItem({
{ value: 'fragment', label: 'Fragment' },
{ value: 'header-custom', label: 'Header Custom' },
{ value: 'sudoku', label: 'Sudoku' },
{ value: 'xmc', label: 'XMC (Minecraft)' },
]}
/>
</Form.Item>
@@ -371,6 +374,41 @@ function TcpMaskItem({
/>
);
}
if (type === 'xmc') {
return (
<>
<Form.Item label="Hostname" name={[fieldName, 'settings', 'hostname']}>
<Input placeholder="Server address mimicked in the handshake" />
</Form.Item>
<Form.Item
label="Usernames"
name={[fieldName, 'settings', 'usernames']}
extra="Player names offered to probes; core defaults to Dream when empty."
>
<Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']} />
</Form.Item>
<Form.Item label="Password" required>
<Space.Compact block>
<Form.Item
name={[fieldName, 'settings', 'password']}
noStyle
rules={[{ required: true, message: 'Password is required' }]}
>
<Input placeholder="Obfuscation password" style={{ width: 'calc(100% - 32px)' }} />
</Form.Item>
<Button
icon={<ReloadOutlined />}
aria-label={t('regenerate')}
onClick={() => form.setFieldValue(
[...absolutePath, 'settings', 'password'],
RandomUtil.randomLowerAndNum(16),
)}
/>
</Space.Compact>
</Form.Item>
</>
);
}
return null;
}}
</Form.Item>
@@ -124,6 +124,10 @@ const NETWORK_SETTINGS_KEY: Record<string, string> = {
};
function healStreamNetworkKey(stream: Record<string, unknown>): void {
if (typeof stream.method === 'string' && stream.method !== '') {
stream.network = stream.method;
}
delete stream.method;
const network = typeof stream.network === 'string' ? stream.network : '';
const key = NETWORK_SETTINGS_KEY[network];
if (!key) return;
@@ -107,7 +107,7 @@ function vmessFromWire(raw: Raw): VmessOutboundFormSettings {
id: asString(u.id),
security: ((): VmessOutboundFormSettings['security'] => {
const s = asString(u.security);
const allowed = ['aes-128-gcm', 'chacha20-poly1305', 'auto', 'none', 'zero'];
const allowed = ['aes-128-gcm', 'chacha20-poly1305', 'auto'];
return (allowed.includes(s) ? s : 'auto') as VmessOutboundFormSettings['security'];
})(),
};
@@ -391,6 +391,10 @@ const XMUX_DEFAULTS = XHttpXmuxSchema.parse({});
function hydrateStreamForm(stream: Raw): OutboundStreamFormValues {
const next = { ...stream };
if (typeof next.method === 'string' && next.method !== '') {
next.network = next.method;
}
delete next.method;
const xh = next.xhttpSettings;
if (xh && typeof xh === 'object' && !Array.isArray(xh)) {
const xhttp = { ...(xh as Raw) };
@@ -360,6 +360,8 @@ export function parseVmessLink(link: string): Raw | null {
}
const port = Number(json.port) || 443;
const rawScy = (json.scy as string) || 'auto';
const userSecurity = rawScy === 'none' || rawScy === 'zero' ? 'auto' : rawScy;
return {
protocol: 'vmess',
tag: typeof json.ps === 'string' ? json.ps : '',
@@ -367,7 +369,7 @@ export function parseVmessLink(link: string): Raw | null {
vnext: [{
address: json.add ?? '',
port,
users: [{ id: json.id ?? '', security: (json.scy as string) || 'auto' }],
users: [{ id: json.id ?? '', security: userSecurity }],
}],
},
streamSettings: stream,
@@ -36,7 +36,7 @@ import { useFail2banStatusQuery, getLimitIpNotice } from '@/api/queries/useFail2
import { ClientFormSchema, ClientCreateFormSchema, type ClientFormValues } from '@/schemas/client';
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
const VMESS_SECURITY_OPTIONS = ['auto', 'aes-128-gcm', 'chacha20-poly1305', 'none', 'zero'] as const;
const VMESS_SECURITY_OPTIONS = ['auto', 'aes-128-gcm', 'chacha20-poly1305'] as const;
const MULTI_CLIENT_PROTOCOLS = new Set([
'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'wireguard', 'mtproto',
@@ -217,7 +217,9 @@ export default function ClientFormModal({
password: client.password || '',
auth: client.auth || '',
flow: client.flow || '',
security: client.security || 'auto',
security: !client.security || client.security === 'none' || client.security === 'zero'
? 'auto'
: client.security,
reverseTag: client.reverse?.tag || '',
totalGB: bytesToGB(client.totalGB || 0),
reset: Number(client.reset) || 0,
@@ -128,7 +128,7 @@ export default function RealityForm({
name={['streamSettings', 'realitySettings', 'minClientVer']}
label={t('pages.inbounds.form.minClientVer')}
>
<Input placeholder="25.9.11" />
<Input placeholder="26.3.27" />
</FormField>
<FormField
name={['streamSettings', 'realitySettings', 'maxClientVer']}
@@ -30,8 +30,6 @@ export const USERS_SECURITY = Object.freeze({
AES_128_GCM: 'aes-128-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
AUTO: 'auto',
NONE: 'none',
ZERO: 'zero',
});
export const MODE_OPTION = Object.freeze({
@@ -4,16 +4,16 @@ const VmessSecurityEnum = z.enum([
'aes-128-gcm',
'chacha20-poly1305',
'auto',
'none',
'zero',
]);
// Legacy rows persisted `security: ""` (especially on VMess inbounds
// created before the enum was nailed down). Preprocess maps the empty
// string back to the documented default so existing data parses cleanly
// — subsequent writes serialize the normalized value.
// created before the enum was nailed down), and rows predating xray-core
// v26.7.11 may still hold the removed "none"/"zero" values that the core
// now treats as "auto". Preprocess maps all of them to the documented
// default so existing data parses cleanly — subsequent writes serialize
// the normalized value.
export const VmessSecuritySchema = z.preprocess(
(val) => (val === '' ? 'auto' : val),
(val) => (val === '' || val === 'none' || val === 'zero' ? 'auto' : val),
VmessSecurityEnum,
);
export type VmessSecurity = z.infer<typeof VmessSecurityEnum>;
@@ -5,12 +5,12 @@ import { z } from 'zod';
// plus optional QUIC tuning. The `settings` sub-object is polymorphic on
// `type`; we model the wire-faithful shape with a permissive
// record-of-unknown for `settings` and leave per-type tightening to
// Step 6 — there are 8 UDP mask types plus 3 TCP mask types, each with
// Step 6 — there are 8 UDP mask types plus 4 TCP mask types, each with
// distinct setting fields, and modeling them all as discriminated unions
// here would dwarf the rest of the stream module without buying anything
// the safety net doesn't already cover.
export const TcpMaskTypeSchema = z.enum(['fragment', 'sudoku', 'header-custom']);
export const TcpMaskTypeSchema = z.enum(['fragment', 'sudoku', 'header-custom', 'xmc']);
export type TcpMaskType = z.infer<typeof TcpMaskTypeSchema>;
export const TcpMaskSchema = z.object({
+22 -4
View File
@@ -52,10 +52,28 @@ const TransportNetworkSettingsSchema = z.discriminatedUnion('network', [
// mode. The transportless branch accepts that shape (network absent), while a
// present-but-invalid network still fails both branches so a typo can't slip
// through. `network: never().optional()` reads as "this key must be absent".
export const NetworkSettingsSchema = z.union([
TransportNetworkSettingsSchema,
z.object({ network: z.never().optional() }),
]);
//
// The preprocess folds `method` — xray-core v26.7.11's preferred alias for
// `network`, which wins over `network` when both are present — back into the
// panel-canonical `network` key, so imported/pasted configs keyed on the
// alias don't silently match the transportless branch and lose their
// transport.
export const NetworkSettingsSchema = z.preprocess(
(val) => {
if (val && typeof val === 'object' && 'method' in val) {
const { method, ...rest } = val as Record<string, unknown>;
if (typeof method === 'string' && method !== '') {
return { ...rest, network: method };
}
return rest;
}
return val;
},
z.union([
TransportNetworkSettingsSchema,
z.object({ network: z.never().optional() }),
]),
);
export type NetworkSettings = z.infer<typeof NetworkSettingsSchema>;
// Orthogonal extras that ride alongside the network and security branches.
@@ -167,6 +167,16 @@ exports[`FinalMaskStreamSettingsSchema fixtures > parses tcp-mask byte-stably 1`
},
"type": "header-custom",
},
{
"settings": {
"hostname": "mc.example.com",
"password": "s3cr3t",
"usernames": [
"Dream",
],
},
"type": "xmc",
},
],
"udp": [],
}
@@ -25,6 +25,14 @@
],
"errors": []
}
},
{
"type": "xmc",
"settings": {
"hostname": "mc.example.com",
"usernames": ["Dream"],
"password": "s3cr3t"
}
}
]
}
+13
View File
@@ -24,3 +24,16 @@ describe('NetworkSettingsSchema fixtures', () => {
});
}
});
describe('NetworkSettingsSchema method alias', () => {
it('folds xray-core v26.7.11 method alias back into network', () => {
const parsed = NetworkSettingsSchema.parse({ method: 'ws', wsSettings: {} });
expect((parsed as { network?: string }).network).toBe('ws');
expect((parsed as Record<string, unknown>).method).toBeUndefined();
});
it('prefers method over network when both are present', () => {
const parsed = NetworkSettingsSchema.parse({ method: 'grpc', network: 'tcp', grpcSettings: {} });
expect((parsed as { network?: string }).network).toBe('grpc');
});
});