@@ -1426,7 +1426,7 @@ func fetchRaw(rawURL string, timeout time.Duration) fetchResult {
14261426type failDetail struct {
14271427 mu sync.Mutex
14281428 reasons map [string ]int
1429- samples map [string ][]string // up to 100 sample config URIs per reason
1429+ samples map [string ][]string
14301430}
14311431
14321432func validateAll (lines []string ) []configResult {
@@ -1665,14 +1665,11 @@ func classifyFailReason(reason string) string {
16651665 return "PARSE › url parse error"
16661666 case strings .HasPrefix (r , "PARSE: unsupported cipher:" ):
16671667 return "PARSE › unsupported SS cipher"
1668+ case strings .HasPrefix (r , "PARSE: unsupported transport: xhttp" ),
1669+ strings .HasPrefix (r , "PARSE: unsupported transport: splithttp" ):
1670+ return "PARSE › unsupported transport (xhttp/splithttp)"
16681671 case strings .HasPrefix (r , "PARSE: unsupported transport:" ):
1669- msg := strings .TrimPrefix (r , "PARSE: unsupported transport: " )
1670- switch msg {
1671- case "xhttp" , "splithttp" :
1672- return "PARSE › unsupported transport (xhttp/splithttp)"
1673- default :
1674- return "PARSE › unsupported transport (kcp/quic/mkcp)"
1675- }
1672+ return "PARSE › unsupported transport (kcp/quic/mkcp)"
16761673 case r == "PARSE: missing @" || r == "PARSE: missing server" ||
16771674 r == "PARSE: missing uuid" || r == "PARSE: missing password" ||
16781675 r == "PARSE: missing port" || r == "PARSE: missing auth" :
@@ -2146,10 +2143,10 @@ func toSingBoxOutbound(configURL, protocol string) (string, string) {
21462143}
21472144
21482145func sanitizeProxyURL (raw string ) string {
2149- // ── HTML entity decode ──────────────────────────────────────────────
2150- // Config sources ( Telegram web, HTML pages) often HTML-encode ampersands:
2151- // ?security=reality&pbk=KEY → url.Parse reads "amp;pbk" not "pbk"
2152- // so q.Get("pbk") returns "" → "reality missing public key" error on ~420 configs.
2146+ // ── HTML entity decode ────────────────────────────────────────────────────
2147+ // Sources like Telegram web / HTML pages HTML-encode ampersands:
2148+ // ?security=reality&pbk=KEY → url.Parse reads key as "amp;pbk" not "pbk"
2149+ // so q.Get("pbk") returns "" → "reality missing public key" for ~420 configs.
21532150 raw = strings .ReplaceAll (raw , "&" , "&" )
21542151 raw = strings .ReplaceAll (raw , "<" , "<" )
21552152 raw = strings .ReplaceAll (raw , ">" , ">" )
@@ -2187,15 +2184,13 @@ func sanitizeProxyURL(raw string) string {
21872184}
21882185
21892186func normalizeUUID (u string ) string {
2190- // Standard UUID: 8-4-4-4-12 hex chars with dashes
2191- // Some configs provide a UUID as 32 hex chars without dashes.
2192- // sing-box requires the dashed format.
2187+ // Some configs provide UUID as 32 hex chars without dashes.
2188+ // sing-box requires the standard 8-4-4-4-12 dashed format.
21932189 if len (u ) == 32 {
21942190 allHex := true
21952191 for _ , c := range u {
21962192 if ! ((c >= '0' && c <= '9' ) || (c >= 'a' && c <= 'f' ) || (c >= 'A' && c <= 'F' )) {
2197- allHex = false
2198- break
2193+ allHex = false ; break
21992194 }
22002195 }
22012196 if allHex {
@@ -2205,6 +2200,28 @@ func normalizeUUID(u string) string {
22052200 return u
22062201}
22072202
2203+ func sanitizeSNI (sni , fallback string ) string {
2204+ // SNI must be a valid ASCII hostname — no emoji, spaces, wildcards in SNI extension.
2205+ // Wildcard like *.domain.com: strip the "*." prefix → domain.com
2206+ // (TLS RFC 6066 does not permit wildcards in SNI)
2207+ if strings .HasPrefix (sni , "*." ) {
2208+ sni = sni [2 :]
2209+ }
2210+ if sni == "" {
2211+ return fallback
2212+ }
2213+ for _ , c := range sni {
2214+ if c > 127 || (c != '.' && c != '-' && ! (c >= 'a' && c <= 'z' ) &&
2215+ ! (c >= 'A' && c <= 'Z' ) && ! (c >= '0' && c <= '9' )) {
2216+ return fallback
2217+ }
2218+ }
2219+ if len (sni ) > 0 && (sni [0 ] == '.' || sni [0 ] == '-' || sni [len (sni )- 1 ] == '.' || sni [len (sni )- 1 ] == '-' ) {
2220+ return fallback
2221+ }
2222+ return sni
2223+ }
2224+
22082225func encodeUserInfo (s string ) string {
22092226 var buf strings.Builder
22102227 for i := 0 ; i < len (s ); i ++ {
@@ -2252,7 +2269,7 @@ func parseVMessURItoD(data string) (map[string]interface{}, string) {
22522269 "scy" : first (q .Get ("encryption" ), q .Get ("scy" ), "auto" ),
22532270 "net" : first (q .Get ("type" ), q .Get ("net" ), "tcp" ),
22542271 "tls" : tlsVal ,
2255- "sni" : first (q .Get ("sni" ), q .Get ("peer" ), host ),
2272+ "sni" : sanitizeSNI ( first (q .Get ("sni" ), q .Get ("peer" ), " " ), host ),
22562273 "path" : q .Get ("path" ),
22572274 "host" : q .Get ("host" ),
22582275 "serviceName" : q .Get ("serviceName" ),
@@ -2424,20 +2441,21 @@ func parseVLess(raw string) (string, string) {
24242441 return "" , "port: " + err .Error ()
24252442 }
24262443 q := u .Query ()
2427- // TrimSpace: handles configs where the security value has trailing whitespace
2444+ // TrimSpace: handles configs with trailing whitespace in security value
24282445 security := strings .TrimSpace (strings .ToLower (q .Get ("security" )))
24292446 network := strings .ToLower (q .Get ("type" ))
24302447 if network == "" {
24312448 network = "tcp"
24322449 }
24332450 // Reject transports not supported by installed sing-box:
2434- // xhttp/splithttp causes ~5000 START failures → filter at PARSE
2435- // kcp/mkcp/quic are Xray-only → never supported by sing-box
2451+ // xhttp/splithttp → ~5000 START failures (rejected by sing-box config parser)
2452+ // kcp/mkcp/quic → Xray-only, never supported by sing-box
24362453 switch network {
24372454 case "xhttp" , "splithttp" , "kcp" , "mkcp" , "quic" :
24382455 return "" , "unsupported transport: " + network
24392456 }
2440- sni := first (q .Get ("sni" ), q .Get ("peer" ), server )
2457+ // sanitizeSNI: fixes wildcard SNI (*.domain.com→domain.com) and strips emoji/invalid chars
2458+ sni := sanitizeSNI (first (q .Get ("sni" ), q .Get ("peer" ), "" ), server )
24412459 // Filter flow: sing-box only supports xtls-rprx-vision; others cause FATAL config errors
24422460 flow := q .Get ("flow" )
24432461 if ! singboxSupportedFlows [flow ] {
@@ -2449,8 +2467,14 @@ func parseVLess(raw string) (string, string) {
24492467 }
24502468 transport := buildTransportJSON (network , first (q .Get ("path" ), "/" ), q .Get ("host" ),
24512469 first (q .Get ("serviceName" ), q .Get ("path" )))
2452- return fmt .Sprintf (`{"type":"vless","tag":"proxy","server":%q,"server_port":%d,"uuid":%q%s%s}` ,
2453- server , port , uuid , tlsJSON , transport ), ""
2470+ // packetEncoding: pass through to sing-box (supports xudp/packetaddr for VLESS)
2471+ packetEnc := q .Get ("packetEncoding" )
2472+ packetJSON := ""
2473+ if packetEnc == "xudp" || packetEnc == "packetaddr" {
2474+ packetJSON = fmt .Sprintf (`,"packet_encoding":%q` , packetEnc )
2475+ }
2476+ return fmt .Sprintf (`{"type":"vless","tag":"proxy","server":%q,"server_port":%d,"uuid":%q%s%s%s}` ,
2477+ server , port , uuid , tlsJSON , transport , packetJSON ), ""
24542478}
24552479
24562480func vlessTLS (security , sni , flow string , q url.Values ) (string , string ) {
@@ -2461,7 +2485,7 @@ func vlessTLS(security, sni, flow string, q url.Values) (string, string) {
24612485 switch security {
24622486 case "tls" , "xtls" :
24632487 // Do NOT include flowJSON: sing-box only accepts xtls-rprx-vision flow
2464- // with reality TLS. Adding it to plain TLS causes START failures.
2488+ // with reality. Including it with plain TLS causes ~5000 START failures.
24652489 s := fmt .Sprintf (`,"tls":{"enabled":true,"insecure":true,"server_name":%q` , sni )
24662490 if fp := q .Get ("fp" ); fp != "" {
24672491 s += fmt .Sprintf (`,"utls":{"enabled":true,"fingerprint":%q}` , fp )
@@ -2536,7 +2560,6 @@ func parseTrojan(raw string) (string, string) {
25362560 return "" , "port: " + err .Error ()
25372561 }
25382562 q := u .Query ()
2539- sni := first (q .Get ("sni" ), q .Get ("peer" ), server )
25402563 tls := fmt .Sprintf (`,"tls":{"enabled":true,"insecure":true,"server_name":%q` , sni )
25412564 if fp := q .Get ("fp" ); fp != "" {
25422565 tls += fmt .Sprintf (`,"utls":{"enabled":true,"fingerprint":%q}` , fp )
@@ -2547,6 +2570,7 @@ func parseTrojan(raw string) (string, string) {
25472570 case "xhttp" , "splithttp" , "kcp" , "mkcp" , "quic" :
25482571 return "" , "unsupported transport: " + network
25492572 }
2573+ sni := sanitizeSNI (first (q .Get ("sni" ), q .Get ("peer" ), "" ), server )
25502574 transport := buildTransportJSON (network , first (q .Get ("path" ), "/" ), q .Get ("host" ),
25512575 first (q .Get ("serviceName" ), q .Get ("path" )))
25522576 return fmt .Sprintf (`{"type":"trojan","tag":"proxy","server":%q,"server_port":%d,"password":%q%s%s}` ,
@@ -2611,7 +2635,6 @@ func parseShadowsocks(raw string) (string, string) {
26112635 if ! fastPathOK {
26122636 atIdx := strings .LastIndex (trimmed , "@" )
26132637 if atIdx == - 1 {
2614- // Strip query string before b64 decode: ss://BASE64?plugin=obfs
26152638 b64Src := trimmed
26162639 if qi := strings .Index (b64Src , "?" ); qi != - 1 { b64Src = b64Src [:qi ] }
26172640 decoded , err := decodeBase64 ([]byte (b64Src ))
@@ -3976,18 +3999,14 @@ func extractErr(stderr string) string {
39763999}
39774000
39784001func extractErrVerbose (stderr string ) string {
3979- // Picks the most informative sing-box error line.
3980- // Extracts the "msg" field from JSON-format sing-box log lines.
39814002 var first , best string
39824003 priority := []string {"invalid" , "failed" , "decode" , "unsupported" , "error" }
39834004 for _ , line := range strings .Split (stderr , "\n " ) {
39844005 line = strings .TrimSpace (line )
39854006 if line == "" { continue }
39864007 lower := strings .ToLower (line )
39874008 if strings .Contains (lower , "warn" ) || strings .Contains (lower , "deprecated" ) { continue }
3988- if strings .Contains (lower , `"level":"info"` ) || strings .Contains (lower , `"level":"debug"` ) ||
3989- strings .Contains (lower , "level=info" ) || strings .Contains (lower , "level=debug" ) { continue }
3990- // Extract msg field from JSON log: {"level":"error","msg":"decode failed: ..."}
4009+ if strings .Contains (lower , `"level":"info"` ) || strings .Contains (lower , "level=info" ) { continue }
39914010 if idx := strings .Index (line , `"msg":"` ); idx != - 1 {
39924011 end := strings .Index (line [idx + 7 :], `"` )
39934012 if end != - 1 { line = line [idx + 7 : idx + 7 + end ]; lower = strings .ToLower (line ) }
@@ -3999,8 +4018,7 @@ func extractErrVerbose(stderr string) string {
39994018 }
40004019 }
40014020 }
4002- r := best
4003- if r == "" { r = first }
4021+ r := best ; if r == "" { r = first }
40044022 if len (r ) > 180 { r = r [:180 ] + "..." }
40054023 return r
40064024}
0 commit comments