checker-tls/checker/starttls.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

65 lines
2.2 KiB
Go

package checker
import (
"bufio"
"fmt"
"io"
"net"
)
// maxSTARTTLSLineBytes caps the length of a single line read from a STARTTLS
// peer. Real banners and CAPABILITY responses are well under 1 KiB; this
// bound prevents a malicious or buggy server from exhausting memory by
// withholding the line terminator.
const maxSTARTTLSLineBytes = 8 * 1024
// readLineLimited reads bytes from r up to and including the next '\n', or
// until maxSTARTTLSLineBytes have been read without one (in which case it
// returns an error). The returned string keeps the trailing '\n' so callers
// can use the same parsing logic as bufio.Reader.ReadString('\n').
func readLineLimited(r *bufio.Reader) (string, error) {
out := make([]byte, 0, 128)
for {
b, err := r.ReadByte()
if err != nil {
if err == io.EOF && len(out) > 0 {
return string(out), io.ErrUnexpectedEOF
}
return string(out), err
}
out = append(out, b)
if b == '\n' {
return string(out), nil
}
if len(out) >= maxSTARTTLSLineBytes {
return string(out), fmt.Errorf("line exceeds %d bytes without terminator", maxSTARTTLSLineBytes)
}
}
}
// starttlsUpgrader performs the plaintext portion of a STARTTLS upgrade on
// conn, leaving conn ready for tls.Client(conn, …).Handshake(). On success
// the returned function returns nil; on failure it returns a descriptive
// error (wrap errStartTLSNotOffered when the server advertises no STARTTLS).
type starttlsUpgrader func(conn net.Conn, sni string) error
var starttlsUpgraders = map[string]starttlsUpgrader{}
func registerStartTLS(protocol string, upgrader starttlsUpgrader) {
starttlsUpgraders[protocol] = upgrader
}
// upgraderFor returns a tlsenum-compatible upgrader callback for a given
// STARTTLS dialect, plus an ok flag. An empty dialect means direct TLS and
// returns (nil, true) — tlsenum will skip the upgrade phase. An unknown
// dialect returns (nil, false) so the caller can record the skip reason.
func upgraderFor(dialect, sni string) (func(net.Conn) error, bool) {
if dialect == "" {
return nil, true
}
up, ok := starttlsUpgraders[dialect]
if !ok {
return nil, false
}
return func(c net.Conn) error { return up(c, sni) }, true
}