fix(converter): fill servername and fingerprint for CDN VLESS/WS
Client YAML for domain+WS on 443 was missing TLS SNI/servername and client-fingerprint, which breaks Cloudflare edge handshake.
This commit is contained in:
1 parent
d2a80f5f80
commit
b3170423ba
2 files changed
+122
No files matched your search
@@ -281,6 +281,8 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred
|
||||
copyOption(p, opts, "alterId")
|
||||
}
|
||||
applyClientWrappers(p, opts)
|
||||
ensureClientAccessTLS(p, l, server)
|
||||
applyClientSkipCertVerify(p, opts, server, l.ID)
|
||||
result = append(result, p)
|
||||
}
|
||||
case "trojan":
|
||||
@@ -303,6 +305,8 @@ func listenerToProxies(l models.Listener, server string, credentials []user.Cred
|
||||
if value, ok := opts["ss-option"]; ok {
|
||||
p["ss-opts"] = value
|
||||
}
|
||||
ensureClientAccessTLS(p, l, server)
|
||||
applyClientSkipCertVerify(p, opts, server, l.ID)
|
||||
result = append(result, p)
|
||||
}
|
||||
case "hysteria2":
|
||||
@@ -852,6 +856,81 @@ func normalizeTUICToken(token interface{}) interface{} {
|
||||
|
||||
// applyClientSkipCertVerify sets skip-cert-verify on a client proxy map using
|
||||
// smart cert detection (panel self-signed, host match, explicit override).
|
||||
|
||||
// ensureClientAccessTLS fills TLS hostname / fingerprint for CDN and domain
|
||||
// access profiles. Listener config often omits sni (panel-only / stripped),
|
||||
// while clients still need servername when connecting via Cloudflare or a
|
||||
// public domain on 443.
|
||||
func ensureClientAccessTLS(p map[string]interface{}, l models.Listener, server string) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
accessSNI := strings.TrimSpace(l.AccessSNI)
|
||||
publicHost := netutil.NormalizeHost(l.PublicHost)
|
||||
hint := accessSNI
|
||||
if hint == "" {
|
||||
hint = publicHost
|
||||
}
|
||||
if hint == "" {
|
||||
hint = strings.TrimSpace(server)
|
||||
}
|
||||
publicPort := strings.TrimSpace(l.PublicPort)
|
||||
if publicPort == "" {
|
||||
publicPort = strings.TrimSpace(l.Port)
|
||||
}
|
||||
network, _ := p["network"].(string)
|
||||
domainHint := hint != "" && net.ParseIP(hint) == nil
|
||||
// WS + public domain (+ common CDN ports) ⇒ client talks TLS to the edge.
|
||||
if network == "ws" && domainHint && (publicPort == "443" || publicPort == "8443" || publicPort == "2053" || publicPort == "2083" || publicPort == "2087" || publicPort == "2096") {
|
||||
p["tls"] = true
|
||||
}
|
||||
tlsOn := false
|
||||
switch v := p["tls"].(type) {
|
||||
case bool:
|
||||
tlsOn = v
|
||||
case string:
|
||||
tlsOn = strings.EqualFold(v, "true")
|
||||
}
|
||||
if !tlsOn {
|
||||
return
|
||||
}
|
||||
if p["servername"] == nil && p["sni"] == nil && hint != "" {
|
||||
p["servername"] = hint
|
||||
p["sni"] = hint
|
||||
} else if p["servername"] == nil {
|
||||
if s, ok := p["sni"].(string); ok && strings.TrimSpace(s) != "" {
|
||||
p["servername"] = strings.TrimSpace(s)
|
||||
}
|
||||
} else if p["sni"] == nil {
|
||||
if s, ok := p["servername"].(string); ok && strings.TrimSpace(s) != "" {
|
||||
p["sni"] = strings.TrimSpace(s)
|
||||
}
|
||||
}
|
||||
if p["client-fingerprint"] == nil {
|
||||
if fp, ok := p["fingerprint"].(string); ok && strings.TrimSpace(fp) != "" {
|
||||
p["client-fingerprint"] = strings.TrimSpace(fp)
|
||||
} else {
|
||||
p["client-fingerprint"] = "chrome"
|
||||
}
|
||||
}
|
||||
// WS Host header for CDN when still empty.
|
||||
if network == "ws" {
|
||||
ws, _ := p["ws-opts"].(map[string]interface{})
|
||||
if ws == nil {
|
||||
ws = map[string]interface{}{"path": "/"}
|
||||
p["ws-opts"] = ws
|
||||
}
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
if headers == nil {
|
||||
headers = map[string]interface{}{}
|
||||
}
|
||||
if h, _ := headers["Host"].(string); strings.TrimSpace(h) == "" && hint != "" {
|
||||
headers["Host"] = hint
|
||||
ws["headers"] = headers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyClientSkipCertVerify(p, opts map[string]interface{}, connectHost string, listenerID uint) {
|
||||
if p == nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kazeyukiro/3m-ui/backend/internal/database/models"
|
||||
"github.com/kazeyukiro/3m-ui/backend/internal/user"
|
||||
)
|
||||
|
||||
func TestListenerToProxiesCDNWSFillsServername(t *testing.T) {
|
||||
l := models.Listener{
|
||||
Name: "VLESS-CF",
|
||||
Protocol: "vless",
|
||||
Port: "2053",
|
||||
PublicHost: "dzx1283.cc.cd",
|
||||
PublicPort: "443",
|
||||
AccessSNI: "dzx1283.cc.cd",
|
||||
Enabled: true,
|
||||
Config: `{"ws-path":"/ws","allow-insecure":true}`,
|
||||
}
|
||||
proxies, err := listenerToProxies(l, "dzx1283.cc.cd", []user.Credential{{UUID: "f3b71a1a-ff1e-45a4-9de9-c672aa347f62", Username: "admin"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(proxies) != 1 {
|
||||
t.Fatalf("len=%d", len(proxies))
|
||||
}
|
||||
p := proxies[0]
|
||||
if p["tls"] != true {
|
||||
t.Fatalf("tls: %#v", p["tls"])
|
||||
}
|
||||
if p["servername"] != "dzx1283.cc.cd" {
|
||||
t.Fatalf("servername: %#v", p["servername"])
|
||||
}
|
||||
if p["client-fingerprint"] != "chrome" {
|
||||
t.Fatalf("fingerprint: %#v", p["client-fingerprint"])
|
||||
}
|
||||
ws, _ := p["ws-opts"].(map[string]interface{})
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
if headers["Host"] != "dzx1283.cc.cd" {
|
||||
t.Fatalf("Host: %#v", headers)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user