checker-tls/checker/enumerate.go
Pierre-Olivier Mercier a9f37c79cf Add tlsenum package and add version/cipher enumeration into the checker
tlsenum package probes a remote endpoint with one ClientHello
per (version, cipher) pair via utls, so the checker can report the
exact set the server accepts rather than only the suite Go's stdlib
happens to negotiate. Probe accepts an Upgrader callback so STARTTLS
dialects plug in without tlsenum learning about them; the checker
bridges its existing dialect registry through upgraderFor.
2026-04-29 13:35:29 +07:00

68 lines
2.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package checker
import (
"context"
"net"
"strconv"
"strings"
"time"
"git.happydns.org/checker-tls/contract"
"git.happydns.org/checker-tls/tlsenum"
)
// enumerationProbeTimeout caps each individual sub-probe. It is intentionally
// shorter than the main probe timeout: a sweep does dozens of handshakes and
// most rejections come back in tens of ms, so 3s is enough to absorb a slow
// network without dragging the total cost.
const enumerationProbeTimeout = 3 * time.Second
// enumerateEndpoint runs a (version × cipher) sweep against an endpoint —
// direct TLS or STARTTLS — and returns the result in the wire-format consumed
// by rules. It returns (nil, "<reason>") to signal the sweep was deliberately
// skipped.
func enumerateEndpoint(ctx context.Context, ep contract.TLSEndpoint, totalBudget time.Duration) (*TLSEnumeration, string) {
host := strings.TrimSuffix(ep.Host, ".")
addr := net.JoinHostPort(host, strconv.Itoa(int(ep.Port)))
sni := ep.SNI
if sni == "" {
sni = host
}
upgrader, ok := upgraderFor(ep.STARTTLS, sni)
if !ok {
return nil, "unsupported starttls dialect: " + ep.STARTTLS
}
sweepCtx := ctx
if totalBudget > 0 {
var cancel context.CancelFunc
sweepCtx, cancel = context.WithTimeout(ctx, totalBudget)
defer cancel()
}
start := time.Now()
res, err := tlsenum.Enumerate(sweepCtx, addr, sni, tlsenum.EnumerateOptions{
ProbeTimeout: enumerationProbeTimeout,
Upgrader: upgrader,
})
elapsed := time.Since(start).Milliseconds()
if err != nil {
return &TLSEnumeration{Skipped: "enumeration error: " + err.Error(), DurationMS: elapsed}, ""
}
out := &TLSEnumeration{DurationMS: elapsed}
for _, v := range res.SupportedVersions {
ev := EnumVersion{Version: v, Name: tlsenum.VersionName(v)}
for _, c := range res.CiphersByVersion[v] {
ev.Ciphers = append(ev.Ciphers, EnumCipher{ID: c.ID, Name: c.Name})
}
out.Versions = append(out.Versions, ev)
}
return out, ""
}
// enumerationBudget is the upper bound we give one endpoint's sweep. ~50
// handshakes × enumerationProbeTimeout would be 2-3 minutes worst case; we
// cap at 60s so a black-holing target can't stall the whole collect run.
const enumerationBudget = 60 * time.Second