From 136bedb8bb370a3c1076a58aad1216eb58ae34d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 02:17:25 +0000 Subject: [PATCH 1/6] chore(deps): bump golang.org/x/crypto from 0.38.0 to 0.45.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.38.0 to 0.45.0. - [Commits](https://github.com/golang/crypto/compare/v0.38.0...v0.45.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.45.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 16 ++++++++-------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 6a614822..85f8cffd 100644 --- a/go.mod +++ b/go.mod @@ -92,8 +92,8 @@ require ( github.com/zcalusic/sysinfo v1.0.2 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/term v0.32.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/term v0.37.0 // indirect ) require ( @@ -125,12 +125,12 @@ require ( github.com/weppos/publicsuffix-go v0.40.3-0.20250408071509-6074bbe7fd39 // indirect github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect go.etcd.io/bbolt v1.4.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.40.0 - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/tools v0.31.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/djherbis/times.v1 v1.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 8407feb2..657a16ba 100644 --- a/go.sum +++ b/go.sum @@ -414,8 +414,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -445,8 +445,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -474,8 +474,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -493,8 +493,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -531,8 +531,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -540,8 +540,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -554,8 +554,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -585,8 +585,8 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 1b1c04773ebce5421657aac164394f175e55e48e Mon Sep 17 00:00:00 2001 From: Charles Wong Date: Tue, 10 Mar 2026 14:03:48 -0700 Subject: [PATCH 2/6] fix: prevent indefinite hangs during TLS operations (#819) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three independent bugs that together cause tlsx to hang indefinitely on long target lists: 1. ztls tlsHandshakeWithTimeout: the select statement used 'case errChan <- tlsConn.Handshake()' which evaluates Handshake() synchronously before the channel send — the ctx.Done() branch could never fire, making timeouts impossible. Fixed by running Handshake() in a goroutine and closing the connection on context cancellation. 2. ztls EnumerateCiphers: called tlsHandshakeWithTimeout with context.TODO() (no timeout). Replaced with a per-handshake context.WithTimeout using the configured timeout value. 3. tls EnumerateCiphers: called conn.Handshake() directly without any context or timeout. Replaced with conn.HandshakeContext() using a per-handshake timeout context. All three paths can block indefinitely on unresponsive hosts. With high concurrency (300+) and thousands of targets, goroutines accumulate until the process hangs. Fixes #819 --- pkg/tlsx/tls/tls.go | 5 ++++- pkg/tlsx/ztls/ztls.go | 33 ++++++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/pkg/tlsx/tls/tls.go b/pkg/tlsx/tls/tls.go index c07a5ed2..e0aa64f9 100644 --- a/pkg/tlsx/tls/tls.go +++ b/pkg/tlsx/tls/tls.go @@ -236,10 +236,13 @@ func (c *Client) EnumerateCiphers(hostname, ip, port string, options clients.Con conn := tls.Client(baseConn, baseCfg) - if err := conn.Handshake(); err == nil { + // Use HandshakeContext with timeout to prevent indefinite hangs (#819) + handshakeCtx, cancel := context.WithTimeout(context.Background(), time.Duration(c.options.Timeout)*time.Second) + if err := conn.HandshakeContext(handshakeCtx); err == nil { ciphersuite := conn.ConnectionState().CipherSuite enumeratedCiphers = append(enumeratedCiphers, tls.CipherSuiteName(ciphersuite)) } + cancel() _ = conn.Close() // close baseConn internally } return enumeratedCiphers, nil diff --git a/pkg/tlsx/ztls/ztls.go b/pkg/tlsx/ztls/ztls.go index a03b7267..9458f040 100644 --- a/pkg/tlsx/ztls/ztls.go +++ b/pkg/tlsx/ztls/ztls.go @@ -257,10 +257,13 @@ func (c *Client) EnumerateCiphers(hostname, ip, port string, options clients.Con conn := tls.Client(baseConn, baseCfg) baseCfg.CipherSuites = []uint16{ztlsCiphers[v]} - if err := c.tlsHandshakeWithTimeout(conn, context.TODO()); err == nil { + // Use a timeout context for each cipher handshake to prevent indefinite hangs (#819) + handshakeCtx, cancel := context.WithTimeout(context.Background(), time.Duration(c.options.Timeout)*time.Second) + if err := c.tlsHandshakeWithTimeout(conn, handshakeCtx); err == nil { h1 := conn.GetHandshakeLog() enumeratedCiphers = append(enumeratedCiphers, h1.ServerHello.CipherSuite.String()) } + cancel() _ = conn.Close() // also closes baseConn internally } return enumeratedCiphers, nil @@ -321,19 +324,31 @@ func (c *Client) getConfig(hostname, ip, port string, options clients.ConnectOpt } // tlsHandshakeWithCtx attempts tls handshake with given timeout +// tlsHandshakeWithTimeout attempts tls handshake with given timeout. +// +// Previous implementation had a critical bug: it used +// case errChan <- tlsConn.Handshake(): +// which evaluates Handshake() synchronously before the send, making the +// ctx.Done() branch unreachable — the select could never cancel a stuck +// handshake (#819). +// +// Fixed by running Handshake() in a goroutine and closing the connection +// on timeout to unblock it. func (c *Client) tlsHandshakeWithTimeout(tlsConn *tls.Conn, ctx context.Context) error { errChan := make(chan error, 1) - defer close(errChan) + go func() { + errChan <- tlsConn.Handshake() + }() select { case <-ctx.Done(): + // Close the connection to unblock the goroutine stuck in Handshake() + _ = tlsConn.Close() return errorutil.NewWithTag("ztls", "timeout while attempting handshake") //nolint - case errChan <- tlsConn.Handshake(): - } - - err := <-errChan - if err == tls.ErrCertsOnly { - err = nil + case err := <-errChan: + if err == tls.ErrCertsOnly { + err = nil + } + return err } - return err } From fd2b5356aa8933125634515f11ddf1d6ccd5003c Mon Sep 17 00:00:00 2001 From: Charles Wong Date: Tue, 10 Mar 2026 14:42:49 -0700 Subject: [PATCH 3/6] fix: handle zero timeout to avoid already-expired context Per review feedback: when c.options.Timeout is 0, context.WithTimeout(ctx, 0) creates an already-expired context. Fall back to a 10s default in both tls and ztls cipher enumeration. --- pkg/tlsx/tls/tls.go | 7 ++++++- pkg/tlsx/ztls/ztls.go | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/tlsx/tls/tls.go b/pkg/tlsx/tls/tls.go index e0aa64f9..cf4937de 100644 --- a/pkg/tlsx/tls/tls.go +++ b/pkg/tlsx/tls/tls.go @@ -237,7 +237,12 @@ func (c *Client) EnumerateCiphers(hostname, ip, port string, options clients.Con conn := tls.Client(baseConn, baseCfg) // Use HandshakeContext with timeout to prevent indefinite hangs (#819) - handshakeCtx, cancel := context.WithTimeout(context.Background(), time.Duration(c.options.Timeout)*time.Second) + // If Timeout is zero, use a generous default to avoid an already-expired context. + timeout := time.Duration(c.options.Timeout) * time.Second + if timeout <= 0 { + timeout = 10 * time.Second + } + handshakeCtx, cancel := context.WithTimeout(context.Background(), timeout) if err := conn.HandshakeContext(handshakeCtx); err == nil { ciphersuite := conn.ConnectionState().CipherSuite enumeratedCiphers = append(enumeratedCiphers, tls.CipherSuiteName(ciphersuite)) diff --git a/pkg/tlsx/ztls/ztls.go b/pkg/tlsx/ztls/ztls.go index 9458f040..9db20cba 100644 --- a/pkg/tlsx/ztls/ztls.go +++ b/pkg/tlsx/ztls/ztls.go @@ -258,7 +258,12 @@ func (c *Client) EnumerateCiphers(hostname, ip, port string, options clients.Con baseCfg.CipherSuites = []uint16{ztlsCiphers[v]} // Use a timeout context for each cipher handshake to prevent indefinite hangs (#819) - handshakeCtx, cancel := context.WithTimeout(context.Background(), time.Duration(c.options.Timeout)*time.Second) + // If Timeout is zero, use a generous default to avoid an already-expired context. + timeout := time.Duration(c.options.Timeout) * time.Second + if timeout <= 0 { + timeout = 10 * time.Second + } + handshakeCtx, cancel := context.WithTimeout(context.Background(), timeout) if err := c.tlsHandshakeWithTimeout(conn, handshakeCtx); err == nil { h1 := conn.GetHandshakeLog() enumeratedCiphers = append(enumeratedCiphers, h1.ServerHello.CipherSuite.String()) From 57d4d61468850488bf041e2e69fcdb09c276d177 Mon Sep 17 00:00:00 2001 From: Charles Wong Date: Wed, 11 Mar 2026 09:07:28 -0700 Subject: [PATCH 4/6] fix: use idiomatic if Timeout>0 guard for cipher enum contexts Per CodeRabbit review: replace the 'default to 10s fallback' pattern with the idiomatic Go pattern already used in the main connect path (tls.go:109 and ztls.go:118). When Timeout==0, use context.Background() (no expiry) rather than an always-expired or magic-default context. - pkg/tlsx/tls/tls.go: SupportedTLSCiphers cipher enum loop - pkg/tlsx/ztls/ztls.go: SupportedTLSCiphers cipher enum loop --- pkg/tlsx/tls/tls.go | 13 +++++++------ pkg/tlsx/ztls/ztls.go | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pkg/tlsx/tls/tls.go b/pkg/tlsx/tls/tls.go index cf4937de..15f22e06 100644 --- a/pkg/tlsx/tls/tls.go +++ b/pkg/tlsx/tls/tls.go @@ -236,13 +236,14 @@ func (c *Client) EnumerateCiphers(hostname, ip, port string, options clients.Con conn := tls.Client(baseConn, baseCfg) - // Use HandshakeContext with timeout to prevent indefinite hangs (#819) - // If Timeout is zero, use a generous default to avoid an already-expired context. - timeout := time.Duration(c.options.Timeout) * time.Second - if timeout <= 0 { - timeout = 10 * time.Second + // Use HandshakeContext with timeout to prevent indefinite hangs (#819). + // Mirror the zero-timeout guard from the main connect path (lines 109-114): + // when Timeout is unset (0), use a plain Background context (no expiry). + handshakeCtx := context.Background() + cancel := func() {} + if c.options.Timeout > 0 { + handshakeCtx, cancel = context.WithTimeout(handshakeCtx, time.Duration(c.options.Timeout)*time.Second) } - handshakeCtx, cancel := context.WithTimeout(context.Background(), timeout) if err := conn.HandshakeContext(handshakeCtx); err == nil { ciphersuite := conn.ConnectionState().CipherSuite enumeratedCiphers = append(enumeratedCiphers, tls.CipherSuiteName(ciphersuite)) diff --git a/pkg/tlsx/ztls/ztls.go b/pkg/tlsx/ztls/ztls.go index 9db20cba..c89df73b 100644 --- a/pkg/tlsx/ztls/ztls.go +++ b/pkg/tlsx/ztls/ztls.go @@ -257,13 +257,14 @@ func (c *Client) EnumerateCiphers(hostname, ip, port string, options clients.Con conn := tls.Client(baseConn, baseCfg) baseCfg.CipherSuites = []uint16{ztlsCiphers[v]} - // Use a timeout context for each cipher handshake to prevent indefinite hangs (#819) - // If Timeout is zero, use a generous default to avoid an already-expired context. - timeout := time.Duration(c.options.Timeout) * time.Second - if timeout <= 0 { - timeout = 10 * time.Second + // Use a timeout context for each cipher handshake to prevent indefinite hangs (#819). + // Mirror the zero-timeout guard at lines 118-122: when Timeout is unset (0), + // use a plain Background context (no expiry) rather than an already-expired one. + handshakeCtx := context.Background() + cancel := func() {} + if c.options.Timeout > 0 { + handshakeCtx, cancel = context.WithTimeout(handshakeCtx, time.Duration(c.options.Timeout)*time.Second) } - handshakeCtx, cancel := context.WithTimeout(context.Background(), timeout) if err := c.tlsHandshakeWithTimeout(conn, handshakeCtx); err == nil { h1 := conn.GetHandshakeLog() enumeratedCiphers = append(enumeratedCiphers, h1.ServerHello.CipherSuite.String()) From b936f577aa812cecb76928f2329cfd7ed5aeeb35 Mon Sep 17 00:00:00 2001 From: Charles Wong Date: Wed, 11 Mar 2026 09:19:40 -0700 Subject: [PATCH 5/6] refactor: fix ctx parameter order and stale comment in ztls - tlsHandshakeWithTimeout: move ctx to first parameter per Go convention (context.Context should always be the first parameter) - Remove duplicate stale comment line '// tlsHandshakeWithCtx attempts...' that was left alongside the new doc comment --- pkg/tlsx/ztls/ztls.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/tlsx/ztls/ztls.go b/pkg/tlsx/ztls/ztls.go index c89df73b..0139d2c2 100644 --- a/pkg/tlsx/ztls/ztls.go +++ b/pkg/tlsx/ztls/ztls.go @@ -140,7 +140,7 @@ func (c *Client) ConnectWithOptions(hostname, ip, port string, options clients.C // new tls connection tlsConn := tls.Client(conn, config) - err = c.tlsHandshakeWithTimeout(tlsConn, ctx) + err = c.tlsHandshakeWithTimeout(ctx, tlsConn) if err != nil { if clients.IsClientCertRequiredError(err) { clientCertRequired = true @@ -265,7 +265,7 @@ func (c *Client) EnumerateCiphers(hostname, ip, port string, options clients.Con if c.options.Timeout > 0 { handshakeCtx, cancel = context.WithTimeout(handshakeCtx, time.Duration(c.options.Timeout)*time.Second) } - if err := c.tlsHandshakeWithTimeout(conn, handshakeCtx); err == nil { + if err := c.tlsHandshakeWithTimeout(handshakeCtx, conn); err == nil { h1 := conn.GetHandshakeLog() enumeratedCiphers = append(enumeratedCiphers, h1.ServerHello.CipherSuite.String()) } @@ -329,7 +329,6 @@ func (c *Client) getConfig(hostname, ip, port string, options clients.ConnectOpt return config, nil } -// tlsHandshakeWithCtx attempts tls handshake with given timeout // tlsHandshakeWithTimeout attempts tls handshake with given timeout. // // Previous implementation had a critical bug: it used @@ -340,7 +339,7 @@ func (c *Client) getConfig(hostname, ip, port string, options clients.ConnectOpt // // Fixed by running Handshake() in a goroutine and closing the connection // on timeout to unblock it. -func (c *Client) tlsHandshakeWithTimeout(tlsConn *tls.Conn, ctx context.Context) error { +func (c *Client) tlsHandshakeWithTimeout(ctx context.Context, tlsConn *tls.Conn) error { errChan := make(chan error, 1) go func() { errChan <- tlsConn.Handshake() From 7194d08f693685701a6eb37ce3271a76c283a4fe Mon Sep 17 00:00:00 2001 From: Charles Wong Date: Wed, 11 Mar 2026 09:30:59 -0700 Subject: [PATCH 6/6] fix(ztls): prefer completed handshake over simultaneous timeout Go's select statement is non-deterministic when multiple cases are ready at the same time. When Handshake() completes at the exact moment the context deadline fires, the original code could randomly take the ctx.Done() branch and return a spurious timeout error even though the handshake succeeded. Fix: reorder cases to check errChan first, and add a non-blocking errChan drain in the ctx.Done() branch so a just-completed handshake is never discarded in favour of a timeout. --- pkg/tlsx/ztls/ztls.go | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pkg/tlsx/ztls/ztls.go b/pkg/tlsx/ztls/ztls.go index 0139d2c2..6683bae1 100644 --- a/pkg/tlsx/ztls/ztls.go +++ b/pkg/tlsx/ztls/ztls.go @@ -339,6 +339,11 @@ func (c *Client) getConfig(hostname, ip, port string, options clients.ConnectOpt // // Fixed by running Handshake() in a goroutine and closing the connection // on timeout to unblock it. +// +// Note on select fairness: when both errChan and ctx.Done() are ready +// simultaneously, Go's select chooses randomly. We prefer a completed +// handshake result over a timeout by checking errChan non-blockingly +// in the ctx.Done() branch before returning the timeout error. func (c *Client) tlsHandshakeWithTimeout(ctx context.Context, tlsConn *tls.Conn) error { errChan := make(chan error, 1) go func() { @@ -346,14 +351,24 @@ func (c *Client) tlsHandshakeWithTimeout(ctx context.Context, tlsConn *tls.Conn) }() select { - case <-ctx.Done(): - // Close the connection to unblock the goroutine stuck in Handshake() - _ = tlsConn.Close() - return errorutil.NewWithTag("ztls", "timeout while attempting handshake") //nolint case err := <-errChan: if err == tls.ErrCertsOnly { err = nil } return err + case <-ctx.Done(): + // Prefer a completed handshake result that arrived at the same time + // as the deadline — select is non-deterministic when both are ready. + select { + case err := <-errChan: + if err == tls.ErrCertsOnly { + err = nil + } + return err + default: + } + // Close the connection to unblock the goroutine stuck in Handshake() + _ = tlsConn.Close() + return errorutil.NewWithTag("ztls", "timeout while attempting handshake") //nolint } }