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.
This commit is contained in:
Z User committed 2026-08-31 05:57:21 +00:00
1 parent 5b8e42dfc1
commit 8f9ffb1a08
4 files changed
+144 -11

No files matched your search

+38
View File
@@ -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 {
@@ -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)
}
@@ -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
}
@@ -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)