Преглед на файлове

Feat: clarify VLESS encryption auth selection (#4271)

* feat(traffic_writer): enhance traffic writer with concurrency safety and state management

* Revert "feat(traffic_writer): enhance traffic writer with concurrency safety and state management"

This reverts commit e6760ae39629a592dec293197768f27ff0f5a578.

* feat(vless): clarify VLESS encryption auth selection and enhance parsing logic
Farhad H. P. Shirvan преди 16 часа
родител
ревизия
fdaa65ad7e
променени са 4 файла, в които са добавени 151 реда и са изтрити 17 реда
  1. 1 1
      frontend/src/pages/api-docs/endpoints.js
  2. 42 10
      frontend/src/pages/inbounds/InboundFormModal.vue
  3. 26 6
      web/service/server.go
  4. 82 0
      web/service/server_vlessenc_test.go

+ 1 - 1
frontend/src/pages/api-docs/endpoints.js

@@ -325,7 +325,7 @@ export const sections = [
       {
         method: 'GET',
         path: '/panel/api/server/getNewVlessEnc',
-        summary: 'Generate a new VLESS encryption keypair.',
+        summary: 'Generate VLESS encryption auth options. Returns auths with id, label, decryption, and encryption.',
       },
       {
         method: 'POST',

+ 42 - 10
frontend/src/pages/inbounds/InboundFormModal.vue

@@ -393,16 +393,29 @@ async function fetchDefaultCertSettings() {
 }
 
 // === VLESS encryption helpers =======================================
-// `xray vlessenc` returns both X25519 and ML-KEM-768 variants every
-// call; the user clicks one of two buttons to pick which block goes
-// into decryption/encryption.
-async function getNewVlessEnc(authLabel) {
-  if (!authLabel || !inbound.value?.settings) return;
+// `xray vlessenc` returns both X25519 and ML-KEM-768 auth variants every
+// call; the user clicks one button to pick which block goes into
+// decryption/encryption. Both generated strings share the same hybrid
+// mlkem768x25519plus prefix; the auth choice is the final key block.
+function normalizeVlessAuthLabel(label = '') {
+  return label.toLowerCase().replace(/[-_\s]/g, '');
+}
+
+function matchesVlessAuth(block, authId) {
+  if (block?.id === authId) return true;
+  const label = normalizeVlessAuthLabel(block?.label);
+  if (authId === 'mlkem768') return label.includes('mlkem768');
+  if (authId === 'x25519') return label.includes('x25519');
+  return false;
+}
+
+async function getNewVlessEnc(authId) {
+  if (!authId || !inbound.value?.settings) return;
   saving.value = true;
   try {
     const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
     if (!msg?.success) return;
-    const block = (msg.obj?.auths || []).find((a) => a.label === authLabel);
+    const block = (msg.obj?.auths || []).find((a) => matchesVlessAuth(a, authId));
     if (!block) return;
     inbound.value.settings.decryption = block.decryption;
     inbound.value.settings.encryption = block.encryption;
@@ -417,6 +430,17 @@ function clearVlessEnc() {
   inbound.value.settings.encryption = 'none';
 }
 
+const selectedVlessAuth = computed(() => {
+  const encryption = inbound.value?.settings?.encryption;
+  if (!encryption || encryption === 'none') return 'None';
+
+  const parts = encryption.split('.').filter(Boolean);
+  const authKey = parts[parts.length - 1] || '';
+  if (!authKey) return 'Custom';
+
+  return authKey.length > 300 ? 'ML-KEM-768 auth' : 'X25519 auth';
+});
+
 // === SS method change tracks legacy semantics =========================
 function onSSMethodChange() {
   inbound.value.settings.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
@@ -731,14 +755,17 @@ watch(
           </a-form-item>
           <a-form-item label=" ">
             <a-space :size="8" wrap>
-              <a-button type="primary" :loading="saving" @click="getNewVlessEnc('X25519, not Post-Quantum')">
-                X25519
+              <a-button type="primary" :loading="saving" @click="getNewVlessEnc('x25519')">
+                X25519 auth
               </a-button>
-              <a-button type="primary" :loading="saving" @click="getNewVlessEnc('ML-KEM-768, Post-Quantum')">
-                ML-KEM-768
+              <a-button type="primary" :loading="saving" @click="getNewVlessEnc('mlkem768')">
+                ML-KEM-768 auth
               </a-button>
               <a-button danger @click="clearVlessEnc">Clear</a-button>
             </a-space>
+            <a-typography-text type="secondary" class="vless-auth-state">
+              Selected: {{ selectedVlessAuth }}
+            </a-typography-text>
           </a-form-item>
         </a-form>
 
@@ -1741,6 +1768,11 @@ watch(
   color: #ff4d4f;
 }
 
+.vless-auth-state {
+  display: block;
+  margin-top: 6px;
+}
+
 .json-editor {
   font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
   font-size: 12px;

+ 26 - 6
web/service/server.go

@@ -1275,7 +1275,13 @@ func (s *ServerService) GetNewVlessEnc() (any, error) {
 		return nil, err
 	}
 
-	lines := strings.Split(out.String(), "\n")
+	return map[string]any{
+		"auths": parseVlessEncAuths(out.String()),
+	}, nil
+}
+
+func parseVlessEncAuths(output string) []map[string]string {
+	lines := strings.Split(output, "\n")
 	var auths []map[string]string
 	var current map[string]string
 
@@ -1285,14 +1291,18 @@ func (s *ServerService) GetNewVlessEnc() (any, error) {
 			if current != nil {
 				auths = append(auths, current)
 			}
+			label := strings.TrimSpace(strings.TrimPrefix(line, "Authentication:"))
 			current = map[string]string{
-				"label": strings.TrimSpace(strings.TrimPrefix(line, "Authentication:")),
+				"id":    vlessEncAuthID(label),
+				"label": label,
 			}
 		} else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
 			parts := strings.SplitN(line, ":", 2)
 			if len(parts) == 2 && current != nil {
 				key := strings.Trim(parts[0], `" `)
-				val := strings.Trim(parts[1], `" `)
+				val := strings.TrimSpace(parts[1])
+				val = strings.TrimSuffix(val, ",")
+				val = strings.Trim(val, `" `)
 				current[key] = val
 			}
 		}
@@ -1302,9 +1312,19 @@ func (s *ServerService) GetNewVlessEnc() (any, error) {
 		auths = append(auths, current)
 	}
 
-	return map[string]any{
-		"auths": auths,
-	}, nil
+	return auths
+}
+
+func vlessEncAuthID(label string) string {
+	normalized := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(label))
+	switch {
+	case strings.Contains(normalized, "mlkem768"):
+		return "mlkem768"
+	case strings.Contains(normalized, "x25519"):
+		return "x25519"
+	default:
+		return normalized
+	}
 }
 
 func (s *ServerService) GetNewUUID() (map[string]string, error) {

+ 82 - 0
web/service/server_vlessenc_test.go

@@ -0,0 +1,82 @@
+package service
+
+import "testing"
+
+func TestParseVlessEncAuthsAddsStableIDs(t *testing.T) {
+	output := `
+Authentication: X25519, not Post-Quantum
+{
+  "decryption": "mlkem768x25519plus.native.600s.server-x25519",
+  "encryption": "mlkem768x25519plus.native.0rtt.client-x25519"
+}
+
+Authentication: ML-KEM-768, Post-Quantum
+{
+  "decryption": "mlkem768x25519plus.native.600s.server-mlkem",
+  "encryption": "mlkem768x25519plus.native.0rtt.client-mlkem"
+}
+`
+
+	auths := parseVlessEncAuths(output)
+	if len(auths) != 2 {
+		t.Fatalf("expected 2 auth blocks, got %d", len(auths))
+	}
+
+	tests := []struct {
+		index      int
+		id         string
+		label      string
+		decryption string
+		encryption string
+	}{
+		{
+			index:      0,
+			id:         "x25519",
+			label:      "X25519, not Post-Quantum",
+			decryption: "mlkem768x25519plus.native.600s.server-x25519",
+			encryption: "mlkem768x25519plus.native.0rtt.client-x25519",
+		},
+		{
+			index:      1,
+			id:         "mlkem768",
+			label:      "ML-KEM-768, Post-Quantum",
+			decryption: "mlkem768x25519plus.native.600s.server-mlkem",
+			encryption: "mlkem768x25519plus.native.0rtt.client-mlkem",
+		},
+	}
+
+	for _, test := range tests {
+		auth := auths[test.index]
+		if auth["id"] != test.id {
+			t.Errorf("auth[%d] id = %q, want %q", test.index, auth["id"], test.id)
+		}
+		if auth["label"] != test.label {
+			t.Errorf("auth[%d] label = %q, want %q", test.index, auth["label"], test.label)
+		}
+		if auth["decryption"] != test.decryption {
+			t.Errorf("auth[%d] decryption = %q, want %q", test.index, auth["decryption"], test.decryption)
+		}
+		if auth["encryption"] != test.encryption {
+			t.Errorf("auth[%d] encryption = %q, want %q", test.index, auth["encryption"], test.encryption)
+		}
+	}
+}
+
+func TestParseVlessEncAuthsHandlesMissingTrailingComma(t *testing.T) {
+	output := `
+Authentication: X25519, not Post-Quantum
+"decryption": "server"
+"encryption": "client"
+`
+
+	auths := parseVlessEncAuths(output)
+	if len(auths) != 1 {
+		t.Fatalf("expected 1 auth block, got %d", len(auths))
+	}
+	if auths[0]["decryption"] != "server" {
+		t.Fatalf("decryption = %q, want server", auths[0]["decryption"])
+	}
+	if auths[0]["encryption"] != "client" {
+		t.Fatalf("encryption = %q, want client", auths[0]["encryption"])
+	}
+}