From 8f9ffb1a08dbedd060da1041cdb67ce1bdff79f2 Mon Sep 17 00:00:00 2001 From: Z User Date: Mon, 31 Aug 2026 05:57:21 +0000 Subject: [PATCH] fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. fix(listener): strip incomplete jls-config before Mihomo validate Empty or half-filled jls-config (missing dest/users) causes mihomo -t to fail. Require complete JLS on the form; sanitize on save and at config generation. --- backend/internal/listener/autofill.go | 38 ++++++++ backend/internal/mihomo/config/generator.go | 2 + .../mihomo/config/wrapper_sanitize.go | 90 +++++++++++++++++++ .../src/components/ListenerConfigFields.tsx | 25 +++--- 4 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 backend/internal/mihomo/config/wrapper_sanitize.go diff --git a/backend/internal/listener/autofill.go b/backend/internal/listener/autofill.go index 8a7da1c..8a84c64 100644 --- a/backend/internal/listener/autofill.go +++ b/backend/internal/listener/autofill.go @@ -119,6 +119,44 @@ func sanitizeServerConfig(cfg map[string]interface{}) { delete(raw, "public_key") cfg["reality-config"] = raw } + // Incomplete JLS blocks make Mihomo reject the whole config: + // "jls-config has unset fields: dest, users". + if jls, ok := cfg["jls-config"].(map[string]interface{}); ok { + dest, _ := jls["dest"].(string) + if strings.TrimSpace(dest) == "" || !jlsHasUsers(jls) { + delete(cfg, "jls-config") + } + } +} + +func jlsHasUsers(m map[string]interface{}) bool { + raw, ok := m["users"] + if !ok || raw == nil { + return false + } + switch users := raw.(type) { + case []interface{}: + for _, item := range users { + u, ok := item.(map[string]interface{}) + if !ok { + continue + } + user, _ := u["username"].(string) + pass, _ := u["password"].(string) + if strings.TrimSpace(user) != "" && strings.TrimSpace(pass) != "" { + return true + } + } + case []map[string]interface{}: + for _, u := range users { + user, _ := u["username"].(string) + pass, _ := u["password"].(string) + if strings.TrimSpace(user) != "" && strings.TrimSpace(pass) != "" { + return true + } + } + } + return false } func needsReality(cfg map[string]interface{}) bool { diff --git a/backend/internal/mihomo/config/generator.go b/backend/internal/mihomo/config/generator.go index f19cc42..d9d054e 100644 --- a/backend/internal/mihomo/config/generator.go +++ b/backend/internal/mihomo/config/generator.go @@ -201,6 +201,8 @@ func generateListeners(listeners []models.Listener, creds map[uint][]Credential) if err != nil { return nil, fmt.Errorf("listener %q: %w", l.Name, err) } + // Drop half-filled wrappers first so TLS cert ensure is not skipped. + sanitizeIncompleteTLSWrappers(configMap) if err := ensureListenerTLSMaterial(protocolName, configMap); err != nil { return nil, fmt.Errorf("listener %q: %w", l.Name, err) } diff --git a/backend/internal/mihomo/config/wrapper_sanitize.go b/backend/internal/mihomo/config/wrapper_sanitize.go new file mode 100644 index 0000000..44647c7 --- /dev/null +++ b/backend/internal/mihomo/config/wrapper_sanitize.go @@ -0,0 +1,90 @@ +package config + +import "strings" + +// sanitizeIncompleteTLSWrappers drops TLS-wrapper blocks that Mihomo would +// reject for missing required nested fields (e.g. jls-config without dest/users). +// Incomplete blocks often come from panel toggles that were saved without filling +// the required sub-fields. +func sanitizeIncompleteTLSWrappers(cfg map[string]interface{}) { + if cfg == nil { + return + } + if jls, ok := cfg["jls-config"].(map[string]interface{}); ok { + if !wrapperEnabled(jls) || !hasNonEmptyString(jls, "dest") || !hasJLSUsers(jls) { + delete(cfg, "jls-config") + } + } + if st, ok := cfg["shadow-tls"].(map[string]interface{}); ok { + // Minimal: if explicitly disabled or empty object, drop. + if !wrapperEnabled(st) { + delete(cfg, "shadow-tls") + } + } + if rt, ok := cfg["res-tls"].(map[string]interface{}); ok { + if !wrapperEnabled(rt) || !hasNonEmptyString(rt, "dest") { + delete(cfg, "res-tls") + } + } + // allow-insecure alone without cert is valid for anytls only when intentional; + // do not strip it here. +} + +func wrapperEnabled(m map[string]interface{}) bool { + if m == nil || len(m) == 0 { + return false + } + if en, ok := m["enable"].(bool); ok { + return en + } + if en, ok := m["enabled"].(bool); ok { + return en + } + // Present without enable flag: treat as enabled only if it has meaningful keys. + for k := range m { + if k == "enable" || k == "enabled" { + continue + } + return true + } + return false +} + +func hasNonEmptyString(m map[string]interface{}, key string) bool { + v, ok := m[key] + if !ok || v == nil { + return false + } + s, ok := v.(string) + return ok && strings.TrimSpace(s) != "" +} + +func hasJLSUsers(m map[string]interface{}) bool { + raw, ok := m["users"] + if !ok || raw == nil { + return false + } + switch users := raw.(type) { + case []interface{}: + for _, item := range users { + u, ok := item.(map[string]interface{}) + if !ok { + continue + } + user, _ := u["username"].(string) + pass, _ := u["password"].(string) + if strings.TrimSpace(user) != "" && strings.TrimSpace(pass) != "" { + return true + } + } + case []map[string]interface{}: + for _, u := range users { + user, _ := u["username"].(string) + pass, _ := u["password"].(string) + if strings.TrimSpace(user) != "" && strings.TrimSpace(pass) != "" { + return true + } + } + } + return false +} diff --git a/frontend/src/components/ListenerConfigFields.tsx b/frontend/src/components/ListenerConfigFields.tsx index 87acf2e..0e4385d 100644 --- a/frontend/src/components/ListenerConfigFields.tsx +++ b/frontend/src/components/ListenerConfigFields.tsx @@ -574,21 +574,24 @@ export function formValuesToConfig( }) || { enable: true }; } - // jls-config + // jls-config — Mihomo requires dest + users; never emit a half-filled block. if (WRAPPER_TLS_PROTOCOLS.has(protocol) && values.jls_enabled) { const users = asArray(values.jls_users) - .filter((u: any) => u?.username || u?.password) + .filter((u: any) => u?.username && u?.password) .map((u: any) => cleanObj({ username: u.username, password: u.password })) .filter(Boolean); - cfg['jls-config'] = cleanObj({ - enable: true, - dest: values.jls_dest, - sni: values.jls_sni, - alpn: values.jls_alpn, - proxy: values.jls_proxy, - 'rate-limit': values.jls_rate_limit, - users: users.length ? users : undefined, - }) || { enable: true }; + const dest = typeof values.jls_dest === 'string' ? values.jls_dest.trim() : ''; + if (dest && users.length > 0) { + cfg['jls-config'] = cleanObj({ + enable: true, + dest, + sni: values.jls_sni, + alpn: values.jls_alpn, + proxy: values.jls_proxy, + 'rate-limit': values.jls_rate_limit, + users, + }); + } } // mux-option (listener-side: only padding + brutal per official docs)