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.
85 lines
2.6 KiB
Go
85 lines
2.6 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
"git.happydns.org/checker-tls/contract"
|
|
)
|
|
|
|
func (p *tlsProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
|
|
raw, ok := sdk.GetOption[[]sdk.DiscoveryEntry](opts, OptionEndpoints)
|
|
if !ok {
|
|
return nil, fmt.Errorf("no discovery entries in options: did the host wire AutoFillDiscoveryEntries?")
|
|
}
|
|
|
|
timeoutMs := sdk.GetIntOption(opts, OptionProbeTimeoutMs, DefaultProbeTimeoutMs)
|
|
if timeoutMs <= 0 {
|
|
timeoutMs = DefaultProbeTimeoutMs
|
|
}
|
|
timeout := time.Duration(timeoutMs) * time.Millisecond
|
|
enumerate := sdk.GetBoolOption(opts, OptionEnumerateCiphers, false)
|
|
|
|
entries, warnings := contract.ParseEntries(raw)
|
|
for _, w := range warnings {
|
|
log.Printf("checker-tls: discarding malformed entry: %v", w)
|
|
}
|
|
// An empty entry set is not an error: it is the steady state on any
|
|
// target where no producer has published yet, and the first run after
|
|
// a fresh publication when the producer hasn't finished its own cycle.
|
|
// The rule surfaces this as StatusUnknown rather than StatusError so a
|
|
// freshly-enrolled domain doesn't flap red.
|
|
if len(entries) == 0 {
|
|
return &TLSData{Probes: map[string]TLSProbe{}, CollectedAt: time.Now()}, nil
|
|
}
|
|
|
|
probes := make(map[string]TLSProbe, len(entries))
|
|
var mu sync.Mutex
|
|
var wg sync.WaitGroup
|
|
sem := make(chan struct{}, MaxConcurrentProbes)
|
|
dispatch:
|
|
for _, e := range entries {
|
|
select {
|
|
case sem <- struct{}{}:
|
|
case <-ctx.Done():
|
|
break dispatch
|
|
}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer func() { <-sem }()
|
|
pr := probe(ctx, e.Endpoint, timeout)
|
|
log.Printf("checker-tls: %s %s:%d → tls=%s handshake_ok=%t elapsed=%dms err=%q",
|
|
pr.Type, pr.Host, pr.Port, pr.TLSVersion, pr.TLSHandshakeOK, pr.ElapsedMS, pr.Error)
|
|
if enumerate && pr.TLSHandshakeOK {
|
|
enumRes, skipReason := enumerateEndpoint(ctx, e.Endpoint, enumerationBudget)
|
|
switch {
|
|
case enumRes != nil && enumRes.Skipped != "":
|
|
pr.Enum = enumRes
|
|
log.Printf("checker-tls: enum %s:%d → error: %s (duration=%dms)",
|
|
pr.Host, pr.Port, enumRes.Skipped, enumRes.DurationMS)
|
|
case enumRes != nil:
|
|
pr.Enum = enumRes
|
|
log.Printf("checker-tls: enum %s:%d → versions=%d duration=%dms",
|
|
pr.Host, pr.Port, len(enumRes.Versions), enumRes.DurationMS)
|
|
case skipReason != "":
|
|
log.Printf("checker-tls: enum %s:%d → skipped: %s",
|
|
pr.Host, pr.Port, skipReason)
|
|
}
|
|
}
|
|
mu.Lock()
|
|
probes[e.Ref] = pr
|
|
mu.Unlock()
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
return &TLSData{
|
|
Probes: probes,
|
|
CollectedAt: time.Now(),
|
|
}, nil
|
|
}
|