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:
kazeyukiro committed 2026-09-21 23:55:00 +08:00
1 parent 8c5e21adc4
commit 446caa37d4
3 files changed
+127 -29

No files matched your search

+63
View File
@@ -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)
}
}
+29 -23
View File
@@ -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
+35 -6
View File
@@ -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/ ---