ProbeSNI runs a single TLS handshake attempt against cfg.TargetIP using sni as the SNI. Returns a classified Result; Err is always non-empty for non-OK outcomes and always empty for OK.
(ctx context.Context, sni string, cfg ProbeConfig)
| 110 | // sni as the SNI. Returns a classified Result; Err is always non-empty for |
| 111 | // non-OK outcomes and always empty for OK. |
| 112 | func ProbeSNI(ctx context.Context, sni string, cfg ProbeConfig) Result { |
| 113 | cfg.defaults() |
| 114 | |
| 115 | host := sni |
| 116 | if cfg.TargetIP.IsValid() { |
| 117 | host = cfg.TargetIP.String() |
| 118 | } |
| 119 | addr := net.JoinHostPort(host, fmt.Sprintf("%d", cfg.TargetPort)) |
| 120 | |
| 121 | r := Result{SNI: sni, TargetIP: cfg.TargetIP} |
| 122 | |
| 123 | // Stage 1: TCP connect. |
| 124 | dialCtx, cancel := context.WithTimeout(ctx, cfg.ConnectTimeout) |
| 125 | defer cancel() |
| 126 | start := time.Now() |
| 127 | conn, err := (&net.Dialer{}).DialContext(dialCtx, "tcp", addr) |
| 128 | r.RTT = time.Since(start) |
| 129 | if err != nil { |
| 130 | r.Outcome, r.Err = classifyDialErr(err) |
| 131 | return r |
| 132 | } |
| 133 | defer conn.Close() |
| 134 | |
| 135 | // Stage 2: TLS ClientHello. We deliberately use InsecureSkipVerify so |
| 136 | // cert mismatches (e.g. TargetIP != sni host) don't count against the |
| 137 | // SNI — we only care whether the handshake *reached* a server response. |
| 138 | tlsConf := &tls.Config{ |
| 139 | ServerName: sni, |
| 140 | InsecureSkipVerify: true, |
| 141 | NextProtos: []string{"h2", "http/1.1"}, |
| 142 | } |
| 143 | hsCtx, hsCancel := context.WithTimeout(ctx, cfg.HandshakeTimeout) |
| 144 | defer hsCancel() |
| 145 | |
| 146 | tlsConn := tls.Client(conn, tlsConf) |
| 147 | hsStart := time.Now() |
| 148 | err = tlsConn.HandshakeContext(hsCtx) |
| 149 | r.Handshake = time.Since(hsStart) + r.RTT |
| 150 | if err != nil { |
| 151 | r.Outcome, r.Err = classifyTLSErr(err) |
| 152 | return r |
| 153 | } |
| 154 | r.Outcome = OutcomeOK |
| 155 | return r |
| 156 | } |
| 157 | |
| 158 | // ProbeSNIs runs ProbeSNI for every sni in the slice concurrently bounded |
| 159 | // by cfg.Concurrency. Results are returned in the same order as input. |