fix(share): AnyTLS URI per official anytls-go scheme
anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only) fix(share): AnyTLS URI per official anytls-go scheme anytls://[password@]host[:port]/?sni=...&insecure=1#name - password as URI userinfo (percent-encoded) - only official query keys: sni, insecure - fragment for display name - subscription ExportShare now emits URI (was YAML-only)
This commit is contained in:
1 parent
8c5e21adc4
commit
446caa37d4
3 files changed
+127
-29
No files matched your search
@@ -0,0 +1,63 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAnyTLSURIMatchesOfficialScheme(t *testing.T) {
|
||||
cfg := map[string]interface{}{
|
||||
"users": map[string]interface{}{
|
||||
"alice": "letmein",
|
||||
},
|
||||
"sni": "real.example.com",
|
||||
"skip-cert-verify": true,
|
||||
}
|
||||
uris, err := anytlsURIs("My Node", "example.com", "443", cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(uris) != 1 {
|
||||
t.Fatalf("want 1 uri, got %#v", uris)
|
||||
}
|
||||
u := uris[0]
|
||||
if !strings.HasPrefix(u, "anytls://letmein@example.com:443?") && !strings.HasPrefix(u, "anytls://letmein@example.com:443#") {
|
||||
// password may be encoded; still must be anytls scheme with @host
|
||||
if !strings.Contains(u, "anytls://") || !strings.Contains(u, "@example.com:443") {
|
||||
t.Fatalf("unexpected uri: %s", u)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(u, "sni=real.example.com") {
|
||||
t.Fatalf("missing sni: %s", u)
|
||||
}
|
||||
if !strings.Contains(u, "insecure=1") {
|
||||
t.Fatalf("missing insecure=1: %s", u)
|
||||
}
|
||||
if strings.Contains(u, "allowInsecure") || strings.Contains(u, "fp=") {
|
||||
t.Fatalf("non-official query keys should not appear: %s", u)
|
||||
}
|
||||
if !strings.Contains(u, "#") {
|
||||
t.Fatalf("missing fragment name: %s", u)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnyTLSURIPasswordEncoding(t *testing.T) {
|
||||
cfg := map[string]interface{}{
|
||||
"users": map[string]interface{}{
|
||||
"u": "p@ss:word/x",
|
||||
},
|
||||
"sni": "example.com",
|
||||
}
|
||||
uris, err := anytlsURIs("n", "1.2.3.4", "8443", cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
u := uris[0]
|
||||
// Special chars in password must be percent-encoded in userinfo.
|
||||
if strings.Contains(u, "p@ss:word/x@") {
|
||||
t.Fatalf("password not encoded: %s", u)
|
||||
}
|
||||
if !strings.Contains(u, "@1.2.3.4:8443") {
|
||||
t.Fatalf("host missing: %s", u)
|
||||
}
|
||||
}
|
||||
@@ -411,43 +411,49 @@ func tuicURIs(name, host, port string, cfg map[string]interface{}) ([]string, er
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// anytlsURIs builds share links per official AnyTLS URI scheme:
|
||||
//
|
||||
// anytls://[password@]hostname[:port]/?sni=...&insecure=1#name
|
||||
//
|
||||
// https://github.com/anytls/anytls-go/blob/main/docs/uri_scheme.md
|
||||
func anytlsURIs(name, host, port string, cfg map[string]interface{}) ([]string, error) {
|
||||
users := userMap(cfg)
|
||||
if len(users) == 0 {
|
||||
return nil, fmt.Errorf("anytls listener requires at least one user for URI export")
|
||||
}
|
||||
sni := ""
|
||||
if v, ok := cfg["sni"].(string); ok && strings.TrimSpace(v) != "" {
|
||||
sni = strings.TrimSpace(v)
|
||||
} else if v, ok := cfg["servername"].(string); ok && strings.TrimSpace(v) != "" {
|
||||
sni = strings.TrimSpace(v)
|
||||
}
|
||||
insecure := clientSkipCert(cfg, host)
|
||||
result := make([]string, 0, len(users))
|
||||
for username, raw := range users {
|
||||
password, ok := raw.(string)
|
||||
if !ok || password == "" {
|
||||
if !ok || strings.TrimSpace(password) == "" {
|
||||
return nil, fmt.Errorf("anytls user %q has empty password", username)
|
||||
}
|
||||
params := map[string]string{}
|
||||
if v, ok := cfg["sni"].(string); ok && v != "" {
|
||||
params["sni"] = v
|
||||
}
|
||||
if v, ok := cfg["client-fingerprint"].(string); ok && v != "" {
|
||||
params["fp"] = v
|
||||
}
|
||||
if clientSkipCert(cfg, host) {
|
||||
params["insecure"] = "1"
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
if v, ok := cfg["idle-session-check-interval"].(string); ok && v != "" {
|
||||
params["idle_session_check_interval"] = v
|
||||
}
|
||||
if v, ok := cfg["idle-session-timeout"].(string); ok && v != "" {
|
||||
params["idle_session_timeout"] = v
|
||||
}
|
||||
if v, ok := cfg["min-idle-session"].(string); ok && v != "" {
|
||||
params["min_idle_session"] = v
|
||||
}
|
||||
_ = username
|
||||
result = append(result, addName(query("anytls://"+url.PathEscape(password)+"@"+netutil.JoinHostPort(host, port), params), name))
|
||||
result = append(result, buildAnyTLSURI(strings.TrimSpace(password), host, port, sni, name, insecure))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// buildAnyTLSURI encodes password as URI userinfo and only official query keys.
|
||||
func buildAnyTLSURI(password, host, port, sni, name string, insecure bool) string {
|
||||
// Auth is the password in the standard URI username position (percent-encoded).
|
||||
userinfo := url.User(password).String()
|
||||
base := "anytls://" + userinfo + "@" + netutil.JoinHostPort(host, port)
|
||||
params := map[string]string{}
|
||||
if strings.TrimSpace(sni) != "" {
|
||||
params["sni"] = strings.TrimSpace(sni)
|
||||
}
|
||||
if insecure {
|
||||
params["insecure"] = "1"
|
||||
}
|
||||
return addName(query(base, params), name)
|
||||
}
|
||||
|
||||
func realityPublicKey(cfg map[string]interface{}) (string, error) {
|
||||
if public, ok := cfg["public-key"].(string); ok && strings.TrimSpace(public) != "" {
|
||||
return public, nil
|
||||
|
||||
@@ -448,6 +448,8 @@ func (g GenericCompiler) BuildShare(in ShareInput) (Share, error) {
|
||||
// --- AnyTLS https://wiki.metacubex.one/config/proxies/anytls/ ---
|
||||
|
||||
func (AnyTLSCompiler) BuildShare(in ShareInput) (Share, error) {
|
||||
// URI: https://github.com/anytls/anytls-go/blob/main/docs/uri_scheme.md
|
||||
// anytls://[password@]host[:port]/?sni=...&insecure=0|1#name
|
||||
host, port, err := shareHostPort(in.Node, "")
|
||||
if err != nil {
|
||||
return Share{}, err
|
||||
@@ -463,17 +465,44 @@ func (AnyTLSCompiler) BuildShare(in ShareInput) (Share, error) {
|
||||
if pass == "" {
|
||||
return Share{}, fmt.Errorf("anytls share requires password")
|
||||
}
|
||||
extra := map[string]interface{}{"password": pass, "udp": true}
|
||||
sni := strings.TrimSpace(in.Node.AccessSNI)
|
||||
if sni == "" {
|
||||
sni = strFrom(cfg, "sni", "servername")
|
||||
}
|
||||
if sni == "" {
|
||||
sni = host
|
||||
// Official: omit port → 443. We always include explicit port when known.
|
||||
insecure := true
|
||||
if b, ok := cfg["skip-cert-verify"].(bool); ok {
|
||||
insecure = b
|
||||
} else if b, ok := cfg["allow-insecure"].(bool); ok {
|
||||
insecure = b
|
||||
}
|
||||
// Legitimate cert + domain connect host → prefer secure unless explicitly skipped.
|
||||
if !insecure {
|
||||
// keep false
|
||||
} else if cert, _ := cfg["certificate"].(string); strings.TrimSpace(cert) != "" {
|
||||
// panel may still want skip for self-signed; default true for share convenience
|
||||
insecure = true
|
||||
}
|
||||
|
||||
params := map[string]string{}
|
||||
if sni != "" {
|
||||
params["sni"] = sni
|
||||
}
|
||||
if insecure {
|
||||
params["insecure"] = "1"
|
||||
}
|
||||
userinfo := url.User(pass).String()
|
||||
uri := shareName(
|
||||
shareQuery("anytls://"+userinfo+"@"+netutil.JoinHostPort(host, port), params),
|
||||
in.Node.Name,
|
||||
)
|
||||
|
||||
extra := map[string]interface{}{"password": pass, "udp": true}
|
||||
if sni != "" {
|
||||
extra["sni"] = sni
|
||||
}
|
||||
extra["sni"] = sni
|
||||
extra["client-fingerprint"] = strOr(strings.TrimSpace(in.Node.Fingerprint), "chrome")
|
||||
extra["skip-cert-verify"] = true
|
||||
extra["skip-cert-verify"] = insecure
|
||||
if alpn := stringListFrom(cfg, "alpn"); len(alpn) > 0 {
|
||||
extra["alpn"] = alpn
|
||||
}
|
||||
@@ -481,7 +510,7 @@ func (AnyTLSCompiler) BuildShare(in ShareInput) (Share, error) {
|
||||
if err != nil {
|
||||
return Share{}, err
|
||||
}
|
||||
return Share{ClientYAML: yamlOut}, nil
|
||||
return Share{URI: uri, QRContent: uri, ClientYAML: yamlOut}, nil
|
||||
}
|
||||
|
||||
// --- ShadowQUIC https://wiki.metacubex.one/config/proxies/shadowquic/ ---
|
||||
|
||||
Reference in new issue
Block a user