fix(tuic): subscription token from Config; formal certs verify

- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
fix(tuic): subscription token from Config; formal certs verify
- BuildShare/Decode: TUIC v4 token only from listener Config, never panel password
- listenerToProxies: refuse tuic-v4 without Config token
- skip-cert-verify only for panel self-signed (O=3m-ui / ShouldSkipCertVerify),
not for operator formal certificates
This commit is contained in:
kazeyukiro committed 2026-09-16 16:05:00 +08:00
1 parent 5338c51414
commit 2cd92bd1e7
3 files changed
+60 -17

No files matched your search

+17 -9
View File
@@ -348,10 +348,9 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred
p["sni"] = sn
}
}
// Only panel self-signed (or explicit skip) — not operator-provided formal certs.
if certutil.ShouldSkipCertVerify(opts) {
p["skip-cert-verify"] = true
} else if cert, _ := opts["certificate"].(string); strings.TrimSpace(cert) != "" {
p["skip-cert-verify"] = true
}
if value, ok := opts["ech-opts"]; ok {
p["ech-opts"] = value
@@ -360,7 +359,7 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred
}
case "tuic", "tuic-v4", "tuic-v5":
// MetaCubeX proxies/tuic: type is always "tuic".
// V4 = token (string); V5 = uuid + password. Prefer Config token/users.
// V4 = token from Config only; V5 = uuid+password.
copyTUICOpts := func(p map[string]interface{}) {
for _, key := range []string{
"congestion-controller", "bbr-profile", "alpn", "max-udp-relay-packet-size",
@@ -376,17 +375,26 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred
p["name-cert-verify"] = v
}
ensureTUICClientDefaults(p, opts)
// Formal cert: keep SNI as public host. Self-signed: skip-verify already set.
if p["sni"] == nil && p["servername"] == nil && server != "" {
p["sni"] = server
}
}
if tuicTokenNonEmpty(opts["token"]) {
// Prefer Config token for any TUIC that still has token (v4 / dual).
if tuicTokenNonEmpty(opts["token"]) && protocol != "tuic-v5" {
p := makeProxy("")
p["token"] = normalizeTUICToken(opts["token"])
tok := normalizeTUICToken(opts["token"])
if s, ok := tok.(string); !ok || strings.TrimSpace(s) == "" {
return nil, fmt.Errorf("listener %q: empty TUIC token in config", l.Name)
}
p["token"] = tok
copyTUICOpts(p)
result = append(result, p)
break
}
if protocol == "tuic-v4" {
return nil, fmt.Errorf("listener %q: tuic-v4 requires non-empty token in listener config", l.Name)
}
// V5 users from credentials (already preferred from Config when present).
emitted := 0
for i, cred := range credentials {
@@ -830,16 +838,16 @@ func ensureTUICClientDefaults(p, opts map[string]interface{}) {
if p["congestion-controller"] == nil {
p["congestion-controller"] = "bbr"
}
// Panel mints self-signed PEMs (CN=localhost). Public host (domain/IP)
// will never match — clients must skip verify or the handshake fails.
// Panel self-signed only. Formal certs (Let's Encrypt etc.) must verify.
if certutil.ShouldSkipCertVerify(opts) {
p["skip-cert-verify"] = true
} else if cert, _ := opts["certificate"].(string); strings.TrimSpace(cert) != "" {
p["skip-cert-verify"] = true
}
if p["udp"] == nil {
p["udp"] = true
}
if p["udp-relay-mode"] == nil {
p["udp-relay-mode"] = "native"
}
}
func copyClientTLS(dst, src map[string]interface{}) {
+7 -3
View File
@@ -131,14 +131,18 @@ func DecodeNodeModel(l models.Listener, users []UserCred) (NodeModel, error) {
n.Users = []UserCred{{Password: key}}
}
}
// TUIC v4: token: [TOKEN] (wiki inbound). Export one share per token.
if len(n.Users) == 0 && (n.Protocol == "tuic-v4" || n.Protocol == "tuic") {
// TUIC v4: token lives only in Config. Panel user rows must not become the token.
if n.Protocol == "tuic-v4" || (n.Protocol == "tuic" && len(stringListFrom(cfg, "token")) > 0) {
var toks []UserCred
for _, tok := range stringListFrom(cfg, "token") {
tok = strings.TrimSpace(tok)
if tok != "" {
n.Users = append(n.Users, UserCred{Password: tok})
toks = append(toks, UserCred{Password: tok})
}
}
if len(toks) > 0 {
n.Users = toks
}
}
}
return n, nil
+36 -5
View File
@@ -624,9 +624,14 @@ func (t TUICCompiler) BuildShare(in ShareInput) (Share, error) {
if sni == "" {
sni = strings.TrimSpace(in.Node.PublicHost)
}
skipCert := true
skipCert := false
if v, ok := cfg["skip-cert-verify"].(bool); ok {
skipCert = v
} else if cert, _ := cfg["certificate"].(string); strings.Contains(cert, "3m-ui") {
// Panel GenerateSelfSigned uses O=3m-ui
skipCert = true
} else if cert, _ := cfg["certificate"].(string); strings.TrimSpace(cert) != "" && sni == "" {
skipCert = true
}
isV4 := kind == "tuic-v4"
@@ -672,14 +677,18 @@ func (t TUICCompiler) BuildShare(in ShareInput) (Share, error) {
var uri string
if isV4 {
token := strings.TrimSpace(in.User.Password)
// Token lives on the listener Config — never the panel user password.
token := firstNonEmptyTokenValue(cfg["token"])
if token == "" {
token = strings.TrimSpace(in.User.Username)
token = strings.TrimSpace(in.User.Password)
// Only accept User.Password when it came from config decode (no UUID).
if strings.TrimSpace(in.User.UUID) != "" || looksLikeUUID(in.User.Username) {
token = ""
}
}
if token == "" {
return Share{}, fmt.Errorf("tuic-v4 share requires token")
return Share{}, fmt.Errorf("tuic-v4 share requires token in listener config")
}
// Client URI: tuic://TOKEN@host:port?... (V4 has no uuid/password).
uri = shareName(
shareQuery("tuic://"+url.PathEscape(token)+"@"+netutil.JoinHostPort(host, port), params),
in.Node.Name,
@@ -847,3 +856,25 @@ func strOr(v, def string) string {
}
return def
}
func firstNonEmptyTokenValue(tok interface{}) string {
switch v := tok.(type) {
case string:
return strings.TrimSpace(v)
case []string:
for _, s := range v {
if s = strings.TrimSpace(s); s != "" {
return s
}
}
case []interface{}:
for _, item := range v {
if s, ok := item.(string); ok {
if s = strings.TrimSpace(s); s != "" {
return s
}
}
}
}
return ""
}