~bigbes/shroud

shroud/internal/reality/scanner.go -rw-r--r-- 2.7 KiB
32187908 — Eugene Blikh refactor: rename Go module to go.bigb.es/shroud a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// Package reality implements TLS scanning to find servers suitable for REALITY protocol.
package reality

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"strings"
	"time"
)

// Host represents a target to scan.
type Host struct {
	IP     net.IP
	Origin string // original input (domain, IP, or CIDR string)
}

// ScanResult holds the outcome of a TLS scan.
type ScanResult struct {
	IP         string
	Origin     string
	CertDomain string        // certificate Subject.CommonName
	CertIssuer string        // pipe-delimited Issuer.Organization
	GeoCode    string        // ISO country code, filled in by caller
	TLSVersion uint16
	ALPN       string
	Latency    time.Duration // TLS handshake duration
	Feasible   bool
	Error      error
}

// ScanConfig controls scanning behavior.
type ScanConfig struct {
	Port    int
	Timeout time.Duration
}

// ScanHost performs a TLS handshake against the given host and checks REALITY feasibility.
func ScanHost(ctx context.Context, host Host, cfg ScanConfig) ScanResult {
	result := ScanResult{
		IP:     host.IP.String(),
		Origin: host.Origin,
	}

	addr := fmt.Sprintf("%s:%d", host.IP.String(), cfg.Port)

	tlsCfg := &tls.Config{
		InsecureSkipVerify: true,
		NextProtos:         []string{"h2", "http/1.1"},
		CurvePreferences:   []tls.CurveID{tls.X25519},
	}

	// Set SNI if the origin is a domain name.
	if net.ParseIP(host.Origin) == nil {
		tlsCfg.ServerName = host.Origin
	}

	dialer := &net.Dialer{Timeout: cfg.Timeout}
	conn, err := dialer.DialContext(ctx, "tcp", addr)
	if err != nil {
		result.Error = fmt.Errorf("dial: %w", err)
		return result
	}
	defer conn.Close()

	_ = conn.SetDeadline(time.Now().Add(cfg.Timeout))

	tlsConn := tls.Client(conn, tlsCfg)
	hsStart := time.Now()
	if err := tlsConn.HandshakeContext(ctx); err != nil {
		result.Error = fmt.Errorf("tls handshake: %w", err)
		return result
	}
	result.Latency = time.Since(hsStart)
	defer tlsConn.Close()

	state := tlsConn.ConnectionState()
	result.TLSVersion = state.Version
	result.ALPN = state.NegotiatedProtocol

	if len(state.PeerCertificates) > 0 {
		cert := state.PeerCertificates[0]
		result.CertDomain = cert.Subject.CommonName
		if len(cert.Issuer.Organization) > 0 {
			result.CertIssuer = strings.Join(cert.Issuer.Organization, " | ")
		}
	}

	result.Feasible = result.TLSVersion == tls.VersionTLS13 &&
		result.ALPN == "h2" &&
		result.CertDomain != "" &&
		result.CertIssuer != ""

	return result
}

// TLSVersionName returns a human-readable name for a TLS version.
func TLSVersionName(v uint16) string {
	switch v {
	case tls.VersionTLS10:
		return "TLS 1.0"
	case tls.VersionTLS11:
		return "TLS 1.1"
	case tls.VersionTLS12:
		return "TLS 1.2"
	case tls.VersionTLS13:
		return "TLS 1.3"
	default:
		return fmt.Sprintf("0x%04x", v)
	}
}