Jelajahi Sumber

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.
MHSanaei 7 jam lalu
induk
melakukan
32f96298f8

+ 44 - 22
frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx

@@ -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>
               </>
             );

+ 3 - 3
frontend/src/pages/inbounds/form/InboundFormModal.tsx

@@ -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: '' } }],
         };
       }
     }

+ 1 - 1
frontend/src/pages/inbounds/list/useInboundColumns.tsx

@@ -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;

+ 3 - 9
frontend/src/schemas/protocols/stream/finalmask.ts

@@ -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>;
 

+ 28 - 8
frontend/src/test/__snapshots__/finalmask.test.ts.snap

@@ -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://[email protected]/my-realm",
+      },
+      "type": "realm",
+    },
   ],
 }
 `;

+ 1 - 1
frontend/src/test/golden/fixtures/finalmask/combined.json

@@ -4,7 +4,7 @@
   ],
   "udp": [
     { "type": "salamander", "settings": { "password": "swordfish" } },
-    { "type": "header-wireguard" }
+    { "type": "mkcp-legacy", "settings": { "header": "wireguard", "value": "" } }
   ],
   "quicParams": {
     "congestion": "brutal",

+ 11 - 4
frontend/src/test/golden/fixtures/finalmask/udp-mask.json

@@ -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://[email protected]/my-realm",
+        "stunServers": ["stun.l.google.com:19302", "global.stun.twilio.com:3478"]
+      }
     }
   ]
 }

+ 23 - 31
sub/subService.go

@@ -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
 		}
 	}
 

+ 40 - 0
sub/subService_test.go

@@ -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)