mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-04 03:19:34 +00:00
feat(finalmask): sync transport with upstream Xray core changes
Consolidate the eight legacy mKCP/header UDP mask types into a single mkcp-legacy type ({header, value}), simplify xicmp to {dgram, ips}, and add the new realm UDP mask type, matching the updated Xray-core wire format. Update the FinalMask schema enum, the transport form, the mKCP seeding default, and the backend KCP share-link translation. Refresh golden fixtures/snapshots and add backend coverage for the mapping.
This commit is contained in:
@@ -48,14 +48,15 @@ function defaultTcpMaskSettings(type: string): Record<string, unknown> {
|
||||
function defaultUdpMaskSettings(type: string): Record<string, unknown> {
|
||||
switch (type) {
|
||||
case 'salamander':
|
||||
case 'mkcp-aes128gcm':
|
||||
return { password: '' };
|
||||
case 'header-dns':
|
||||
return { domain: '' };
|
||||
case 'mkcp-legacy':
|
||||
return { header: '', value: '' };
|
||||
case 'xdns':
|
||||
return { domains: [] };
|
||||
case 'xicmp':
|
||||
return { ip: '0.0.0.0', id: 0 };
|
||||
return { dgram: false, ips: [] };
|
||||
case 'realm':
|
||||
return { url: '', stunServers: [] };
|
||||
case 'header-custom':
|
||||
return { client: [], server: [] };
|
||||
case 'noise':
|
||||
@@ -344,7 +345,7 @@ function UdpMasksList({
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
const def = isHysteria ? 'salamander' : 'mkcp-aes128gcm';
|
||||
const def = isHysteria ? 'salamander' : 'mkcp-legacy';
|
||||
add({ type: def, settings: defaultUdpMaskSettings(def) });
|
||||
}}
|
||||
/>
|
||||
@@ -391,16 +392,10 @@ function UdpMaskItem({
|
||||
const options = isHysteria
|
||||
? [{ value: 'salamander', label: 'Salamander (Hysteria2)' }]
|
||||
: [
|
||||
{ value: 'mkcp-aes128gcm', label: 'mKCP AES-128-GCM' },
|
||||
{ value: 'header-dns', label: 'Header DNS' },
|
||||
{ value: 'header-dtls', label: 'Header DTLS 1.2' },
|
||||
{ value: 'header-srtp', label: 'Header SRTP' },
|
||||
{ value: 'header-utp', label: 'Header uTP' },
|
||||
{ value: 'header-wechat', label: 'Header WeChat Video' },
|
||||
{ value: 'header-wireguard', label: 'Header WireGuard' },
|
||||
{ value: 'mkcp-original', label: 'mKCP Original' },
|
||||
{ value: 'mkcp-legacy', label: 'mKCP Legacy' },
|
||||
{ value: 'xdns', label: 'xDNS' },
|
||||
{ value: 'xicmp', label: 'xICMP' },
|
||||
{ value: 'realm', label: 'Realm' },
|
||||
{ value: 'header-custom', label: 'Header Custom' },
|
||||
{ value: 'noise', label: 'Noise' },
|
||||
];
|
||||
@@ -422,7 +417,7 @@ function UdpMaskItem({
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const type = getFieldValue([...absolutePath, 'type']) as string | undefined;
|
||||
if (type === 'mkcp-aes128gcm' || type === 'salamander') {
|
||||
if (type === 'salamander') {
|
||||
return (
|
||||
<Form.Item label="Password">
|
||||
<Space.Compact block>
|
||||
@@ -440,11 +435,26 @@ function UdpMaskItem({
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
if (type === 'header-dns') {
|
||||
if (type === 'mkcp-legacy') {
|
||||
return (
|
||||
<Form.Item label="Domain" name={[fieldName, 'settings', 'domain']}>
|
||||
<Input placeholder="e.g., www.example.com" />
|
||||
</Form.Item>
|
||||
<>
|
||||
<Form.Item label="Header" name={[fieldName, 'settings', 'header']}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '', label: 'Original / AES-128-GCM' },
|
||||
{ value: 'dns', label: 'DNS' },
|
||||
{ value: 'dtls', label: 'DTLS 1.2' },
|
||||
{ value: 'srtp', label: 'SRTP' },
|
||||
{ value: 'utp', label: 'uTP' },
|
||||
{ value: 'wechat', label: 'WeChat Video' },
|
||||
{ value: 'wireguard', label: 'WireGuard' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Value" name={[fieldName, 'settings', 'value']}>
|
||||
<Input placeholder="password (AES-128-GCM) or domain (DNS header)" />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (type === 'xdns') {
|
||||
@@ -457,11 +467,23 @@ function UdpMaskItem({
|
||||
if (type === 'xicmp') {
|
||||
return (
|
||||
<>
|
||||
<Form.Item label="IP" name={[fieldName, 'settings', 'ip']}>
|
||||
<Input placeholder="0.0.0.0" />
|
||||
<Form.Item label="Dgram" name={[fieldName, 'settings', 'dgram']} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item label="ID" name={[fieldName, 'settings', 'id']}>
|
||||
<InputNumber min={0} />
|
||||
<Form.Item label="IPs" name={[fieldName, 'settings', 'ips']}>
|
||||
<Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (type === 'realm') {
|
||||
return (
|
||||
<>
|
||||
<Form.Item label="URL" name={[fieldName, 'settings', 'url']}>
|
||||
<Input placeholder="realm://token@host:port/id" />
|
||||
</Form.Item>
|
||||
<Form.Item label="STUN Servers" name={[fieldName, 'settings', 'stunServers']}>
|
||||
<Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']} placeholder="host:port" />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -622,7 +622,7 @@ export default function InboundFormModal({
|
||||
}
|
||||
cleaned[`${next}Settings`] = newStreamSlice(next);
|
||||
// mKCP wants a UDP mask wrapper on the FinalMask side; seed it with
|
||||
// `mkcp-original` so the inbound boots with a sensible default
|
||||
// `mkcp-legacy` so the inbound boots with a sensible default
|
||||
// instead of unobfuscated mKCP traffic. The user can still edit or
|
||||
// clear the mask via the FinalMask section.
|
||||
if (next === 'kcp') {
|
||||
@@ -630,12 +630,12 @@ export default function InboundFormModal({
|
||||
const udp = Array.isArray(fm.udp) ? (fm.udp as unknown[]) : [];
|
||||
const hasMkcp = udp.some((m) => {
|
||||
const entry = m as { type?: string };
|
||||
return entry?.type === 'mkcp-original';
|
||||
return entry?.type === 'mkcp-legacy';
|
||||
});
|
||||
if (!hasMkcp) {
|
||||
cleaned.finalmask = {
|
||||
...fm,
|
||||
udp: [...udp, { type: 'mkcp-original', settings: {} }],
|
||||
udp: [...udp, { type: 'mkcp-legacy', settings: { header: '', value: '' } }],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export function useInboundColumns({
|
||||
title: t('clients'),
|
||||
key: 'clients',
|
||||
align: 'left',
|
||||
width: 80,
|
||||
width: 110,
|
||||
render: (_, record) => {
|
||||
const cc = clientCount[record.id];
|
||||
if (!cc) return null;
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 ~13 UDP mask types plus 3 TCP mask types, each with
|
||||
// Step 6 — there are 8 UDP mask types plus 3 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.
|
||||
@@ -21,19 +21,13 @@ export type TcpMask = z.infer<typeof TcpMaskSchema>;
|
||||
|
||||
export const UdpMaskTypeSchema = z.enum([
|
||||
'salamander',
|
||||
'mkcp-aes128gcm',
|
||||
'mkcp-original',
|
||||
'header-dns',
|
||||
'header-dtls',
|
||||
'header-srtp',
|
||||
'header-utp',
|
||||
'header-wechat',
|
||||
'header-wireguard',
|
||||
'mkcp-legacy',
|
||||
'header-custom',
|
||||
'xdns',
|
||||
'xicmp',
|
||||
'noise',
|
||||
'sudoku',
|
||||
'realm',
|
||||
]);
|
||||
export type UdpMaskType = z.infer<typeof UdpMaskTypeSchema>;
|
||||
|
||||
|
||||
@@ -27,7 +27,11 @@ exports[`FinalMaskStreamSettingsSchema fixtures > parses combined byte-stably 1`
|
||||
"type": "salamander",
|
||||
},
|
||||
{
|
||||
"type": "header-wireguard",
|
||||
"settings": {
|
||||
"header": "wireguard",
|
||||
"value": "",
|
||||
},
|
||||
"type": "mkcp-legacy",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -117,18 +121,24 @@ exports[`FinalMaskStreamSettingsSchema fixtures > parses udp-mask byte-stably 1`
|
||||
},
|
||||
{
|
||||
"settings": {
|
||||
"password": "abcdef0123456789",
|
||||
"header": "",
|
||||
"value": "abcdef0123456789",
|
||||
},
|
||||
"type": "mkcp-aes128gcm",
|
||||
"type": "mkcp-legacy",
|
||||
},
|
||||
{
|
||||
"settings": {
|
||||
"domain": "cloudflare.com",
|
||||
"header": "dns",
|
||||
"value": "cloudflare.com",
|
||||
},
|
||||
"type": "header-dns",
|
||||
"type": "mkcp-legacy",
|
||||
},
|
||||
{
|
||||
"type": "header-wireguard",
|
||||
"settings": {
|
||||
"header": "wireguard",
|
||||
"value": "",
|
||||
},
|
||||
"type": "mkcp-legacy",
|
||||
},
|
||||
{
|
||||
"settings": {
|
||||
@@ -164,11 +174,21 @@ exports[`FinalMaskStreamSettingsSchema fixtures > parses udp-mask byte-stably 1`
|
||||
},
|
||||
{
|
||||
"settings": {
|
||||
"id": 0,
|
||||
"listenIp": "0.0.0.0",
|
||||
"dgram": false,
|
||||
"ips": [],
|
||||
},
|
||||
"type": "xicmp",
|
||||
},
|
||||
{
|
||||
"settings": {
|
||||
"stunServers": [
|
||||
"stun.l.google.com:19302",
|
||||
"global.stun.twilio.com:3478",
|
||||
],
|
||||
"url": "realm://public@example.com/my-realm",
|
||||
},
|
||||
"type": "realm",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
],
|
||||
"udp": [
|
||||
{ "type": "salamander", "settings": { "password": "swordfish" } },
|
||||
{ "type": "header-wireguard" }
|
||||
{ "type": "mkcp-legacy", "settings": { "header": "wireguard", "value": "" } }
|
||||
],
|
||||
"quicParams": {
|
||||
"congestion": "brutal",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"udp": [
|
||||
{ "type": "salamander", "settings": { "password": "swordfish" } },
|
||||
{ "type": "mkcp-aes128gcm", "settings": { "password": "abcdef0123456789" } },
|
||||
{ "type": "header-dns", "settings": { "domain": "cloudflare.com" } },
|
||||
{ "type": "header-wireguard" },
|
||||
{ "type": "mkcp-legacy", "settings": { "header": "", "value": "abcdef0123456789" } },
|
||||
{ "type": "mkcp-legacy", "settings": { "header": "dns", "value": "cloudflare.com" } },
|
||||
{ "type": "mkcp-legacy", "settings": { "header": "wireguard", "value": "" } },
|
||||
{
|
||||
"type": "noise",
|
||||
"settings": {
|
||||
@@ -23,7 +23,14 @@
|
||||
},
|
||||
{
|
||||
"type": "xicmp",
|
||||
"settings": { "listenIp": "0.0.0.0", "id": 0 }
|
||||
"settings": { "dgram": false, "ips": [] }
|
||||
},
|
||||
{
|
||||
"type": "realm",
|
||||
"settings": {
|
||||
"url": "realm://public@example.com/my-realm",
|
||||
"stunServers": ["stun.l.google.com:19302", "global.stun.twilio.com:3478"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1465,28 +1465,22 @@ func applyXhttpExtraParams(xhttp map[string]any, params map[string]string) {
|
||||
}
|
||||
|
||||
var kcpMaskToHeaderType = map[string]string{
|
||||
"header-dns": "dns",
|
||||
"header-dtls": "dtls",
|
||||
"header-srtp": "srtp",
|
||||
"header-utp": "utp",
|
||||
"header-wechat": "wechat-video",
|
||||
"header-wireguard": "wireguard",
|
||||
"dns": "dns",
|
||||
"dtls": "dtls",
|
||||
"srtp": "srtp",
|
||||
"utp": "utp",
|
||||
"wechat": "wechat-video",
|
||||
"wireguard": "wireguard",
|
||||
}
|
||||
|
||||
var validFinalMaskUDPTypes = map[string]struct{}{
|
||||
"salamander": {},
|
||||
"mkcp-aes128gcm": {},
|
||||
"header-dns": {},
|
||||
"header-dtls": {},
|
||||
"header-srtp": {},
|
||||
"header-utp": {},
|
||||
"header-wechat": {},
|
||||
"header-wireguard": {},
|
||||
"mkcp-original": {},
|
||||
"xdns": {},
|
||||
"xicmp": {},
|
||||
"noise": {},
|
||||
"header-custom": {},
|
||||
"salamander": {},
|
||||
"mkcp-legacy": {},
|
||||
"xdns": {},
|
||||
"xicmp": {},
|
||||
"noise": {},
|
||||
"header-custom": {},
|
||||
"realm": {},
|
||||
}
|
||||
|
||||
var validFinalMaskTCPTypes = map[string]struct{}{
|
||||
@@ -1557,21 +1551,19 @@ func extractKcpShareFields(stream map[string]any) kcpShareFields {
|
||||
if mask == nil {
|
||||
continue
|
||||
}
|
||||
maskType, _ := mask["type"].(string)
|
||||
if mapped, ok := kcpMaskToHeaderType[maskType]; ok {
|
||||
fields.headerType = mapped
|
||||
if maskType, _ := mask["type"].(string); maskType != "mkcp-legacy" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch maskType {
|
||||
case "mkcp-original":
|
||||
fields.seed = ""
|
||||
case "mkcp-aes128gcm":
|
||||
fields.seed = ""
|
||||
settings, _ := mask["settings"].(map[string]any)
|
||||
if value, ok := settings["password"].(string); ok && value != "" {
|
||||
fields.seed = value
|
||||
}
|
||||
settings, _ := mask["settings"].(map[string]any)
|
||||
header, _ := settings["header"].(string)
|
||||
value, _ := settings["value"].(string)
|
||||
if header == "" {
|
||||
fields.seed = value
|
||||
continue
|
||||
}
|
||||
if mapped, ok := kcpMaskToHeaderType[header]; ok {
|
||||
fields.headerType = mapped
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -665,6 +665,46 @@ func TestExtractKcpShareFields_ReadsAllFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractKcpShareFields_FinalMaskLegacyHeader(t *testing.T) {
|
||||
stream := map[string]any{
|
||||
"finalmask": map[string]any{
|
||||
"udp": []any{
|
||||
map[string]any{
|
||||
"type": "mkcp-legacy",
|
||||
"settings": map[string]any{"header": "wechat", "value": ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
got := extractKcpShareFields(stream)
|
||||
if got.headerType != "wechat-video" {
|
||||
t.Fatalf("headerType = %q, want wechat-video", got.headerType)
|
||||
}
|
||||
if got.seed != "" {
|
||||
t.Fatalf("seed = %q, want empty for header mask", got.seed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractKcpShareFields_FinalMaskLegacySeed(t *testing.T) {
|
||||
stream := map[string]any{
|
||||
"finalmask": map[string]any{
|
||||
"udp": []any{
|
||||
map[string]any{
|
||||
"type": "mkcp-legacy",
|
||||
"settings": map[string]any{"header": "", "value": "obfs-pass"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
got := extractKcpShareFields(stream)
|
||||
if got.headerType != "none" {
|
||||
t.Fatalf("headerType = %q, want none for empty-header legacy mask", got.headerType)
|
||||
}
|
||||
if got.seed != "obfs-pass" {
|
||||
t.Fatalf("seed = %q, want obfs-pass", got.seed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKcpShareFields_ApplyToParams(t *testing.T) {
|
||||
params := map[string]string{}
|
||||
kcpShareFields{headerType: "wechat-video", seed: "s", mtu: 1350, tti: 50}.applyToParams(params)
|
||||
|
||||
Reference in New Issue
Block a user