From fdaa65ad7ed367105ae6720a970aa9c6ecf26021 Mon Sep 17 00:00:00 2001 From: "Farhad H. P. Shirvan" <9374298+farhadh@users.noreply.github.com> Date: Tue, 12 May 2026 11:39:28 +0200 Subject: [PATCH] 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 --- frontend/src/pages/api-docs/endpoints.js | 2 +- .../src/pages/inbounds/InboundFormModal.vue | 52 +++++++++--- web/service/server.go | 32 ++++++-- web/service/server_vlessenc_test.go | 82 +++++++++++++++++++ 4 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 web/service/server_vlessenc_test.go diff --git a/frontend/src/pages/api-docs/endpoints.js b/frontend/src/pages/api-docs/endpoints.js index baf2bda5..ca82e585 100644 --- a/frontend/src/pages/api-docs/endpoints.js +++ b/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', diff --git a/frontend/src/pages/inbounds/InboundFormModal.vue b/frontend/src/pages/inbounds/InboundFormModal.vue index cd01691d..fd045bbb 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.vue +++ b/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( - - X25519 + + X25519 auth - - ML-KEM-768 + + ML-KEM-768 auth Clear + + Selected: {{ selectedVlessAuth }} + @@ -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; diff --git a/web/service/server.go b/web/service/server.go index e2ad9deb..e8aed5bc 100644 --- a/web/service/server.go +++ b/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) { diff --git a/web/service/server_vlessenc_test.go b/web/service/server_vlessenc_test.go new file mode 100644 index 00000000..2e8b8572 --- /dev/null +++ b/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"]) + } +}