mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-12 07:36:07 +00:00
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:
@@ -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({
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user