mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-18 12:05:53 +00:00
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 e6760ae396.
* feat(vless): clarify VLESS encryption auth selection and enhance parsing logic
This commit is contained in:
parent
d86e87ed30
commit
fdaa65ad7e
4 changed files with 151 additions and 17 deletions
|
|
@ -325,7 +325,7 @@ export const sections = [
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/panel/api/server/getNewVlessEnc',
|
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',
|
method: 'POST',
|
||||||
|
|
|
||||||
|
|
@ -393,16 +393,29 @@ async function fetchDefaultCertSettings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// === VLESS encryption helpers =======================================
|
// === VLESS encryption helpers =======================================
|
||||||
// `xray vlessenc` returns both X25519 and ML-KEM-768 variants every
|
// `xray vlessenc` returns both X25519 and ML-KEM-768 auth variants every
|
||||||
// call; the user clicks one of two buttons to pick which block goes
|
// call; the user clicks one button to pick which block goes into
|
||||||
// into decryption/encryption.
|
// decryption/encryption. Both generated strings share the same hybrid
|
||||||
async function getNewVlessEnc(authLabel) {
|
// mlkem768x25519plus prefix; the auth choice is the final key block.
|
||||||
if (!authLabel || !inbound.value?.settings) return;
|
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;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
|
const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
|
||||||
if (!msg?.success) return;
|
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;
|
if (!block) return;
|
||||||
inbound.value.settings.decryption = block.decryption;
|
inbound.value.settings.decryption = block.decryption;
|
||||||
inbound.value.settings.encryption = block.encryption;
|
inbound.value.settings.encryption = block.encryption;
|
||||||
|
|
@ -417,6 +430,17 @@ function clearVlessEnc() {
|
||||||
inbound.value.settings.encryption = 'none';
|
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 =========================
|
// === SS method change tracks legacy semantics =========================
|
||||||
function onSSMethodChange() {
|
function onSSMethodChange() {
|
||||||
inbound.value.settings.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
|
inbound.value.settings.password = RandomUtil.randomShadowsocksPassword(inbound.value.settings.method);
|
||||||
|
|
@ -731,14 +755,17 @@ watch(
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label=" ">
|
<a-form-item label=" ">
|
||||||
<a-space :size="8" wrap>
|
<a-space :size="8" wrap>
|
||||||
<a-button type="primary" :loading="saving" @click="getNewVlessEnc('X25519, not Post-Quantum')">
|
<a-button type="primary" :loading="saving" @click="getNewVlessEnc('x25519')">
|
||||||
X25519
|
X25519 auth
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="primary" :loading="saving" @click="getNewVlessEnc('ML-KEM-768, Post-Quantum')">
|
<a-button type="primary" :loading="saving" @click="getNewVlessEnc('mlkem768')">
|
||||||
ML-KEM-768
|
ML-KEM-768 auth
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button danger @click="clearVlessEnc">Clear</a-button>
|
<a-button danger @click="clearVlessEnc">Clear</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
|
<a-typography-text type="secondary" class="vless-auth-state">
|
||||||
|
Selected: {{ selectedVlessAuth }}
|
||||||
|
</a-typography-text>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
|
|
@ -1741,6 +1768,11 @@ watch(
|
||||||
color: #ff4d4f;
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vless-auth-state {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.json-editor {
|
.json-editor {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
|
||||||
|
|
@ -1275,7 +1275,13 @@ func (s *ServerService) GetNewVlessEnc() (any, error) {
|
||||||
return nil, err
|
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 auths []map[string]string
|
||||||
var current map[string]string
|
var current map[string]string
|
||||||
|
|
||||||
|
|
@ -1285,14 +1291,18 @@ func (s *ServerService) GetNewVlessEnc() (any, error) {
|
||||||
if current != nil {
|
if current != nil {
|
||||||
auths = append(auths, current)
|
auths = append(auths, current)
|
||||||
}
|
}
|
||||||
|
label := strings.TrimSpace(strings.TrimPrefix(line, "Authentication:"))
|
||||||
current = map[string]string{
|
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"`) {
|
} else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
|
||||||
parts := strings.SplitN(line, ":", 2)
|
parts := strings.SplitN(line, ":", 2)
|
||||||
if len(parts) == 2 && current != nil {
|
if len(parts) == 2 && current != nil {
|
||||||
key := strings.Trim(parts[0], `" `)
|
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
|
current[key] = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1302,9 +1312,19 @@ func (s *ServerService) GetNewVlessEnc() (any, error) {
|
||||||
auths = append(auths, current)
|
auths = append(auths, current)
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[string]any{
|
return auths
|
||||||
"auths": auths,
|
}
|
||||||
}, nil
|
|
||||||
|
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) {
|
func (s *ServerService) GetNewUUID() (map[string]string, error) {
|
||||||
|
|
|
||||||
82
web/service/server_vlessenc_test.go
Normal file
82
web/service/server_vlessenc_test.go
Normal file
|
|
@ -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"])
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue