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.
This commit is contained in:
nemunaire 2026-04-29 13:34:27 +07:00
commit a9f37c79cf
18 changed files with 1569 additions and 5 deletions

103
tlsenum/ciphers.go Normal file
View file

@ -0,0 +1,103 @@
package tlsenum
// CipherSuite pairs an IANA TLS cipher suite ID with its standard name.
//
// The catalog below intentionally covers the "real-world" set: modern AEAD
// suites used by TLS 1.2/1.3, plus a long tail of legacy CBC/RC4/3DES/EXPORT
// suites we want to *detect* on remote servers (so we can flag them), even
// though Go's stdlib refuses to negotiate them. utls lets us put any 16-bit
// value in the offered list, so the server's accept/reject decision is the
// source of truth.
type CipherSuite struct {
ID uint16
Name string
// TLS13 is true for the five TLS 1.3 AEAD suites; those must only be
// offered with TLS 1.3 ClientHellos.
TLS13 bool
}
// TLS13Ciphers are the AEAD suites defined for TLS 1.3 (RFC 8446 §B.4).
var TLS13Ciphers = []CipherSuite{
{0x1301, "TLS_AES_128_GCM_SHA256", true},
{0x1302, "TLS_AES_256_GCM_SHA384", true},
{0x1303, "TLS_CHACHA20_POLY1305_SHA256", true},
{0x1304, "TLS_AES_128_CCM_SHA256", true},
{0x1305, "TLS_AES_128_CCM_8_SHA256", true},
}
// LegacyCiphers covers TLS 1.0/1.1/1.2 (and SSLv3) suites. Not exhaustive of
// the IANA registry, but it includes everything any modern audit cares about:
// ECDHE/DHE/RSA/PSK kex, AES-GCM/CCM/CBC, ChaCha20, 3DES, RC4, NULL, EXPORT,
// anonymous, and a handful of GOST/CAMELLIA/ARIA entries seen in the wild.
var LegacyCiphers = []CipherSuite{
// ECDHE-ECDSA
{0xC02B, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", false},
{0xC02C, "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", false},
{0xCCA9, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", false},
{0xC023, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", false},
{0xC024, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", false},
{0xC009, "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", false},
{0xC00A, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", false},
{0xC008, "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", false},
{0xC007, "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", false},
{0xC006, "TLS_ECDHE_ECDSA_WITH_NULL_SHA", false},
// ECDHE-RSA
{0xC02F, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", false},
{0xC030, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", false},
{0xCCA8, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", false},
{0xC027, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", false},
{0xC028, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", false},
{0xC013, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", false},
{0xC014, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", false},
{0xC012, "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", false},
{0xC011, "TLS_ECDHE_RSA_WITH_RC4_128_SHA", false},
{0xC010, "TLS_ECDHE_RSA_WITH_NULL_SHA", false},
// DHE-RSA
{0x009E, "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", false},
{0x009F, "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", false},
{0xCCAA, "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", false},
{0x0067, "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", false},
{0x006B, "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", false},
{0x0033, "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", false},
{0x0039, "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", false},
{0x0016, "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA", false},
// Plain RSA
{0x009C, "TLS_RSA_WITH_AES_128_GCM_SHA256", false},
{0x009D, "TLS_RSA_WITH_AES_256_GCM_SHA384", false},
{0x003C, "TLS_RSA_WITH_AES_128_CBC_SHA256", false},
{0x003D, "TLS_RSA_WITH_AES_256_CBC_SHA256", false},
{0x002F, "TLS_RSA_WITH_AES_128_CBC_SHA", false},
{0x0035, "TLS_RSA_WITH_AES_256_CBC_SHA", false},
{0x000A, "TLS_RSA_WITH_3DES_EDE_CBC_SHA", false},
{0x0005, "TLS_RSA_WITH_RC4_128_SHA", false},
{0x0004, "TLS_RSA_WITH_RC4_128_MD5", false},
{0x003B, "TLS_RSA_WITH_NULL_SHA256", false},
{0x0002, "TLS_RSA_WITH_NULL_SHA", false},
{0x0001, "TLS_RSA_WITH_NULL_MD5", false},
// Anonymous (broken by design — flag if seen)
{0x006D, "TLS_DH_anon_WITH_AES_256_CBC_SHA256", false},
{0x0034, "TLS_DH_anon_WITH_AES_128_CBC_SHA", false},
{0x003A, "TLS_DH_anon_WITH_AES_256_CBC_SHA", false},
{0xC018, "TLS_ECDH_anon_WITH_AES_128_CBC_SHA", false},
{0xC019, "TLS_ECDH_anon_WITH_AES_256_CBC_SHA", false},
// EXPORT (40-bit, illegal since ~2000 — flag if seen)
{0x0008, "TLS_RSA_EXPORT_WITH_DES40_CBC_SHA", false},
{0x0014, "TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", false},
{0x0017, "TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", false},
{0x0019, "TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", false},
{0x0003, "TLS_RSA_EXPORT_WITH_RC4_40_MD5", false},
{0x0006, "TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5", false},
}
// AllCiphers concatenates legacy and TLS 1.3 cipher suites.
func AllCiphers() []CipherSuite {
out := make([]CipherSuite, 0, len(LegacyCiphers)+len(TLS13Ciphers))
out = append(out, LegacyCiphers...)
out = append(out, TLS13Ciphers...)
return out
}

283
tlsenum/tlsenum.go Normal file
View file

@ -0,0 +1,283 @@
// Package tlsenum probes a remote endpoint to discover the exact set of
// SSL/TLS protocol versions and cipher suites it accepts.
//
// The Go stdlib's crypto/tls only negotiates a curated subset of modern
// suites and refuses to even offer legacy ones (RC4, 3DES, EXPORT, NULL,
// anonymous, …), so it cannot be used to *audit* what a server accepts.
// Instead we use github.com/refraction-networking/utls to craft a fully
// custom ClientHello carrying a single (version, cipher) pair and let the
// server tell us — by ServerHello or alert — whether it accepts it.
//
// Scope of the minimal version:
// - TLS 1.0, 1.1, 1.2, 1.3 (negotiated via the SupportedVersions extension).
// - Direct TLS only; STARTTLS upgrade is the caller's responsibility for
// now (the existing checker package owns those dialect handlers).
// - SSLv3 and SSLv2 are deliberately out of scope; SSLv2 has a different
// wire format and would require either raw byte crafting or a legacy
// OpenSSL sidecar.
package tlsenum
import (
"context"
"errors"
"fmt"
"net"
"strconv"
"time"
utls "github.com/refraction-networking/utls"
)
// AllVersions is the set of protocol versions Probe knows how to offer.
var AllVersions = []uint16{
utls.VersionTLS10,
utls.VersionTLS11,
utls.VersionTLS12,
utls.VersionTLS13,
}
// VersionName returns a human-readable label for a TLS protocol version.
func VersionName(v uint16) string {
switch v {
case utls.VersionTLS10:
return "TLS 1.0"
case utls.VersionTLS11:
return "TLS 1.1"
case utls.VersionTLS12:
return "TLS 1.2"
case utls.VersionTLS13:
return "TLS 1.3"
default:
return "0x" + strconv.FormatUint(uint64(v), 16)
}
}
// ProbeResult is the outcome of a single (version, cipher) attempt.
type ProbeResult struct {
OfferedVersion uint16
OfferedCipher uint16
// Accepted is true when the server completed enough of the handshake to
// echo back a ServerHello with our offered version and cipher. We do not
// require a fully successful handshake (certificate verification can fail
// for unrelated reasons); ServerHello acceptance is what we measure.
Accepted bool
// NegotiatedVersion / NegotiatedCipher are populated when Accepted is
// true. They should match the offered values; if they differ, the server
// is misbehaving (or downgrading).
NegotiatedVersion uint16
NegotiatedCipher uint16
// Err is the underlying error from the dial or handshake. For a clean
// "server rejected this combination" outcome it will typically be a TLS
// alert (handshake_failure, protocol_version, insufficient_security…).
Err error
}
// ProbeOptions controls a single Probe call.
type ProbeOptions struct {
// Timeout bounds dial + (optional) upgrade + handshake. A zero value
// means no deadline beyond the parent context's.
Timeout time.Duration
// Upgrader, when non-nil, is invoked on the freshly-dialed connection
// before the TLS ClientHello is sent. It is the injection point for
// STARTTLS dialect handlers (SMTP, IMAP, POP3, …): the callback drives
// the plaintext exchange that requests the upgrade and returns nil once
// the connection is ready for tls.Client. tlsenum stays agnostic of the
// dialect; the caller owns that knowledge.
Upgrader func(net.Conn) error
}
// Probe attempts a TLS handshake against addr offering exactly one protocol
// version and one cipher suite. It never panics; transport / handshake errors
// are reported on the returned ProbeResult.
//
// addr must be host:port. sni is the SNI to send (pass the host if unsure).
func Probe(ctx context.Context, addr, sni string, version, cipher uint16, opts ProbeOptions) ProbeResult {
res := ProbeResult{OfferedVersion: version, OfferedCipher: cipher}
dialCtx := ctx
if opts.Timeout > 0 {
var cancel context.CancelFunc
dialCtx, cancel = context.WithTimeout(ctx, opts.Timeout)
defer cancel()
}
d := &net.Dialer{}
raw, err := d.DialContext(dialCtx, "tcp", addr)
if err != nil {
res.Err = fmt.Errorf("dial: %w", err)
return res
}
defer raw.Close()
if dl, ok := dialCtx.Deadline(); ok {
_ = raw.SetDeadline(dl)
}
if opts.Upgrader != nil {
if err := opts.Upgrader(raw); err != nil {
res.Err = fmt.Errorf("upgrade: %w", err)
return res
}
}
cfg := &utls.Config{
ServerName: sni,
InsecureSkipVerify: true, // #nosec G402 -- enumeration; we only care about handshake outcome
}
uc := utls.UClient(raw, cfg, utls.HelloCustom)
spec := buildSpec(version, cipher, sni)
if err := uc.ApplyPreset(&spec); err != nil {
res.Err = fmt.Errorf("apply-preset: %w", err)
return res
}
err = uc.Handshake()
state := uc.ConnectionState()
if err == nil {
res.Accepted = true
res.NegotiatedVersion = state.Version
res.NegotiatedCipher = state.CipherSuite
return res
}
// Some servers complete ServerHello (so we know they accepted version +
// cipher) but fail later — for example, certificate-mismatch or the
// client failing to verify. If state has a non-zero Version/CipherSuite
// matching what we offered, we still count it as accepted.
if state.Version == version && state.CipherSuite == cipher && state.CipherSuite != 0 {
res.Accepted = true
res.NegotiatedVersion = state.Version
res.NegotiatedCipher = state.CipherSuite
}
res.Err = err
return res
}
// EnumerateOptions controls Enumerate.
type EnumerateOptions struct {
// Timeout for each individual probe. Defaults to 5s when zero.
ProbeTimeout time.Duration
// Versions to try. Defaults to AllVersions when nil.
Versions []uint16
// Ciphers to try. Defaults to AllCiphers() when nil. The TLS13 flag is
// honored: TLS 1.3 ciphers are only offered with TLS 1.3 probes, and
// vice-versa.
Ciphers []CipherSuite
// Upgrader, when non-nil, is forwarded to every sub-probe (see
// ProbeOptions.Upgrader). It is invoked on a freshly-dialed connection
// before each ClientHello, so STARTTLS dialect handlers run once per
// probe, not once for the whole sweep.
Upgrader func(net.Conn) error
}
// EnumerationResult is the aggregate outcome of an enumeration sweep.
type EnumerationResult struct {
// SupportedVersions lists protocol versions for which at least one
// cipher was accepted.
SupportedVersions []uint16
// CiphersByVersion lists, per accepted version, the cipher suites the
// server agreed to negotiate.
CiphersByVersion map[uint16][]CipherSuite
}
// Enumerate sweeps a (version × cipher) matrix against addr and returns what
// the server actually accepts. Probes are performed sequentially; concurrency
// can be added later but tends to upset some middleboxes when probing too
// hard.
func Enumerate(ctx context.Context, addr, sni string, opts EnumerateOptions) (EnumerationResult, error) {
if opts.ProbeTimeout == 0 {
opts.ProbeTimeout = 5 * time.Second
}
versions := opts.Versions
if versions == nil {
versions = AllVersions
}
ciphers := opts.Ciphers
if ciphers == nil {
ciphers = AllCiphers()
}
out := EnumerationResult{
CiphersByVersion: make(map[uint16][]CipherSuite),
}
seenVersion := make(map[uint16]bool)
for _, v := range versions {
isTLS13 := v == utls.VersionTLS13
for _, c := range ciphers {
if c.TLS13 != isTLS13 {
continue
}
if err := ctx.Err(); err != nil {
return out, err
}
r := Probe(ctx, addr, sni, v, c.ID, ProbeOptions{
Timeout: opts.ProbeTimeout,
Upgrader: opts.Upgrader,
})
if !r.Accepted {
continue
}
out.CiphersByVersion[v] = append(out.CiphersByVersion[v], c)
if !seenVersion[v] {
seenVersion[v] = true
out.SupportedVersions = append(out.SupportedVersions, v)
}
}
}
return out, nil
}
// buildSpec assembles a ClientHelloSpec offering exactly one cipher and one
// protocol version. For TLS 1.3 the legacy version field stays at TLS 1.2 and
// the real version is signalled through the SupportedVersions extension, per
// RFC 8446 §4.1.2 / §4.2.1.
func buildSpec(version, cipher uint16, sni string) utls.ClientHelloSpec {
tlsVersMin := version
tlsVersMax := version
if version == utls.VersionTLS13 {
// utls inspects TLSVersMax to decide whether to drive TLS 1.3
// machinery; the on-the-wire legacy_version stays TLS 1.2.
tlsVersMin = utls.VersionTLS12
}
exts := []utls.TLSExtension{
&utls.SNIExtension{ServerName: sni},
&utls.SupportedCurvesExtension{Curves: []utls.CurveID{
utls.X25519, utls.CurveP256, utls.CurveP384, utls.CurveP521,
}},
&utls.SupportedPointsExtension{SupportedPoints: []byte{0}}, // uncompressed
&utls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []utls.SignatureScheme{
utls.ECDSAWithP256AndSHA256, utls.ECDSAWithP384AndSHA384, utls.ECDSAWithP521AndSHA512,
utls.PSSWithSHA256, utls.PSSWithSHA384, utls.PSSWithSHA512,
utls.PKCS1WithSHA256, utls.PKCS1WithSHA384, utls.PKCS1WithSHA512,
utls.PKCS1WithSHA1, utls.ECDSAWithSHA1,
}},
&utls.RenegotiationInfoExtension{Renegotiation: utls.RenegotiateOnceAsClient},
}
if version == utls.VersionTLS13 {
exts = append(exts,
&utls.SupportedVersionsExtension{Versions: []uint16{utls.VersionTLS13}},
&utls.KeyShareExtension{KeyShares: []utls.KeyShare{
{Group: utls.X25519},
}},
&utls.PSKKeyExchangeModesExtension{Modes: []uint8{utls.PskModeDHE}},
)
}
return utls.ClientHelloSpec{
TLSVersMin: tlsVersMin,
TLSVersMax: tlsVersMax,
CipherSuites: []uint16{cipher},
CompressionMethods: []byte{0}, // null
Extensions: exts,
}
}
// ErrNoVersions is returned when an enumeration request asks for an empty set
// of versions or ciphers.
var ErrNoVersions = errors.New("tlsenum: no versions or ciphers to probe")

223
tlsenum/tlsenum_test.go Normal file
View file

@ -0,0 +1,223 @@
package tlsenum
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
stdtls "crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
"net"
"os"
"testing"
"time"
utls "github.com/refraction-networking/utls"
)
// selfSignedCert returns a brand-new in-memory self-signed cert + key for
// "test.local", suitable for stdlib tls.Server.
func selfSignedCert() (stdtls.Certificate, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return stdtls.Certificate{}, err
}
tmpl := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "test.local"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
DNSNames: []string{"test.local"},
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &key.PublicKey, key)
if err != nil {
return stdtls.Certificate{}, err
}
keyDER, err := x509.MarshalECPrivateKey(key)
if err != nil {
return stdtls.Certificate{}, err
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
return stdtls.X509KeyPair(certPEM, keyPEM)
}
// runFakeStartTLSServer accepts one connection, expects a "STARTTLS\r\n"
// line, replies "OK\r\n", then runs a TLS handshake. It returns once the
// handshake completes (or fails) and the connection is closed.
func runFakeStartTLSServer(ln net.Listener, cert stdtls.Certificate) error {
c, err := ln.Accept()
if err != nil {
return err
}
defer c.Close()
buf := make([]byte, len("STARTTLS\r\n"))
if _, err := io.ReadFull(c, buf); err != nil {
return err
}
if string(buf) != "STARTTLS\r\n" {
return fmt.Errorf("unexpected pre-tls line: %q", string(buf))
}
if _, err := c.Write([]byte("OK\r\n")); err != nil {
return err
}
tc := stdtls.Server(c, &stdtls.Config{
Certificates: []stdtls.Certificate{cert},
MinVersion: stdtls.VersionTLS12,
})
defer tc.Close()
return tc.Handshake()
}
// liveTarget returns a host:port to enumerate against, or skips the test if
// the environment hasn't opted in. Network tests are gated behind
// TLSENUM_LIVE=1 so the unit-test suite stays hermetic.
func liveTarget(t *testing.T) (addr, sni string) {
t.Helper()
if os.Getenv("TLSENUM_LIVE") == "" {
t.Skip("set TLSENUM_LIVE=1 to run live enumeration tests")
}
host := os.Getenv("TLSENUM_HOST")
if host == "" {
host = "tls-v1-2.badssl.com"
}
port := os.Getenv("TLSENUM_PORT")
if port == "" {
port = "1012"
}
return net.JoinHostPort(host, port), host
}
func TestProbe_TLS12_AESGCM(t *testing.T) {
addr, sni := liveTarget(t)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
r := Probe(ctx, addr, sni, utls.VersionTLS12, 0xC02F /* ECDHE-RSA-AES128-GCM-SHA256 */, ProbeOptions{Timeout: 5 * time.Second})
if !r.Accepted {
t.Fatalf("expected ECDHE-RSA-AES128-GCM-SHA256 to be accepted on TLS 1.2 target; got err=%v", r.Err)
}
if r.NegotiatedVersion != utls.VersionTLS12 {
t.Fatalf("negotiated version = %x, want %x", r.NegotiatedVersion, utls.VersionTLS12)
}
}
func TestEnumerate_BasicShape(t *testing.T) {
addr, sni := liveTarget(t)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
res, err := Enumerate(ctx, addr, sni, EnumerateOptions{
ProbeTimeout: 5 * time.Second,
})
if err != nil {
t.Fatalf("Enumerate: %v", err)
}
if len(res.SupportedVersions) == 0 {
t.Fatalf("no supported versions discovered")
}
for v, ciphers := range res.CiphersByVersion {
if len(ciphers) == 0 {
t.Errorf("version %s listed as supported but no ciphers recorded", VersionName(v))
}
t.Logf("%s: %d cipher(s)", VersionName(v), len(ciphers))
}
}
// TestProbe_UpgraderInvoked uses a tiny in-memory STARTTLS-style server: a
// goroutine listens, reads one "STARTTLS\r\n" line, replies "OK\r\n", then
// performs a real Go-stdlib TLS handshake. We probe through the matching
// Upgrader and assert the handshake succeeds — proving the callback runs in
// the right place between dial and ClientHello.
func TestProbe_UpgraderInvoked(t *testing.T) {
cert, err := selfSignedCert()
if err != nil {
t.Fatalf("self-signed cert: %v", err)
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
srvDone := make(chan error, 1)
go func() { srvDone <- runFakeStartTLSServer(ln, cert) }()
upgrader := func(c net.Conn) error {
if _, err := c.Write([]byte("STARTTLS\r\n")); err != nil {
return err
}
buf := make([]byte, 16)
n, err := c.Read(buf)
if err != nil {
return err
}
if got := string(buf[:n]); got != "OK\r\n" {
return fmt.Errorf("unexpected reply: %q", got)
}
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
r := Probe(ctx, ln.Addr().String(), "test.local",
utls.VersionTLS12, 0xC02B, /* ECDHE-ECDSA-AES128-GCM-SHA256 (matches the P-256 cert) */
ProbeOptions{Timeout: 3 * time.Second, Upgrader: upgrader})
if !r.Accepted {
t.Fatalf("expected handshake to succeed through upgrader; err=%v", r.Err)
}
if r.NegotiatedVersion != utls.VersionTLS12 {
t.Fatalf("negotiated %#x, want %#x", r.NegotiatedVersion, utls.VersionTLS12)
}
if err := <-srvDone; err != nil {
t.Logf("fake server done with: %v", err) // accept clean close from utls
}
}
func TestProbe_UpgraderError(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
go func() {
c, _ := ln.Accept()
if c != nil {
c.Close()
}
}()
wantErr := errors.New("plaintext refused starttls")
r := Probe(context.Background(), ln.Addr().String(), "x",
utls.VersionTLS12, 0xC02F,
ProbeOptions{Timeout: 2 * time.Second, Upgrader: func(net.Conn) error { return wantErr }})
if r.Accepted {
t.Fatalf("expected probe to fail when upgrader returns error")
}
if r.Err == nil || !errors.Is(r.Err, wantErr) {
t.Fatalf("expected wrapped upgrader error, got %v", r.Err)
}
}
func TestVersionName(t *testing.T) {
cases := map[uint16]string{
utls.VersionTLS10: "TLS 1.0",
utls.VersionTLS11: "TLS 1.1",
utls.VersionTLS12: "TLS 1.2",
utls.VersionTLS13: "TLS 1.3",
0x9999: "0x9999",
}
for v, want := range cases {
if got := VersionName(v); got != want {
t.Errorf("VersionName(%#x) = %q, want %q", v, got, want)
}
}
}