| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- package service
- import (
- "context"
- "crypto/tls"
- "crypto/x509"
- "fmt"
- "net"
- "slices"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/mhsanaei/3x-ui/v3/internal/util/common"
- "github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
- )
- const (
- realityScanTimeout = 10 * time.Second
- realityDiscoverTimeout = 4 * time.Second
- realityScanConcurrency = 32
- realityDiscoverMaxIPs = 256
- realityScanMaxTotal = 512
- )
- var defaultRealityScanCandidates = []string{
- "www.cloudflare.com:443",
- "www.microsoft.com:443",
- "www.amazon.com:443",
- "aws.amazon.com:443",
- "www.samsung.com:443",
- "www.nvidia.com:443",
- "www.amd.com:443",
- "www.intel.com:443",
- "www.sony.com:443",
- "dl.google.com:443",
- }
- type RealityScanResult struct {
- Target string `json:"target" example:"www.cloudflare.com:443"`
- Host string `json:"host" example:"www.cloudflare.com"`
- IP string `json:"ip" example:"104.16.124.96"`
- Port int `json:"port" example:"443"`
- Feasible bool `json:"feasible" example:"true"`
- TLS13 bool `json:"tls13" example:"true"`
- TLSVersion string `json:"tlsVersion" example:"1.3"`
- H2 bool `json:"h2" example:"true"`
- ALPN string `json:"alpn" example:"h2"`
- X25519 bool `json:"x25519" example:"true"`
- CurveID string `json:"curveID" example:"X25519"`
- CertValid bool `json:"certValid" example:"true"`
- CertSubject string `json:"certSubject" example:"cloudflare.com"`
- CertIssuer string `json:"certIssuer" example:"Google Trust Services"`
- NotAfter string `json:"notAfter" example:"2026-08-01T00:00:00Z"`
- ServerNames []string `json:"serverNames"`
- LatencyMs int `json:"latencyMs" example:"180"`
- Reason string `json:"reason" example:""`
- }
- type realityProbeTask struct {
- dialHost string
- port int
- sni string
- timeout time.Duration
- bulk bool
- }
- func tlsVersionName(v uint16) string {
- switch v {
- case tls.VersionTLS13:
- return "1.3"
- case tls.VersionTLS12:
- return "1.2"
- case tls.VersionTLS11:
- return "1.1"
- case tls.VersionTLS10:
- return "1.0"
- default:
- return "unknown"
- }
- }
- func realityCurveName(id tls.CurveID) string {
- switch id {
- case tls.X25519:
- return "X25519"
- case tls.X25519MLKEM768:
- return "X25519MLKEM768"
- case tls.CurveP256:
- return "P-256"
- case tls.CurveP384:
- return "P-384"
- case tls.CurveP521:
- return "P-521"
- case 0:
- return ""
- default:
- return fmt.Sprintf("0x%04x", uint16(id))
- }
- }
- func filterUsableSANs(dnsNames []string) []string {
- out := make([]string, 0, len(dnsNames))
- for _, n := range dnsNames {
- n = strings.TrimSpace(n)
- if n == "" || strings.HasPrefix(n, "*.") {
- continue
- }
- out = append(out, n)
- }
- return out
- }
- func firstUsableName(leaf *x509.Certificate) string {
- cn := strings.TrimSpace(leaf.Subject.CommonName)
- if cn != "" && !strings.HasPrefix(cn, "*.") {
- return cn
- }
- for _, n := range leaf.DNSNames {
- n = strings.TrimSpace(n)
- if n != "" && !strings.HasPrefix(n, "*.") {
- return n
- }
- }
- return ""
- }
- func splitRealityTarget(target string) (string, int, error) {
- target = strings.TrimSpace(target)
- if target == "" {
- return "", 0, common.NewError("target is required")
- }
- host, portStr := target, "443"
- if h, p, err := net.SplitHostPort(target); err == nil {
- host, portStr = h, p
- }
- host, err := netsafe.NormalizeHost(host)
- if err != nil {
- return "", 0, common.NewError("invalid target host: ", err)
- }
- port, err := strconv.Atoi(portStr)
- if err != nil || port < 1 || port > 65535 {
- return "", 0, common.NewError("invalid target port")
- }
- return host, port, nil
- }
- func incIP(ip net.IP) {
- for j := len(ip) - 1; j >= 0; j-- {
- ip[j]++
- if ip[j] > 0 {
- break
- }
- }
- }
- func enumerateCIDR(cidr string, max int) ([]string, error) {
- _, ipnet, err := net.ParseCIDR(strings.TrimSpace(cidr))
- if err != nil {
- return nil, err
- }
- ips := make([]string, 0, max)
- for ip := ipnet.IP.Mask(ipnet.Mask); ipnet.Contains(ip); incIP(ip) {
- ips = append(ips, ip.String())
- if len(ips) >= max {
- break
- }
- }
- return ips, nil
- }
- func (s *ServerService) probeRealityAddr(dialHost string, port int, sni string, timeout time.Duration) *RealityScanResult {
- addr := net.JoinHostPort(dialHost, strconv.Itoa(port))
- res := &RealityScanResult{Port: port}
- if net.ParseIP(dialHost) != nil {
- res.IP = dialHost
- }
- if sni != "" {
- res.Host = sni
- res.Target = net.JoinHostPort(sni, strconv.Itoa(port))
- } else {
- res.Host = dialHost
- res.Target = addr
- }
- ctx, cancel := context.WithTimeout(context.Background(), timeout)
- defer cancel()
- start := time.Now()
- conn, err := netsafe.SSRFGuardedDialContext(ctx, "tcp", addr)
- if err != nil {
- res.Reason = "connection failed: " + err.Error()
- return res
- }
- defer conn.Close()
- _ = conn.SetDeadline(time.Now().Add(timeout))
- cfg := &tls.Config{
- ServerName: sni,
- InsecureSkipVerify: true,
- NextProtos: []string{"h2", "http/1.1"},
- CurvePreferences: []tls.CurveID{tls.X25519, tls.X25519MLKEM768},
- MinVersion: tls.VersionTLS12,
- }
- tlsConn := tls.Client(conn, cfg)
- if err := tlsConn.HandshakeContext(ctx); err != nil {
- res.Reason = "TLS handshake failed: " + err.Error()
- return res
- }
- res.LatencyMs = int(time.Since(start).Milliseconds())
- st := tlsConn.ConnectionState()
- res.TLS13 = st.Version == tls.VersionTLS13
- res.TLSVersion = tlsVersionName(st.Version)
- res.ALPN = st.NegotiatedProtocol
- res.H2 = st.NegotiatedProtocol == "h2"
- res.CurveID = realityCurveName(st.CurveID)
- res.X25519 = st.CurveID == tls.X25519 || st.CurveID == tls.X25519MLKEM768
- verifyHost := sni
- if len(st.PeerCertificates) > 0 {
- leaf := st.PeerCertificates[0]
- res.CertSubject = leaf.Subject.CommonName
- if res.CertSubject == "" && len(leaf.DNSNames) > 0 {
- res.CertSubject = leaf.DNSNames[0]
- }
- if len(leaf.Issuer.Organization) > 0 {
- res.CertIssuer = leaf.Issuer.Organization[0]
- } else {
- res.CertIssuer = leaf.Issuer.CommonName
- }
- res.NotAfter = leaf.NotAfter.UTC().Format(time.RFC3339)
- res.ServerNames = filterUsableSANs(leaf.DNSNames)
- if sni == "" {
- if discovered := firstUsableName(leaf); discovered != "" {
- res.Host = discovered
- res.Target = net.JoinHostPort(discovered, strconv.Itoa(port))
- verifyHost = discovered
- }
- }
- if verifyHost != "" {
- opts := x509.VerifyOptions{DNSName: verifyHost, Intermediates: x509.NewCertPool()}
- for _, c := range st.PeerCertificates[1:] {
- opts.Intermediates.AddCert(c)
- }
- if _, verr := leaf.Verify(opts); verr == nil {
- res.CertValid = true
- } else {
- res.Reason = "certificate not trusted: " + verr.Error()
- }
- } else {
- res.Reason = "no usable domain in certificate"
- }
- } else {
- res.Reason = "no certificate presented"
- }
- res.Feasible = res.TLS13 && res.H2 && res.X25519 && res.CertValid
- if !res.Feasible && res.Reason == "" {
- switch {
- case !res.TLS13:
- res.Reason = "server does not negotiate TLS 1.3"
- case !res.H2:
- res.Reason = "server does not negotiate HTTP/2 (h2)"
- case !res.X25519:
- res.Reason = "server did not use X25519 key exchange"
- }
- }
- return res
- }
- func (s *ServerService) probeRealityTarget(host string, port int) *RealityScanResult {
- return s.probeRealityAddr(host, port, host, realityScanTimeout)
- }
- func (s *ServerService) ScanRealityTarget(target string) (*RealityScanResult, error) {
- host, port, err := splitRealityTarget(target)
- if err != nil {
- return nil, err
- }
- return s.probeRealityTarget(host, port), nil
- }
- func (s *ServerService) ScanRealityTargets(targetsCSV string) ([]*RealityScanResult, error) {
- var tokens []string
- for _, raw := range strings.Split(targetsCSV, ",") {
- if t := strings.TrimSpace(raw); t != "" {
- tokens = append(tokens, t)
- }
- }
- if len(tokens) == 0 {
- tokens = append(tokens, defaultRealityScanCandidates...)
- }
- var tasks []realityProbeTask
- var invalid []*RealityScanResult
- for _, token := range tokens {
- if len(tasks) >= realityScanMaxTotal {
- break
- }
- if strings.Contains(token, "/") {
- ips, err := enumerateCIDR(token, realityDiscoverMaxIPs)
- if err != nil {
- invalid = append(invalid, &RealityScanResult{Target: token, Reason: "invalid CIDR: " + err.Error()})
- continue
- }
- for _, ip := range ips {
- if len(tasks) >= realityScanMaxTotal {
- break
- }
- tasks = append(tasks, realityProbeTask{dialHost: ip, port: 443, timeout: realityDiscoverTimeout, bulk: true})
- }
- continue
- }
- host, port, err := splitRealityTarget(token)
- if err != nil {
- invalid = append(invalid, &RealityScanResult{Target: token, Reason: err.Error()})
- continue
- }
- if net.ParseIP(host) != nil {
- tasks = append(tasks, realityProbeTask{dialHost: host, port: port, timeout: realityDiscoverTimeout})
- } else {
- tasks = append(tasks, realityProbeTask{dialHost: host, port: port, sni: host, timeout: realityScanTimeout})
- }
- }
- probed := make([]*RealityScanResult, len(tasks))
- sem := make(chan struct{}, realityScanConcurrency)
- var wg sync.WaitGroup
- for i, task := range tasks {
- wg.Add(1)
- sem <- struct{}{}
- go func(idx int, tk realityProbeTask) {
- defer wg.Done()
- defer func() { <-sem }()
- r := s.probeRealityAddr(tk.dialHost, tk.port, tk.sni, tk.timeout)
- if tk.bulk && r.TLSVersion == "" {
- return
- }
- probed[idx] = r
- }(i, task)
- }
- wg.Wait()
- results := dedupRealityResults(append(probed, invalid...))
- sortRealityResults(results)
- return results, nil
- }
- func dedupRealityResults(results []*RealityScanResult) []*RealityScanResult {
- best := make(map[string]*RealityScanResult)
- order := make([]string, 0, len(results))
- for _, r := range results {
- if r == nil {
- continue
- }
- if ex, ok := best[r.Target]; !ok {
- best[r.Target] = r
- order = append(order, r.Target)
- } else if betterRealityResult(r, ex) {
- best[r.Target] = r
- }
- }
- out := make([]*RealityScanResult, 0, len(order))
- for _, k := range order {
- out = append(out, best[k])
- }
- return out
- }
- func betterRealityResult(a, b *RealityScanResult) bool {
- if a.Feasible != b.Feasible {
- return a.Feasible
- }
- return a.LatencyMs > 0 && (b.LatencyMs == 0 || a.LatencyMs < b.LatencyMs)
- }
- func sortRealityResults(results []*RealityScanResult) {
- slices.SortStableFunc(results, func(a, b *RealityScanResult) int {
- if a.Feasible != b.Feasible {
- if a.Feasible {
- return -1
- }
- return 1
- }
- return a.LatencyMs - b.LatencyMs
- })
- }
|