Kaynağa Gözat

Adding TLS session resumption to metrics

* This applies to QUIC, HTTPS Meek and TLS-OSSH connections.
Amir Khan 2 yıl önce
ebeveyn
işleme
fbb50fc0b7

+ 5 - 0
psiphon/common/quic/gquic.go

@@ -110,6 +110,11 @@ func (c *gQUICConnection) isEarlyDataRejected(err error) bool {
 	return false
 	return false
 }
 }
 
 
+func (c *gQUICConnection) hasResumedSession() bool {
+	// gQUIC does not support session resumption.
+	return false
+}
+
 func gQUICDialContext(
 func gQUICDialContext(
 	ctx context.Context,
 	ctx context.Context,
 	packetConn net.PacketConn,
 	packetConn net.PacketConn,

+ 38 - 5
psiphon/common/quic/quic.go

@@ -372,7 +372,7 @@ func Dial(
 	obfuscationNonceTransformerParameters *transforms.ObfuscatorSeedTransformerParameters,
 	obfuscationNonceTransformerParameters *transforms.ObfuscatorSeedTransformerParameters,
 	disablePathMTUDiscovery bool,
 	disablePathMTUDiscovery bool,
 	dialEarly bool,
 	dialEarly bool,
-	tlsClientSessionCache tls.ClientSessionCache) (net.Conn, error) {
+	tlsClientSessionCache *common.TlsClientSessionCacheWrapper) (net.Conn, error) {
 
 
 	if quicVersion == "" {
 	if quicVersion == "" {
 		return nil, errors.TraceNew("missing version")
 		return nil, errors.TraceNew("missing version")
@@ -691,11 +691,19 @@ func (conn *Conn) SetWriteDeadline(t time.Time) error {
 	return conn.stream.SetWriteDeadline(t)
 	return conn.stream.SetWriteDeadline(t)
 }
 }
 
 
+func (conn *Conn) GetMetrics() common.LogFields {
+	logFields := make(common.LogFields)
+	logFields["resumed_session"] = conn.connection.hasResumedSession()
+	return logFields
+}
+
 // QUICTransporter implements the psiphon.transporter interface, used in
 // QUICTransporter implements the psiphon.transporter interface, used in
 // psiphon.MeekConn for HTTP requests, which requires a RoundTripper and
 // psiphon.MeekConn for HTTP requests, which requires a RoundTripper and
 // CloseIdleConnections.
 // CloseIdleConnections.
 type QUICTransporter struct {
 type QUICTransporter struct {
 	quicRoundTripper
 	quicRoundTripper
+	resumedSession bool
+
 	noticeEmitter           func(string)
 	noticeEmitter           func(string)
 	udpDialer               func(ctx context.Context) (net.PacketConn, *net.UDPAddr, error)
 	udpDialer               func(ctx context.Context) (net.PacketConn, *net.UDPAddr, error)
 	quicSNIAddress          string
 	quicSNIAddress          string
@@ -703,7 +711,7 @@ type QUICTransporter struct {
 	clientHelloSeed         *prng.Seed
 	clientHelloSeed         *prng.Seed
 	disablePathMTUDiscovery bool
 	disablePathMTUDiscovery bool
 	dialEarly               bool
 	dialEarly               bool
-	tlsClientSessionCache   tls.ClientSessionCache
+	tlsClientSessionCache   *common.TlsClientSessionCacheWrapper
 	packetConn              atomic.Value
 	packetConn              atomic.Value
 
 
 	mutex sync.Mutex
 	mutex sync.Mutex
@@ -720,7 +728,7 @@ func NewQUICTransporter(
 	clientHelloSeed *prng.Seed,
 	clientHelloSeed *prng.Seed,
 	disablePathMTUDiscovery bool,
 	disablePathMTUDiscovery bool,
 	dialEarly bool,
 	dialEarly bool,
-	tlsClientSessionCache tls.ClientSessionCache) (*QUICTransporter, error) {
+	tlsClientSessionCache *common.TlsClientSessionCacheWrapper) (*QUICTransporter, error) {
 
 
 	if quicVersion == "" {
 	if quicVersion == "" {
 		return nil, errors.TraceNew("missing version")
 		return nil, errors.TraceNew("missing version")
@@ -788,6 +796,12 @@ func (t *QUICTransporter) closePacketConn() {
 	}
 	}
 }
 }
 
 
+func (t *QUICTransporter) GetMetrics() common.LogFields {
+	logFields := make(common.LogFields)
+	logFields["resumed_session"] = t.resumedSession
+	return logFields
+}
+
 func (t *QUICTransporter) dialIETFQUIC(
 func (t *QUICTransporter) dialIETFQUIC(
 	_ context.Context, _ string, _ *tls.Config, _ *ietf_quic.Config) (ietf_quic.EarlyConnection, error) {
 	_ context.Context, _ string, _ *tls.Config, _ *ietf_quic.Config) (ietf_quic.EarlyConnection, error) {
 	// quic-go now supports the request context in its RoundTripper.Dial, but
 	// quic-go now supports the request context in its RoundTripper.Dial, but
@@ -855,6 +869,10 @@ func (t *QUICTransporter) dialQUIC() (retConnection quicConnection, retErr error
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 
 
+	if connection.hasResumedSession() {
+		t.resumedSession = true
+	}
+
 	// dialQUIC uses quic-go.DialContext as we must create our own UDP sockets to
 	// dialQUIC uses quic-go.DialContext as we must create our own UDP sockets to
 	// set properties such as BIND_TO_DEVICE. However, when DialContext is used,
 	// set properties such as BIND_TO_DEVICE. However, when DialContext is used,
 	// quic-go does not take responsibiity for closing the underlying packetConn
 	// quic-go does not take responsibiity for closing the underlying packetConn
@@ -901,6 +919,7 @@ type quicConnection interface {
 	OpenStream() (quicStream, error)
 	OpenStream() (quicStream, error)
 	isErrorIndicatingClosed(err error) bool
 	isErrorIndicatingClosed(err error) bool
 	isEarlyDataRejected(err error) bool
 	isEarlyDataRejected(err error) bool
+	hasResumedSession() bool
 }
 }
 
 
 type quicStream interface {
 type quicStream interface {
@@ -940,6 +959,10 @@ func (l *ietfQUICListener) Close() error {
 
 
 type ietfQUICConnection struct {
 type ietfQUICConnection struct {
 	ietf_quic.Connection
 	ietf_quic.Connection
+
+	// resumedSession is true if the TLS session was probably resumed.
+	// This is only used by the clients to gather metrics.
+	resumedSession bool
 }
 }
 
 
 func (c *ietfQUICConnection) AcceptStream() (quicStream, error) {
 func (c *ietfQUICConnection) AcceptStream() (quicStream, error) {
@@ -981,6 +1004,10 @@ func (c *ietfQUICConnection) isEarlyDataRejected(err error) bool {
 	return err == ietf_quic.Err0RTTRejected
 	return err == ietf_quic.Err0RTTRejected
 }
 }
 
 
+func (c *ietfQUICConnection) hasResumedSession() bool {
+	return c.resumedSession
+}
+
 func dialQUIC(
 func dialQUIC(
 	ctx context.Context,
 	ctx context.Context,
 	packetConn net.PacketConn,
 	packetConn net.PacketConn,
@@ -993,7 +1020,7 @@ func dialQUIC(
 	clientMaxPacketSizeAdjustment int,
 	clientMaxPacketSizeAdjustment int,
 	disablePathMTUDiscovery bool,
 	disablePathMTUDiscovery bool,
 	dialEarly bool,
 	dialEarly bool,
-	tlsClientSessionCache tls.ClientSessionCache) (quicConnection, error) {
+	tlsClientSessionCache *common.TlsClientSessionCacheWrapper) (quicConnection, error) {
 
 
 	if isIETFVersionNumber(versionNumber) {
 	if isIETFVersionNumber(versionNumber) {
 		quicConfig := &ietf_quic.Config{
 		quicConfig := &ietf_quic.Config{
@@ -1029,6 +1056,9 @@ func dialQUIC(
 			ClientSessionCache: tlsClientSessionCache,
 			ClientSessionCache: tlsClientSessionCache,
 		}
 		}
 
 
+		// Heuristic to determine if TLS dial is resuming a session.
+		resumedSession := tlsClientSessionCache.IsSessionResumptionAvailable()
+
 		if dialEarly {
 		if dialEarly {
 			// Attempting 0-RTT if possible.
 			// Attempting 0-RTT if possible.
 			dialConnection, err = ietf_quic.DialEarly(
 			dialConnection, err = ietf_quic.DialEarly(
@@ -1050,7 +1080,10 @@ func dialQUIC(
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
 
 
-		return &ietfQUICConnection{Connection: dialConnection}, nil
+		return &ietfQUICConnection{
+			Connection:     dialConnection,
+			resumedSession: resumedSession,
+		}, nil
 
 
 	} else {
 	} else {
 
 

+ 113 - 0
psiphon/common/tls_cache.go

@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2024, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package common
+
+import (
+	"fmt"
+
+	tls "github.com/Psiphon-Labs/psiphon-tls"
+	utls "github.com/refraction-networking/utls"
+)
+
+// TlsClientSessionCacheWrapper is a wrapper around tls.ClientSessionCache
+// that provides a hard-coded key for the cache.
+// It implements the TLSClientSessionCacheWrapper interface.
+type TlsClientSessionCacheWrapper struct {
+	tls.ClientSessionCache
+
+	// sessinoKey specifies the value of the hard-coded TLS session cache key.
+	sessionKey string
+}
+
+// WrapClientSessionCache wraps a tls.ClientSessionCache with a hard-coded key
+// derived from the ipAddress and dialPortNumber.
+func WrapClientSessionCache(
+	cache tls.ClientSessionCache,
+	ipAddress string,
+	dialPortNumber int) *TlsClientSessionCacheWrapper {
+
+	return &TlsClientSessionCacheWrapper{
+		ClientSessionCache: cache,
+		sessionKey:         sessionKey(ipAddress, dialPortNumber),
+	}
+}
+
+func (c *TlsClientSessionCacheWrapper) Get(_ string) (session *tls.ClientSessionState, ok bool) {
+	return c.ClientSessionCache.Get(c.sessionKey)
+}
+
+func (c *TlsClientSessionCacheWrapper) Put(_ string, cs *tls.ClientSessionState) {
+	c.ClientSessionCache.Put(c.sessionKey, cs)
+}
+
+func (c *TlsClientSessionCacheWrapper) IsSessionResumptionAvailable() bool {
+	// Ignore the ok return value, as the session may still be till if ok is true.
+	session, _ := c.Get(c.sessionKey)
+	return session != nil
+}
+
+func (c *TlsClientSessionCacheWrapper) RemoveCacheEntry() {
+	c.ClientSessionCache.Put(c.sessionKey, nil)
+}
+
+// UtlClientSessionCacheWrapper is a wrapper around utls.ClientSessionCache
+// that provides a hard-coded key for the cache.
+// It implements the TLSClientSessionCacheWrapper interface.
+type UtlsClientSessionCacheWrapper struct {
+	utls.ClientSessionCache
+
+	// sessinoKey specifies the value of the hard-coded TLS session cache key.
+	sessionKey string
+}
+
+// WrapUtlsClientSessionCache wraps a utls.ClientSessionCache with a hard-coded key
+// derived from the ipAddress and dialPortNumber.
+func WrapUtlsClientSessionCache(
+	cache utls.ClientSessionCache,
+	ipAddress string,
+	dialPortNumber int) *UtlsClientSessionCacheWrapper {
+
+	return &UtlsClientSessionCacheWrapper{
+		ClientSessionCache: cache,
+		sessionKey:         sessionKey(ipAddress, dialPortNumber),
+	}
+}
+
+func (c *UtlsClientSessionCacheWrapper) Get(_ string) (session *utls.ClientSessionState, ok bool) {
+	return c.ClientSessionCache.Get(c.sessionKey)
+}
+
+func (c *UtlsClientSessionCacheWrapper) Put(_ string, cs *utls.ClientSessionState) {
+	c.ClientSessionCache.Put(c.sessionKey, cs)
+}
+
+func (c *UtlsClientSessionCacheWrapper) IsSessionResumptionAvailable() bool {
+	// Ignore the ok return value, as the session may still be till if ok is true.
+	session, _ := c.Get(c.sessionKey)
+	return session != nil
+}
+
+func (c *UtlsClientSessionCacheWrapper) RemoveCacheEntry() {
+	c.ClientSessionCache.Put(c.sessionKey, nil)
+}
+
+func sessionKey(ipAddress string, dialPortNumber int) string {
+	return fmt.Sprintf("%s:%d", ipAddress, dialPortNumber)
+}

+ 40 - 32
psiphon/dialParameters.go

@@ -168,9 +168,8 @@ type DialParameters struct {
 	steeringIPCache    *lrucache.Cache `json:"-"`
 	steeringIPCache    *lrucache.Cache `json:"-"`
 	steeringIPCacheKey string          `json:"-"`
 	steeringIPCacheKey string          `json:"-"`
 
 
-	quicTLSSessionCacheKey    string                  `json:"-"`
-	QUICTLSClientSessionCache tls.ClientSessionCache  `json:"-"`
-	tlsClientSessionCache     utls.ClientSessionCache `json:"-"`
+	QUICTLSClientSessionCache *common.TlsClientSessionCacheWrapper  `json:"-"`
+	tlsClientSessionCache     *common.UtlsClientSessionCacheWrapper `json:"-"`
 
 
 	dialConfig *DialConfig `json:"-"`
 	dialConfig *DialConfig `json:"-"`
 	meekConfig *MeekConfig `json:"-"`
 	meekConfig *MeekConfig `json:"-"`
@@ -371,8 +370,6 @@ func MakeDialParameters(
 
 
 	dialParams.steeringIPCache = steeringIPCache
 	dialParams.steeringIPCache = steeringIPCache
 
 
-	dialParams.tlsClientSessionCache = tlsClientSessionCache
-
 	dialParams.ServerEntry = serverEntry
 	dialParams.ServerEntry = serverEntry
 	dialParams.NetworkID = networkID
 	dialParams.NetworkID = networkID
 	dialParams.IsReplay = isReplay
 	dialParams.IsReplay = isReplay
@@ -688,6 +685,22 @@ func MakeDialParameters(
 		protocol.TunnelProtocolUsesTLSOSSH(dialParams.TunnelProtocol) ||
 		protocol.TunnelProtocolUsesTLSOSSH(dialParams.TunnelProtocol) ||
 		dialParams.ConjureAPIRegistration
 		dialParams.ConjureAPIRegistration
 
 
+	if usingTLS {
+		dialPortNumber, err := serverEntry.GetDialPortNumber(dialParams.TunnelProtocol)
+		if err != nil {
+			return nil, errors.Trace(err)
+		}
+		dialParams.tlsClientSessionCache = common.WrapUtlsClientSessionCache(
+			tlsClientSessionCache,
+			serverEntry.IpAddress,
+			dialPortNumber)
+
+		if !isReplay {
+			// Remove the cache entry to make a fresh dial when !isReplay.
+			dialParams.tlsClientSessionCache.RemoveCacheEntry()
+		}
+	}
+
 	if (!isReplay || !replayTLSProfile) && usingTLS {
 	if (!isReplay || !replayTLSProfile) && usingTLS {
 
 
 		dialParams.SelectedTLSProfile = true
 		dialParams.SelectedTLSProfile = true
@@ -815,14 +828,21 @@ func MakeDialParameters(
 				p.WeightedCoinFlip(parameters.QUICDisableClientPathMTUDiscoveryProbability)
 				p.WeightedCoinFlip(parameters.QUICDisableClientPathMTUDiscoveryProbability)
 	}
 	}
 
 
-	// Sets up client session caching for QUIC with a TLS cache key unique to current endpoint.
 	if protocol.TunnelProtocolUsesQUIC(dialParams.TunnelProtocol) {
 	if protocol.TunnelProtocolUsesQUIC(dialParams.TunnelProtocol) {
 		dialPortNumber, err := serverEntry.GetDialPortNumber(dialParams.TunnelProtocol)
 		dialPortNumber, err := serverEntry.GetDialPortNumber(dialParams.TunnelProtocol)
 		if err != nil {
 		if err != nil {
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
-		dialParams.quicTLSSessionCacheKey = fmt.Sprintf("%s:%d", serverEntry.IpAddress, dialPortNumber)
-		dialParams.QUICTLSClientSessionCache = WrapClientSessionCache(quicTLSClientSessionCache, dialParams.quicTLSSessionCacheKey)
+		dialParams.QUICTLSClientSessionCache = common.WrapClientSessionCache(
+			quicTLSClientSessionCache,
+			serverEntry.IpAddress,
+			dialPortNumber)
+
+		if !isReplay {
+			// Remove the cache entry to make a fresh dial when !isReplay.
+			dialParams.QUICTLSClientSessionCache.RemoveCacheEntry()
+		}
+
 	}
 	}
 
 
 	if (!isReplay || !replayObfuscatedQUIC) &&
 	if (!isReplay || !replayObfuscatedQUIC) &&
@@ -1483,9 +1503,19 @@ func (dialParams *DialParameters) Failed(config *Config) {
 		dialParams.steeringIPCache.Delete(dialParams.steeringIPCacheKey)
 		dialParams.steeringIPCache.Delete(dialParams.steeringIPCacheKey)
 	}
 	}
 
 
-	// Clear the TLS client session cache to avoid (potentially) reusing failed sessions.
+	// Clear the TLS client session cache to avoid (potentially) reusing failed sessions for
+	// Meek, TLS-OSSH and QUIC connections.
+
 	if protocol.TunnelProtocolUsesQUIC(dialParams.TunnelProtocol) {
 	if protocol.TunnelProtocolUsesQUIC(dialParams.TunnelProtocol) {
-		dialParams.QUICTLSClientSessionCache.Put(dialParams.quicTLSSessionCacheKey, nil)
+		dialParams.QUICTLSClientSessionCache.RemoveCacheEntry()
+	}
+
+	usingTLS := protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol) ||
+		protocol.TunnelProtocolUsesTLSOSSH(dialParams.TunnelProtocol) ||
+		dialParams.ConjureAPIRegistration
+
+	if usingTLS {
+		dialParams.tlsClientSessionCache.RemoveCacheEntry()
 	}
 	}
 
 
 }
 }
@@ -1972,25 +2002,3 @@ func selectConjureTransport(
 
 
 	return transports[choice]
 	return transports[choice]
 }
 }
-
-type tlsClientSessionCacheWrapper struct {
-	tls.ClientSessionCache
-
-	// sessinoKey specifies the value of the hard-coded TLS session cache key.
-	sessionKey string
-}
-
-func WrapClientSessionCache(cache tls.ClientSessionCache, sessionKey string) tls.ClientSessionCache {
-	return &tlsClientSessionCacheWrapper{
-		ClientSessionCache: cache,
-		sessionKey:         sessionKey,
-	}
-}
-
-func (c *tlsClientSessionCacheWrapper) Get(_ string) (session *tls.ClientSessionState, ok bool) {
-	return c.ClientSessionCache.Get(c.sessionKey)
-}
-
-func (c *tlsClientSessionCacheWrapper) Put(_ string, cs *tls.ClientSessionState) {
-	c.ClientSessionCache.Put(c.sessionKey, cs)
-}

+ 9 - 6
psiphon/interrupt_dials_test.go

@@ -83,12 +83,15 @@ func TestInterruptDials(t *testing.T) {
 	}
 	}
 
 
 	makeDialers["TLS"] = func(string) common.Dialer {
 	makeDialers["TLS"] = func(string) common.Dialer {
-		return NewCustomTLSDialer(
-			&CustomTLSConfig{
-				Parameters:               params,
-				Dial:                     NewTCPDialer(&DialConfig{ResolveIP: resolveIP}),
-				RandomizedTLSProfileSeed: seed,
-			})
+		// Cast CustomTLSDialer to common.Dialer.
+		return func(context context.Context, network, addr string) (net.Conn, error) {
+			return NewCustomTLSDialer(
+				&CustomTLSConfig{
+					Parameters:               params,
+					Dial:                     NewTCPDialer(&DialConfig{ResolveIP: resolveIP}),
+					RandomizedTLSProfileSeed: seed,
+				})(context, network, addr)
+		}
 	}
 	}
 
 
 	dialGoroutineFunctionNames := []string{"NewTCPDialer", "NewCustomTLSDialer"}
 	dialGoroutineFunctionNames := []string{"NewTCPDialer", "NewCustomTLSDialer"}

+ 18 - 6
psiphon/meekConn.go

@@ -37,7 +37,6 @@ import (
 	"sync/atomic"
 	"sync/atomic"
 	"time"
 	"time"
 
 
-	tls "github.com/Psiphon-Labs/psiphon-tls"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
@@ -48,7 +47,6 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/transforms"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/transforms"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/values"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/values"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/upstreamproxy"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/upstreamproxy"
-	utls "github.com/refraction-networking/utls"
 	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/net/http2"
 	"golang.org/x/net/http2"
 )
 )
@@ -138,11 +136,11 @@ type MeekConfig struct {
 
 
 	// QUICTLSClientSessionCache specifies the TLS session cache to use
 	// QUICTLSClientSessionCache specifies the TLS session cache to use
 	// for Meek connections that use HTTP/2 over QUIC.
 	// for Meek connections that use HTTP/2 over QUIC.
-	QUICTLSClientSessionCache tls.ClientSessionCache
+	QUICTLSClientSessionCache *common.TlsClientSessionCacheWrapper
 
 
 	// TLSClientSessionCache specifies the TLS session cache to use for
 	// TLSClientSessionCache specifies the TLS session cache to use for
 	// HTTPS (non-QUIC) Meek connections.
 	// HTTPS (non-QUIC) Meek connections.
-	TLSClientSessionCache utls.ClientSessionCache
+	TLSClientSessionCache *common.UtlsClientSessionCacheWrapper
 
 
 	// TLSFragmentClientHello specifies whether to fragment the TLS Client Hello.
 	// TLSFragmentClientHello specifies whether to fragment the TLS Client Hello.
 	TLSFragmentClientHello bool
 	TLSFragmentClientHello bool
@@ -267,6 +265,10 @@ type MeekConn struct {
 	relayWaitGroup            *sync.WaitGroup
 	relayWaitGroup            *sync.WaitGroup
 	firstUnderlyingConn       net.Conn
 	firstUnderlyingConn       net.Conn
 
 
+	// resumedTLSSession reprepsents whether the first underlying TLS connection
+	// was resumed for metrics purposes.
+	resumedTLSSession bool
+
 	// For MeekModeObfuscatedRoundTrip
 	// For MeekModeObfuscatedRoundTrip
 	meekCookieEncryptionPublicKey string
 	meekCookieEncryptionPublicKey string
 	meekObfuscatedKey             string
 	meekObfuscatedKey             string
@@ -560,6 +562,8 @@ func DialMeek(
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
 
 
+		meek.resumedTLSSession = preConn.resumedSession
+
 		cachedTLSDialer = newCachedTLSDialer(preConn, tlsDialer)
 		cachedTLSDialer = newCachedTLSDialer(preConn, tlsDialer)
 
 
 		if IsTLSConnUsingHTTP2(preConn) {
 		if IsTLSConnUsingHTTP2(preConn) {
@@ -815,13 +819,13 @@ func (meek *MeekConn) underlyingDial(ctx context.Context, network, addr string)
 type cachedTLSDialer struct {
 type cachedTLSDialer struct {
 	usedCachedConn int32
 	usedCachedConn int32
 	cachedConn     net.Conn
 	cachedConn     net.Conn
-	dialer         common.Dialer
+	dialer         CustomTLSDialer
 
 
 	mutex      sync.Mutex
 	mutex      sync.Mutex
 	requestCtx context.Context
 	requestCtx context.Context
 }
 }
 
 
-func newCachedTLSDialer(cachedConn net.Conn, dialer common.Dialer) *cachedTLSDialer {
+func newCachedTLSDialer(cachedConn net.Conn, dialer CustomTLSDialer) *cachedTLSDialer {
 	return &cachedTLSDialer{
 	return &cachedTLSDialer{
 		cachedConn: cachedConn,
 		cachedConn: cachedConn,
 		dialer:     dialer,
 		dialer:     dialer,
@@ -929,6 +933,14 @@ func (meek *MeekConn) GetMetrics() common.LogFields {
 	if ok {
 	if ok {
 		logFields.Add(underlyingMetrics.GetMetrics())
 		logFields.Add(underlyingMetrics.GetMetrics())
 	}
 	}
+	if meek.cachedTLSDialer != nil {
+		logFields["resumed_session"] = meek.resumedTLSSession
+	}
+
+	if quicTransport, ok := meek.transport.(*quic.QUICTransporter); ok {
+		logFields.Add(quicTransport.GetMetrics())
+	}
+
 	meek.mutex.Unlock()
 	meek.mutex.Unlock()
 	return logFields
 	return logFields
 }
 }

+ 22 - 4
psiphon/tlsDialer.go

@@ -197,9 +197,16 @@ func (config *CustomTLSConfig) EnableClientSessionCache() {
 	}
 	}
 }
 }
 
 
+type CustomTLSDialer = func(ctx context.Context, network, addr string) (*CustomTLSConn, error)
+
+type CustomTLSConn struct {
+	net.Conn
+	resumedSession bool
+}
+
 // NewCustomTLSDialer creates a new dialer based on CustomTLSDial.
 // NewCustomTLSDialer creates a new dialer based on CustomTLSDial.
-func NewCustomTLSDialer(config *CustomTLSConfig) common.Dialer {
-	return func(ctx context.Context, network, addr string) (net.Conn, error) {
+func NewCustomTLSDialer(config *CustomTLSConfig) CustomTLSDialer {
+	return func(ctx context.Context, network, addr string) (*CustomTLSConn, error) {
 		return CustomTLSDial(ctx, network, addr, config)
 		return CustomTLSDial(ctx, network, addr, config)
 	}
 	}
 }
 }
@@ -211,7 +218,7 @@ func NewCustomTLSDialer(config *CustomTLSConfig) common.Dialer {
 func CustomTLSDial(
 func CustomTLSDial(
 	ctx context.Context,
 	ctx context.Context,
 	network, addr string,
 	network, addr string,
-	config *CustomTLSConfig) (net.Conn, error) {
+	config *CustomTLSConfig) (*CustomTLSConn, error) {
 
 
 	// Note that servers may return a chain which excludes the root CA
 	// Note that servers may return a chain which excludes the root CA
 	// cert https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.2.
 	// cert https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.2.
@@ -436,6 +443,12 @@ func CustomTLSDial(
 	}
 	}
 
 
 	clientSessionCache := config.ClientSessionCache
 	clientSessionCache := config.ClientSessionCache
+	var usedSessionTicket bool
+
+	if wrappedCache, ok := clientSessionCache.(*common.UtlsClientSessionCacheWrapper); ok {
+		// Heuristic to determine if TLS dial is resuming a session.
+		usedSessionTicket = wrappedCache.IsSessionResumptionAvailable()
+	}
 	if clientSessionCache == nil {
 	if clientSessionCache == nil {
 		clientSessionCache = utls.NewLRUClientSessionCache(0)
 		clientSessionCache = utls.NewLRUClientSessionCache(0)
 	}
 	}
@@ -476,6 +489,8 @@ func CustomTLSDial(
 
 
 	if config.ObfuscatedSessionTicketKey != "" && !isTLS13 {
 	if config.ObfuscatedSessionTicketKey != "" && !isTLS13 {
 
 
+		usedSessionTicket = true
+
 		var obfuscatedSessionTicketKey [32]byte
 		var obfuscatedSessionTicketKey [32]byte
 
 
 		key, err := hex.DecodeString(config.ObfuscatedSessionTicketKey)
 		key, err := hex.DecodeString(config.ObfuscatedSessionTicketKey)
@@ -639,7 +654,10 @@ func CustomTLSDial(
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 
 
-	return conn, nil
+	return &CustomTLSConn{
+		Conn:           conn,
+		resumedSession: usedSessionTicket,
+	}, nil
 }
 }
 
 
 func verifyLegacyCertificate(rawCerts [][]byte, expectedCertificate *x509.Certificate) error {
 func verifyLegacyCertificate(rawCerts [][]byte, expectedCertificate *x509.Certificate) error {

+ 1 - 1
psiphon/tlsDialer_test.go

@@ -581,7 +581,7 @@ func testTLSDialerCompatibility(t *testing.T, address string, fragmentClientHell
 			} else {
 			} else {
 
 
 				tlsVersion := ""
 				tlsVersion := ""
-				version := conn.(*utls.UConn).ConnectionState().Version
+				version := conn.Conn.(*utls.UConn).ConnectionState().Version
 				if version == utls.VersionTLS12 {
 				if version == utls.VersionTLS12 {
 					tlsVersion = "TLS 1.2"
 					tlsVersion = "TLS 1.2"
 				} else if version == utls.VersionTLS13 {
 				} else if version == utls.VersionTLS13 {

+ 6 - 3
psiphon/tlsTunnelConn.go

@@ -49,7 +49,8 @@ type TLSTunnelConfig struct {
 // TLSTunnelConn is a network connection that tunnels net.Conn flows over TLS.
 // TLSTunnelConn is a network connection that tunnels net.Conn flows over TLS.
 type TLSTunnelConn struct {
 type TLSTunnelConn struct {
 	net.Conn
 	net.Conn
-	tlsPadding int
+	tlsPadding        int
+	resumedTLSSession bool
 }
 }
 
 
 // DialTLSTunnel returns an initialized tls-tunnel connection.
 // DialTLSTunnel returns an initialized tls-tunnel connection.
@@ -116,8 +117,9 @@ func DialTLSTunnel(
 	}
 	}
 
 
 	return &TLSTunnelConn{
 	return &TLSTunnelConn{
-		Conn:       conn,
-		tlsPadding: tlsPadding,
+		Conn:              conn,
+		tlsPadding:        tlsPadding,
+		resumedTLSSession: conn.resumedSession,
 	}, nil
 	}, nil
 }
 }
 
 
@@ -161,6 +163,7 @@ func (conn *TLSTunnelConn) GetMetrics() common.LogFields {
 	logFields := make(common.LogFields)
 	logFields := make(common.LogFields)
 
 
 	logFields["tls_padding"] = conn.tlsPadding
 	logFields["tls_padding"] = conn.tlsPadding
+	logFields["resumed_session"] = conn.resumedTLSSession
 
 
 	// Include metrics, such as fragmentor metrics, from the underlying dial
 	// Include metrics, such as fragmentor metrics, from the underlying dial
 	// conn. Properties of subsequent underlying dial conns are not reflected
 	// conn. Properties of subsequent underlying dial conns are not reflected