package reality import ( "context" "fmt" "io" "log/slog" "net" "net/http" "net/netip" "sort" "strings" "sync" "time" ) // AutodetectConfig controls the autodetect scan. type AutodetectConfig struct { // ServerIP is the server's own public IP. If empty, it will be detected automatically. // Used for /24 subnet scanning mode. ServerIP string // Domains is a list of domain names to scan directly (instead of /24 subnet). // When set, ServerIP is ignored and domains are resolved and scanned. Domains []string // Port to scan (default 443). Port int // Threads for concurrent scanning. Threads int // Timeout per TLS handshake. Timeout time.Duration // Logger for progress output. Logger *slog.Logger } // AutodetectResult holds the best server found by autodetect. type AutodetectResult struct { ServerName string // domain to use for server_names and dest IP string // IP address of the server Issuer string // certificate issuer Latency time.Duration // TLS handshake latency } // Autodetect finds the best REALITY decoy target by scanning either: // - a list of domains (when cfg.Domains is set), or // - the /24 subnet around the server's public IP (default). // // Returns the fastest feasible result. func Autodetect(ctx context.Context, cfg AutodetectConfig) (*AutodetectResult, error) { if cfg.Port == 0 { cfg.Port = 443 } if cfg.Threads == 0 { cfg.Threads = 32 } if cfg.Timeout == 0 { cfg.Timeout = 5 * time.Second } if cfg.Logger == nil { cfg.Logger = slog.Default() } var hosts []Host var description string if len(cfg.Domains) > 0 { // Domain list mode: resolve each domain and scan. description = fmt.Sprintf("%d domains", len(cfg.Domains)) cfg.Logger.Info("Scanning domain list for REALITY-suitable servers...", "domains", len(cfg.Domains), "threads", cfg.Threads) for _, domain := range cfg.Domains { domain = strings.TrimSpace(domain) if domain == "" || strings.HasPrefix(domain, "#") { continue } resolved, err := resolveDomainForAutodetect(domain) if err != nil { cfg.Logger.Debug("Failed to resolve domain.", "domain", domain, "err", err) continue } hosts = append(hosts, resolved...) } if len(hosts) == 0 { return nil, fmt.Errorf("no domains could be resolved from the provided list") } } else { // Subnet mode: scan /24 around server IP. serverIP := cfg.ServerIP if serverIP == "" { var err error serverIP, err = DetectPublicIP(ctx, cfg.Timeout) if err != nil { return nil, fmt.Errorf("detecting public IP: %w", err) } cfg.Logger.Info("Detected public IP.", "ip", serverIP) } addr, err := netip.ParseAddr(serverIP) if err != nil { return nil, fmt.Errorf("parsing server IP %q: %w", serverIP, err) } prefix := netip.PrefixFrom(addr, 24) prefix = prefix.Masked() description = prefix.String() cfg.Logger.Info("Scanning /24 subnet for REALITY-suitable servers...", "subnet", prefix.String(), "threads", cfg.Threads) for a := prefix.Addr(); prefix.Contains(a); a = a.Next() { ip := a.As4() if ip[3] == 0 || ip[3] == 255 { continue } if a == addr { continue } hosts = append(hosts, Host{ IP: net.IP(a.AsSlice()), Origin: a.String(), }) } } return scanAndPickBest(ctx, hosts, cfg, description) } func scanAndPickBest(ctx context.Context, hosts []Host, cfg AutodetectConfig, description string) (*AutodetectResult, error) { scanCfg := ScanConfig{ Port: cfg.Port, Timeout: cfg.Timeout, } hostCh := make(chan Host, cfg.Threads*2) go func() { defer close(hostCh) for _, h := range hosts { select { case hostCh <- h: case <-ctx.Done(): return } } }() var ( mu sync.Mutex results []ScanResult wg sync.WaitGroup scanned int feasible int ) for i := 0; i < cfg.Threads; i++ { wg.Add(1) go func() { defer wg.Done() for host := range hostCh { result := ScanHost(ctx, host, scanCfg) mu.Lock() scanned++ if result.Feasible { feasible++ results = append(results, result) cfg.Logger.Info("Found feasible server.", "ip", result.IP, "origin", result.Origin, "cn", result.CertDomain, "issuer", result.CertIssuer, "latency", result.Latency.Round(time.Millisecond)) } if scanned%50 == 0 { cfg.Logger.Info("Scan progress...", "scanned", scanned, "total", len(hosts), "feasible", feasible) } mu.Unlock() } }() } wg.Wait() cfg.Logger.Info("Scan complete.", "scanned", scanned, "feasible", feasible) if len(results) == 0 { return nil, fmt.Errorf("no REALITY-suitable servers found in %s", description) } sort.Slice(results, func(i, j int) bool { return results[i].Latency < results[j].Latency }) best := results[0] return &AutodetectResult{ ServerName: best.CertDomain, IP: best.IP, Issuer: best.CertIssuer, Latency: best.Latency, }, nil } // resolveDomainForAutodetect resolves a domain to IPv4 hosts with the domain as Origin // (so that ScanHost sets SNI correctly). func resolveDomainForAutodetect(domain string) ([]Host, error) { ips, err := net.LookupIP(domain) if err != nil { return nil, err } var hosts []Host for _, ip := range ips { if ip.To4() == nil { continue // IPv4 only } hosts = append(hosts, Host{ IP: ip, Origin: domain, }) } if len(hosts) == 0 { return nil, fmt.Errorf("no IPv4 addresses for %s", domain) } return hosts, nil } // ipServices are HTTP endpoints that return the caller's public IP as plain text. var ipServices = []string{ "https://ifconfig.me", "https://api.ipify.org", "https://icanhazip.com", } // DetectPublicIP discovers the server's public IPv4 address by querying external services. func DetectPublicIP(ctx context.Context, timeout time.Duration) (string, error) { if timeout == 0 { timeout = 5 * time.Second } client := &http.Client{Timeout: timeout} for _, svc := range ipServices { req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc, nil) if err != nil { continue } resp, err := client.Do(req) if err != nil { continue } body, err := io.ReadAll(io.LimitReader(resp.Body, 256)) resp.Body.Close() if err != nil || resp.StatusCode != http.StatusOK { continue } ip := strings.TrimSpace(string(body)) if parsed := net.ParseIP(ip); parsed != nil && parsed.To4() != nil { return ip, nil } } return "", fmt.Errorf("could not determine public IP from any service") }