Просмотр исходного кода

Merge pull request #737 from adotkhan/shared-cache

Shared cache
Rod Hynes 8 месяцев назад
Родитель
Сommit
8e55c19493

+ 1 - 1
go.mod

@@ -41,7 +41,7 @@ require (
 	github.com/Psiphon-Labs/goptlib v0.0.0-20200406165125-c0e32a7a3464
 	github.com/Psiphon-Labs/goptlib v0.0.0-20200406165125-c0e32a7a3464
 	github.com/Psiphon-Labs/psiphon-tls v0.0.0-20250318183125-2a2fae2db378
 	github.com/Psiphon-Labs/psiphon-tls v0.0.0-20250318183125-2a2fae2db378
 	github.com/Psiphon-Labs/quic-go v0.0.0-20250527153145-79fe45fb83b1
 	github.com/Psiphon-Labs/quic-go v0.0.0-20250527153145-79fe45fb83b1
-	github.com/Psiphon-Labs/utls v0.0.0-20250429162420-6dbd45ae7ceb
+	github.com/Psiphon-Labs/utls v0.0.0-20250617193811-8e54e1fd2162
 	github.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f
 	github.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f
 	github.com/bifurcation/mint v0.0.0-20180306135233-198357931e61
 	github.com/bifurcation/mint v0.0.0-20180306135233-198357931e61
 	github.com/bits-and-blooms/bloom/v3 v3.6.0
 	github.com/bits-and-blooms/bloom/v3 v3.6.0

+ 2 - 6
go.sum

@@ -24,12 +24,10 @@ github.com/Psiphon-Labs/goptlib v0.0.0-20200406165125-c0e32a7a3464 h1:VmnMMMheFX
 github.com/Psiphon-Labs/goptlib v0.0.0-20200406165125-c0e32a7a3464/go.mod h1:Pe5BqN2DdIdChorAXl6bDaQd/wghpCleJfid2NoSli0=
 github.com/Psiphon-Labs/goptlib v0.0.0-20200406165125-c0e32a7a3464/go.mod h1:Pe5BqN2DdIdChorAXl6bDaQd/wghpCleJfid2NoSli0=
 github.com/Psiphon-Labs/psiphon-tls v0.0.0-20250318183125-2a2fae2db378 h1:LqI8cxnYxgUKLLvv+XZKpxZAQcov6xhEKgC82FdvG/k=
 github.com/Psiphon-Labs/psiphon-tls v0.0.0-20250318183125-2a2fae2db378 h1:LqI8cxnYxgUKLLvv+XZKpxZAQcov6xhEKgC82FdvG/k=
 github.com/Psiphon-Labs/psiphon-tls v0.0.0-20250318183125-2a2fae2db378/go.mod h1:7ZUnPnWT5z8J8hxfsVjKHYK77Zme/Y0If1b/zeziiJs=
 github.com/Psiphon-Labs/psiphon-tls v0.0.0-20250318183125-2a2fae2db378/go.mod h1:7ZUnPnWT5z8J8hxfsVjKHYK77Zme/Y0If1b/zeziiJs=
-github.com/Psiphon-Labs/quic-go v0.0.0-20250325201346-c58235406399 h1:FuT4mr/LzJC0KVgTDnFCKgWdkftqIHxjb75B39M2Rbg=
-github.com/Psiphon-Labs/quic-go v0.0.0-20250325201346-c58235406399/go.mod h1:rONdWgPMbFjyyBai7gB1IBF4pT9r4l0GyiDst5XR1SY=
 github.com/Psiphon-Labs/quic-go v0.0.0-20250527153145-79fe45fb83b1 h1:zD7JvZCV8gjvtI0AZmE81Ffc/v7A+qwU1/YfUmN/Flk=
 github.com/Psiphon-Labs/quic-go v0.0.0-20250527153145-79fe45fb83b1 h1:zD7JvZCV8gjvtI0AZmE81Ffc/v7A+qwU1/YfUmN/Flk=
 github.com/Psiphon-Labs/quic-go v0.0.0-20250527153145-79fe45fb83b1/go.mod h1:rONdWgPMbFjyyBai7gB1IBF4pT9r4l0GyiDst5XR1SY=
 github.com/Psiphon-Labs/quic-go v0.0.0-20250527153145-79fe45fb83b1/go.mod h1:rONdWgPMbFjyyBai7gB1IBF4pT9r4l0GyiDst5XR1SY=
-github.com/Psiphon-Labs/utls v0.0.0-20250429162420-6dbd45ae7ceb h1:6q4bNLmVD8WtgwqR6w2VPW9dJIMT4yhbez/XuLT+7ac=
-github.com/Psiphon-Labs/utls v0.0.0-20250429162420-6dbd45ae7ceb/go.mod h1:1vv0gVAzq9e2XYkW8HAKrmtuuZrBdDixQFx5H22KAjI=
+github.com/Psiphon-Labs/utls v0.0.0-20250617193811-8e54e1fd2162 h1:j4UAddx21+WL7Koiy+v+XVj64gP0eyGai8Pc2e2pU6E=
+github.com/Psiphon-Labs/utls v0.0.0-20250617193811-8e54e1fd2162/go.mod h1:1vv0gVAzq9e2XYkW8HAKrmtuuZrBdDixQFx5H22KAjI=
 github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
 github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
 github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
 github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
 github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
 github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
@@ -287,8 +285,6 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y
 github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
 github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
 github.com/wader/filtertransport v0.0.0-20200316221534-bdd9e61eee78 h1:9sreu9e9KOihf2Y0NbpyfWhd1XFDcL4GTkPYL4IvMrg=
 github.com/wader/filtertransport v0.0.0-20200316221534-bdd9e61eee78 h1:9sreu9e9KOihf2Y0NbpyfWhd1XFDcL4GTkPYL4IvMrg=
 github.com/wader/filtertransport v0.0.0-20200316221534-bdd9e61eee78/go.mod h1:HazXTRLhXFyq80TQp7PUXi6BKE6mS+ydEdzEqNBKopQ=
 github.com/wader/filtertransport v0.0.0-20200316221534-bdd9e61eee78/go.mod h1:HazXTRLhXFyq80TQp7PUXi6BKE6mS+ydEdzEqNBKopQ=
-github.com/wlynxg/anet v0.0.1 h1:VbkEEgHxPSrRQSiyRd0pmrbcEQAEU2TTb8fb4DmSYoQ=
-github.com/wlynxg/anet v0.0.1/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
 github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
 github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
 github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
 github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=

+ 2 - 2
psiphon/common/quic/quic.go

@@ -1304,8 +1304,8 @@ func dialQUIC(
 
 
 		metrics := quicClientConnMetrics{
 		metrics := quicClientConnMetrics{
 			dialEarly:           dialEarly,
 			dialEarly:           dialEarly,
-			tlsClientSentTicket: dialConnection.ConnectionState().TLS.DidResume,
-			tlsDidResume:        dialConnection.TLSConnectionMetrics().ClientSentTicket,
+			tlsClientSentTicket: dialConnection.TLSConnectionMetrics().ClientSentTicket,
+			tlsDidResume:        dialConnection.ConnectionState().TLS.DidResume,
 			obfuscatedPSK:       obfuscatedPSKKey != "",
 			obfuscatedPSK:       obfuscatedPSKKey != "",
 		}
 		}
 
 

+ 53 - 9
psiphon/common/tlsCache.go

@@ -24,9 +24,10 @@ import (
 	utls "github.com/Psiphon-Labs/utls"
 	utls "github.com/Psiphon-Labs/utls"
 )
 )
 
 
+const TLS_NULL_SESSION_KEY = ""
+
 // TLSClientSessionCacheWrapper is a wrapper around tls.ClientSessionCache
 // TLSClientSessionCacheWrapper is a wrapper around tls.ClientSessionCache
 // that provides a hard-coded key for the cache.
 // that provides a hard-coded key for the cache.
-// It implements the TLSClientSessionCacheWrapper interface.
 type TLSClientSessionCacheWrapper struct {
 type TLSClientSessionCacheWrapper struct {
 	tls.ClientSessionCache
 	tls.ClientSessionCache
 
 
@@ -34,32 +35,54 @@ type TLSClientSessionCacheWrapper struct {
 	sessionKey string
 	sessionKey string
 }
 }
 
 
-// WrapClientSessionCache wraps a tls.ClientSessionCache with an alternative
-// key, ignoring the SNI-based key that crypto/tls passes to Put/Get, which
-// may be incompatible with SNI obfuscation transforms.
+// WrapUtlsClientSessionCache wraps a tls.ClientSessionCache with an alternative
+// hard-coded session key, ignoring the SNI-based key that crypto/tls passes to Put/Get,
+// which may be incompatible with the SNI obfuscation transforms.
+// If the sessionKey is empty (TLS_NULL_SESSION_KEY), SetSessionKey has to be called
+// before using the cache.
 func WrapClientSessionCache(
 func WrapClientSessionCache(
 	cache tls.ClientSessionCache,
 	cache tls.ClientSessionCache,
 	hardCodedSessionKey string,
 	hardCodedSessionKey string,
 ) *TLSClientSessionCacheWrapper {
 ) *TLSClientSessionCacheWrapper {
-
 	return &TLSClientSessionCacheWrapper{
 	return &TLSClientSessionCacheWrapper{
 		ClientSessionCache: cache,
 		ClientSessionCache: cache,
 		sessionKey:         hardCodedSessionKey,
 		sessionKey:         hardCodedSessionKey,
 	}
 	}
 }
 }
 
 
+// Get retrieves the session from the cache using the hard-coded session key.
 func (c *TLSClientSessionCacheWrapper) Get(_ string) (session *tls.ClientSessionState, ok bool) {
 func (c *TLSClientSessionCacheWrapper) Get(_ string) (session *tls.ClientSessionState, ok bool) {
+	if c.sessionKey == "" {
+		return nil, false
+	}
 	return c.ClientSessionCache.Get(c.sessionKey)
 	return c.ClientSessionCache.Get(c.sessionKey)
 }
 }
 
 
+// Put stores the session in the cache using the hard-coded session key.
 func (c *TLSClientSessionCacheWrapper) Put(_ string, cs *tls.ClientSessionState) {
 func (c *TLSClientSessionCacheWrapper) Put(_ string, cs *tls.ClientSessionState) {
+	if c.sessionKey == "" {
+		return
+	}
+	cs.ResumptionState()
 	c.ClientSessionCache.Put(c.sessionKey, cs)
 	c.ClientSessionCache.Put(c.sessionKey, cs)
 }
 }
 
 
+// RemoveCacheEntry removes the cache entry for the hard-coded session key.
 func (c *TLSClientSessionCacheWrapper) RemoveCacheEntry() {
 func (c *TLSClientSessionCacheWrapper) RemoveCacheEntry() {
+	if c.sessionKey == "" {
+		return
+	}
 	c.ClientSessionCache.Put(c.sessionKey, nil)
 	c.ClientSessionCache.Put(c.sessionKey, nil)
 }
 }
 
 
+// SetSessionKey sets the hard-coded session key if not already set.
+func (c *TLSClientSessionCacheWrapper) SetSessionKey(key string) {
+	if c.sessionKey != TLS_NULL_SESSION_KEY {
+		return
+	}
+	c.sessionKey = key
+}
+
 // UtlClientSessionCacheWrapper is a wrapper around utls.ClientSessionCache
 // UtlClientSessionCacheWrapper is a wrapper around utls.ClientSessionCache
 // that provides a hard-coded key for the cache.
 // that provides a hard-coded key for the cache.
 // It implements the TLSClientSessionCacheWrapper interface.
 // It implements the TLSClientSessionCacheWrapper interface.
@@ -71,27 +94,48 @@ type UtlsClientSessionCacheWrapper struct {
 }
 }
 
 
 // WrapUtlsClientSessionCache wraps a utls.ClientSessionCache with an alternative
 // WrapUtlsClientSessionCache wraps a utls.ClientSessionCache with an alternative
-// key, ignoring the SNI-based key that crypto/tls passes to Put/Get, which
-// may be incompatible with SNI obfuscation transforms.
+// hard-coded session key, ignoring the SNI-based key that crypto/tls passes to Put/Get,
+// which may be incompatible with the SNI obfuscation transforms.
+// If the sessionKey is empty (TLS_NULL_SESSION_KEY), SetSessionKey has to be called
+// before using the cache.
 func WrapUtlsClientSessionCache(
 func WrapUtlsClientSessionCache(
 	cache utls.ClientSessionCache,
 	cache utls.ClientSessionCache,
 	hardCodedSessionKey string,
 	hardCodedSessionKey string,
 ) *UtlsClientSessionCacheWrapper {
 ) *UtlsClientSessionCacheWrapper {
-
 	return &UtlsClientSessionCacheWrapper{
 	return &UtlsClientSessionCacheWrapper{
 		ClientSessionCache: cache,
 		ClientSessionCache: cache,
 		sessionKey:         hardCodedSessionKey,
 		sessionKey:         hardCodedSessionKey,
 	}
 	}
 }
 }
 
 
+// Get retrieves the session from the cache using the hard-coded session key.
 func (c *UtlsClientSessionCacheWrapper) Get(_ string) (session *utls.ClientSessionState, ok bool) {
 func (c *UtlsClientSessionCacheWrapper) Get(_ string) (session *utls.ClientSessionState, ok bool) {
+	if c.sessionKey == "" {
+		return nil, false
+	}
 	return c.ClientSessionCache.Get(c.sessionKey)
 	return c.ClientSessionCache.Get(c.sessionKey)
 }
 }
 
 
+// Put stores the session in the cache using the hard-coded session key.
 func (c *UtlsClientSessionCacheWrapper) Put(_ string, cs *utls.ClientSessionState) {
 func (c *UtlsClientSessionCacheWrapper) Put(_ string, cs *utls.ClientSessionState) {
+	if c.sessionKey == "" {
+		return
+	}
 	c.ClientSessionCache.Put(c.sessionKey, cs)
 	c.ClientSessionCache.Put(c.sessionKey, cs)
 }
 }
 
 
+// RemoveCacheEntry removes the cache entry for the hard-coded session key.
 func (c *UtlsClientSessionCacheWrapper) RemoveCacheEntry() {
 func (c *UtlsClientSessionCacheWrapper) RemoveCacheEntry() {
-	c.ClientSessionCache.Put(c.sessionKey, nil)
+	if c.sessionKey != "" {
+		c.ClientSessionCache.Put(c.sessionKey, nil)
+	}
+}
+
+// SetSessionKey sets the hard-coded session key if not already set.
+// If the session key is already set, it does nothing.
+func (c *UtlsClientSessionCacheWrapper) SetSessionKey(key string) {
+	if c.sessionKey != TLS_NULL_SESSION_KEY {
+		return
+	}
+	c.sessionKey = key
 }
 }

+ 7 - 5
psiphon/controller.go

@@ -275,14 +275,14 @@ func NewController(config *Config) (controller *Controller, err error) {
 	var tacticAppliedReceivers []TacticsAppliedReceiver
 	var tacticAppliedReceivers []TacticsAppliedReceiver
 
 
 	isProxy := false
 	isProxy := false
-	controller.inproxyClientBrokerClientManager = NewInproxyBrokerClientManager(config, isProxy)
+	controller.inproxyClientBrokerClientManager = NewInproxyBrokerClientManager(config, isProxy, controller.tlsClientSessionCache)
 	tacticAppliedReceivers = append(tacticAppliedReceivers, controller.inproxyClientBrokerClientManager)
 	tacticAppliedReceivers = append(tacticAppliedReceivers, controller.inproxyClientBrokerClientManager)
 	controller.inproxyNATStateManager = NewInproxyNATStateManager(config)
 	controller.inproxyNATStateManager = NewInproxyNATStateManager(config)
 	tacticAppliedReceivers = append(tacticAppliedReceivers, controller.inproxyNATStateManager)
 	tacticAppliedReceivers = append(tacticAppliedReceivers, controller.inproxyNATStateManager)
 
 
 	if config.InproxyEnableProxy {
 	if config.InproxyEnableProxy {
 		isProxy = true
 		isProxy = true
-		controller.inproxyProxyBrokerClientManager = NewInproxyBrokerClientManager(config, isProxy)
+		controller.inproxyProxyBrokerClientManager = NewInproxyBrokerClientManager(config, isProxy, controller.tlsClientSessionCache)
 		tacticAppliedReceivers = append(tacticAppliedReceivers, controller.inproxyProxyBrokerClientManager)
 		tacticAppliedReceivers = append(tacticAppliedReceivers, controller.inproxyProxyBrokerClientManager)
 	}
 	}
 
 
@@ -632,7 +632,8 @@ fetcherLoop:
 				controller.config,
 				controller.config,
 				attempt,
 				attempt,
 				tunnel,
 				tunnel,
-				controller.untunneledDialConfig)
+				controller.untunneledDialConfig,
+				controller.tlsClientSessionCache)
 
 
 			if err == nil {
 			if err == nil {
 				lastFetchTime = time.Now()
 				lastFetchTime = time.Now()
@@ -721,7 +722,8 @@ downloadLoop:
 				attempt,
 				attempt,
 				handshakeVersion,
 				handshakeVersion,
 				tunnel,
 				tunnel,
-				controller.untunneledDialConfig)
+				controller.untunneledDialConfig,
+				controller.tlsClientSessionCache)
 
 
 			if err == nil {
 			if err == nil {
 				lastDownloadTime = time.Now()
 				lastDownloadTime = time.Now()
@@ -3036,7 +3038,7 @@ func (controller *Controller) runInproxyProxy() {
 		if useUpstreamProxy {
 		if useUpstreamProxy {
 			NoticeError("inproxy proxy: not run due to upstream proxy configuration")
 			NoticeError("inproxy proxy: not run due to upstream proxy configuration")
 		}
 		}
-		if haveBrokerSpecs {
+		if !haveBrokerSpecs {
 			NoticeError("inproxy proxy: no proxy broker specs")
 			NoticeError("inproxy proxy: no proxy broker specs")
 		}
 		}
 		if !inproxy.Enabled() {
 		if !inproxy.Enabled() {

+ 12 - 3
psiphon/dialParameters.go

@@ -758,10 +758,19 @@ func MakeDialParameters(
 		protocol.TunnelProtocolUsesTLSOSSH(dialParams.TunnelProtocol) ||
 		protocol.TunnelProtocolUsesTLSOSSH(dialParams.TunnelProtocol) ||
 		dialParams.ConjureAPIRegistration
 		dialParams.ConjureAPIRegistration
 
 
+	// Note that ConjureAPIRegistartion is not wired to use the TLS session cache.
 	if tlsClientSessionCache != nil && usingTLS {
 	if tlsClientSessionCache != nil && usingTLS {
-		sessionKey, err := serverEntry.GetTLSSessionCacheKeyAddress(dialParams.TunnelProtocol)
-		if err != nil {
-			return nil, errors.Trace(err)
+
+		var sessionKey string
+		if protocol.TunnelProtocolUsesFrontedMeek(dialParams.TunnelProtocol) {
+			// UsesMeekHTTPS and UsesFrontedMeek
+			// Special case: the session key is the resolved IP address of the CDN edge at dial time.
+			sessionKey = common.TLS_NULL_SESSION_KEY
+		} else {
+			sessionKey, err = serverEntry.GetTLSSessionCacheKeyAddress(dialParams.TunnelProtocol)
+			if err != nil {
+				return nil, errors.Trace(err)
+			}
 		}
 		}
 
 
 		dialParams.tlsClientSessionCache = common.WrapUtlsClientSessionCache(tlsClientSessionCache, sessionKey)
 		dialParams.tlsClientSessionCache = common.WrapUtlsClientSessionCache(tlsClientSessionCache, sessionKey)

+ 1 - 0
psiphon/feedback.go

@@ -231,6 +231,7 @@ func SendFeedback(ctx context.Context, config *Config, diagnostics, uploadPath s
 			feedbackUploadCtx,
 			feedbackUploadCtx,
 			config,
 			config,
 			dialConfig,
 			dialConfig,
+			nil,
 			uploadURL.SkipVerify,
 			uploadURL.SkipVerify,
 			config.DisableSystemRootCAs,
 			config.DisableSystemRootCAs,
 			payloadSecure,
 			payloadSecure,

+ 14 - 6
psiphon/frontedHTTP.go

@@ -14,6 +14,7 @@ import (
 	"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/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
+	utls "github.com/Psiphon-Labs/utls"
 	"github.com/cespare/xxhash"
 	"github.com/cespare/xxhash"
 )
 )
 
 
@@ -44,6 +45,7 @@ func newFrontedHTTPClientInstance(
 	skipVerify,
 	skipVerify,
 	disableSystemRootCAs,
 	disableSystemRootCAs,
 	payloadSecure bool,
 	payloadSecure bool,
+	tlsCache utls.ClientSessionCache,
 ) (*frontedHTTPClientInstance, error) {
 ) (*frontedHTTPClientInstance, error) {
 
 
 	if len(frontingSpecs) == 0 {
 	if len(frontingSpecs) == 0 {
@@ -127,7 +129,8 @@ func newFrontedHTTPClientInstance(
 			useDeviceBinder,
 			useDeviceBinder,
 			skipVerify,
 			skipVerify,
 			disableSystemRootCAs,
 			disableSystemRootCAs,
-			payloadSecure)
+			payloadSecure,
+			tlsCache)
 		if err != nil {
 		if err != nil {
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
@@ -140,7 +143,8 @@ func newFrontedHTTPClientInstance(
 			useDeviceBinder,
 			useDeviceBinder,
 			skipVerify,
 			skipVerify,
 			disableSystemRootCAs,
 			disableSystemRootCAs,
-			payloadSecure)
+			payloadSecure,
+			tlsCache)
 		if err != nil {
 		if err != nil {
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
@@ -362,7 +366,8 @@ func makeFrontedHTTPDialParameters(
 	useDeviceBinder,
 	useDeviceBinder,
 	skipVerify,
 	skipVerify,
 	disableSystemRootCAs,
 	disableSystemRootCAs,
-	payloadSecure bool) (*frontedHTTPDialParameters, error) {
+	payloadSecure bool,
+	tlsCache utls.ClientSessionCache) (*frontedHTTPDialParameters, error) {
 
 
 	currentTimestamp := time.Now()
 	currentTimestamp := time.Now()
 
 
@@ -382,6 +387,7 @@ func makeFrontedHTTPDialParameters(
 		skipVerify,
 		skipVerify,
 		disableSystemRootCAs,
 		disableSystemRootCAs,
 		payloadSecure,
 		payloadSecure,
+		tlsCache,
 	)
 	)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
@@ -397,7 +403,8 @@ func makeFrontedHTTPDialParameters(
 		skipVerify,
 		skipVerify,
 		disableSystemRootCAs,
 		disableSystemRootCAs,
 		useDeviceBinder,
 		useDeviceBinder,
-		payloadSecure)
+		payloadSecure,
+		tlsCache)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
@@ -414,7 +421,8 @@ func (dialParams *frontedHTTPDialParameters) prepareDialConfigs(
 	useDeviceBinder,
 	useDeviceBinder,
 	skipVerify,
 	skipVerify,
 	disableSystemRootCAs,
 	disableSystemRootCAs,
-	payloadSecure bool) error {
+	payloadSecure bool,
+	tlsCache utls.ClientSessionCache) error {
 
 
 	dialParams.isReplay = isReplay
 	dialParams.isReplay = isReplay
 
 
@@ -424,7 +432,7 @@ func (dialParams *frontedHTTPDialParameters) prepareDialConfigs(
 
 
 		err := dialParams.FrontedMeekDialParameters.prepareDialConfigs(
 		err := dialParams.FrontedMeekDialParameters.prepareDialConfigs(
 			config, p, tunnel, nil, useDeviceBinder, skipVerify,
 			config, p, tunnel, nil, useDeviceBinder, skipVerify,
-			disableSystemRootCAs, payloadSecure)
+			disableSystemRootCAs, payloadSecure, tlsCache)
 		if err != nil {
 		if err != nil {
 			return errors.Trace(err)
 			return errors.Trace(err)
 		}
 		}

+ 6 - 3
psiphon/frontedHTTP_test.go

@@ -31,6 +31,7 @@ import (
 
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
+	utls "github.com/Psiphon-Labs/utls"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
 )
 )
 
 
@@ -123,13 +124,15 @@ func TestFrontedHTTPClientInstance(t *testing.T) {
 
 
 	// Make fronted HTTP client instance
 	// Make fronted HTTP client instance
 
 
+	tlsCache := utls.NewLRUClientSessionCache(0)
+
 	// TODO: test that replay is disabled when there is a tunnel
 	// TODO: test that replay is disabled when there is a tunnel
 	var tunnel *Tunnel = nil
 	var tunnel *Tunnel = nil
 	useDeviceBinder := true
 	useDeviceBinder := true
 	skipVerify := false
 	skipVerify := false
 	payloadSecure := true
 	payloadSecure := true
 	client, err := newFrontedHTTPClientInstance(
 	client, err := newFrontedHTTPClientInstance(
-		config, tunnel, frontingSpecs, nil, useDeviceBinder, skipVerify, config.DisableSystemRootCAs, payloadSecure)
+		config, tunnel, frontingSpecs, nil, useDeviceBinder, skipVerify, config.DisableSystemRootCAs, payloadSecure, tlsCache)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("newFrontedHTTPClientInstance failed: %s", err)
 		t.Fatalf("newFrontedHTTPClientInstance failed: %s", err)
 	}
 	}
@@ -140,7 +143,7 @@ func TestFrontedHTTPClientInstance(t *testing.T) {
 	prevClient := client
 	prevClient := client
 
 
 	client, err = newFrontedHTTPClientInstance(
 	client, err = newFrontedHTTPClientInstance(
-		config, tunnel, frontingSpecs, nil, useDeviceBinder, skipVerify, config.DisableSystemRootCAs, payloadSecure)
+		config, tunnel, frontingSpecs, nil, useDeviceBinder, skipVerify, config.DisableSystemRootCAs, payloadSecure, tlsCache)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("newFrontedHTTPClientInstance failed: %s", err)
 		t.Fatalf("newFrontedHTTPClientInstance failed: %s", err)
 	}
 	}
@@ -161,7 +164,7 @@ func TestFrontedHTTPClientInstance(t *testing.T) {
 
 
 	client, err = newFrontedHTTPClientInstance(
 	client, err = newFrontedHTTPClientInstance(
 		config, tunnel, frontingSpecs, nil, useDeviceBinder, skipVerify,
 		config, tunnel, frontingSpecs, nil, useDeviceBinder, skipVerify,
-		config.DisableSystemRootCAs, payloadSecure)
+		config.DisableSystemRootCAs, payloadSecure, tlsCache)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("newFrontedHTTPClientInstance failed: %s", err)
 		t.Fatalf("newFrontedHTTPClientInstance failed: %s", err)
 	}
 	}

+ 8 - 10
psiphon/frontingDialParameters.go

@@ -93,7 +93,8 @@ func makeFrontedMeekDialParameters(
 	useDeviceBinder,
 	useDeviceBinder,
 	skipVerify,
 	skipVerify,
 	disableSystemRootCAs,
 	disableSystemRootCAs,
-	payloadSecure bool) (*FrontedMeekDialParameters, error) {
+	payloadSecure bool,
+	tlsCache utls.ClientSessionCache) (*FrontedMeekDialParameters, error) {
 
 
 	// This function duplicates some code from MakeDialParameters. To simplify
 	// This function duplicates some code from MakeDialParameters. To simplify
 	// the logic, the Replay<Component> tactic flags for individual dial
 	// the logic, the Replay<Component> tactic flags for individual dial
@@ -266,7 +267,7 @@ func makeFrontedMeekDialParameters(
 
 
 	err = frontedMeekDialParams.prepareDialConfigs(
 	err = frontedMeekDialParams.prepareDialConfigs(
 		config, p, tunnel, dialCustomHeaders, useDeviceBinder, skipVerify,
 		config, p, tunnel, dialCustomHeaders, useDeviceBinder, skipVerify,
-		disableSystemRootCAs, payloadSecure)
+		disableSystemRootCAs, payloadSecure, tlsCache)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
@@ -283,7 +284,8 @@ func (f *FrontedMeekDialParameters) prepareDialConfigs(
 	useDeviceBinder,
 	useDeviceBinder,
 	skipVerify,
 	skipVerify,
 	disableSystemRootCAs,
 	disableSystemRootCAs,
-	payloadSecure bool) error {
+	payloadSecure bool,
+	tlsCache utls.ClientSessionCache) error {
 
 
 	if !payloadSecure && (skipVerify || disableSystemRootCAs) {
 	if !payloadSecure && (skipVerify || disableSystemRootCAs) {
 		return errors.TraceNew("cannot skip certificate verification if payload insecure")
 		return errors.TraceNew("cannot skip certificate verification if payload insecure")
@@ -404,13 +406,9 @@ func (f *FrontedMeekDialParameters) prepareDialConfigs(
 		ClientTunnelProtocol:     equivilentTunnelProtocol,
 		ClientTunnelProtocol:     equivilentTunnelProtocol,
 		NetworkLatencyMultiplier: f.NetworkLatencyMultiplier,
 		NetworkLatencyMultiplier: f.NetworkLatencyMultiplier,
 		AdditionalHeaders:        config.MeekAdditionalHeaders,
 		AdditionalHeaders:        config.MeekAdditionalHeaders,
-		// TODO: Change hard-coded session key be something like FrontingProviderID + BrokerID.
-		// This is necessary once longer-term TLS caches are added.
-		// The meek dial address, based on the fronting dial address returned by
-		// parameters.FrontingSpecs.SelectParameters has couple of issues. For some providers there's
-		// only a couple or even just one possible value, in other cases there are millions of possible values
-		// and cached values won't be used as often as they ought to be.
-		TLSClientSessionCache: common.WrapUtlsClientSessionCache(utls.NewLRUClientSessionCache(0), f.DialAddress),
+
+		// CustomTLSDial will use the resolved IP address as the session key.
+		TLSClientSessionCache: common.WrapUtlsClientSessionCache(tlsCache, common.TLS_NULL_SESSION_KEY),
 	}
 	}
 
 
 	if !skipVerify {
 	if !skipVerify {

+ 26 - 10
psiphon/inproxy.go

@@ -41,6 +41,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/resolver"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/resolver"
+	utls "github.com/Psiphon-Labs/utls"
 	"github.com/cespare/xxhash"
 	"github.com/cespare/xxhash"
 )
 )
 
 
@@ -70,6 +71,8 @@ type InproxyBrokerClientManager struct {
 	config  *Config
 	config  *Config
 	isProxy bool
 	isProxy bool
 
 
+	tlsCache utls.ClientSessionCache
+
 	mutex                sync.Mutex
 	mutex                sync.Mutex
 	brokerSelectCount    int
 	brokerSelectCount    int
 	networkID            string
 	networkID            string
@@ -81,11 +84,12 @@ type InproxyBrokerClientManager struct {
 // managed InproxyBrokerClientInstance is initialized when used for a round
 // managed InproxyBrokerClientInstance is initialized when used for a round
 // trip.
 // trip.
 func NewInproxyBrokerClientManager(
 func NewInproxyBrokerClientManager(
-	config *Config, isProxy bool) *InproxyBrokerClientManager {
+	config *Config, isProxy bool, tlsCache utls.ClientSessionCache) *InproxyBrokerClientManager {
 
 
 	b := &InproxyBrokerClientManager{
 	b := &InproxyBrokerClientManager{
-		config:  config,
-		isProxy: isProxy,
+		config:   config,
+		isProxy:  isProxy,
+		tlsCache: tlsCache,
 	}
 	}
 
 
 	// b.brokerClientInstance is initialized on demand, when getBrokerClient
 	// b.brokerClientInstance is initialized on demand, when getBrokerClient
@@ -498,13 +502,13 @@ func NewInproxyBrokerClientInstance(
 	}
 	}
 
 
 	if !isReplay {
 	if !isReplay {
-		brokerDialParams, err = MakeInproxyBrokerDialParameters(config, p, networkID, brokerSpec)
+		brokerDialParams, err = MakeInproxyBrokerDialParameters(config, p, networkID, brokerSpec, brokerClientManager.tlsCache)
 		if err != nil {
 		if err != nil {
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
 	} else {
 	} else {
 		brokerDialParams.brokerSpec = brokerSpec
 		brokerDialParams.brokerSpec = brokerSpec
-		err := brokerDialParams.prepareDialConfigs(config, p, true)
+		err := brokerDialParams.prepareDialConfigs(config, p, true, brokerClientManager.tlsCache)
 		if err != nil {
 		if err != nil {
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
@@ -1036,6 +1040,13 @@ func (b *InproxyBrokerClientInstance) BrokerClientRoundTripperFailed(roundTrippe
 		}
 		}
 	}
 	}
 
 
+	// Remove the TLS session cache entry for the broker's fronting dial address, if present.
+	// This ensures that the next round trip establishes a new TLS session, avoiding potential issues
+	// caused by session resumption fingerprint that may have contributed to the round tripper failure.
+	if hardcodedCache := b.brokerDialParams.FrontedHTTPDialParameters.meekConfig.TLSClientSessionCache; hardcodedCache != nil {
+		hardcodedCache.RemoveCacheEntry()
+	}
+
 	// Invoke resetBrokerClientOnRoundTripperFailed to signal the
 	// Invoke resetBrokerClientOnRoundTripperFailed to signal the
 	// InproxyBrokerClientManager to create a new
 	// InproxyBrokerClientManager to create a new
 	// InproxyBrokerClientInstance, with new dial parameters and a new round
 	// InproxyBrokerClientInstance, with new dial parameters and a new round
@@ -1161,7 +1172,8 @@ func MakeInproxyBrokerDialParameters(
 	config *Config,
 	config *Config,
 	p parameters.ParametersAccessor,
 	p parameters.ParametersAccessor,
 	networkID string,
 	networkID string,
-	brokerSpec *parameters.InproxyBrokerSpec) (*InproxyBrokerDialParameters, error) {
+	brokerSpec *parameters.InproxyBrokerSpec,
+	tlsCache utls.ClientSessionCache) (*InproxyBrokerDialParameters, error) {
 
 
 	if config.UseUpstreamProxy() {
 	if config.UseUpstreamProxy() {
 		return nil, errors.TraceNew("upstream proxy unsupported")
 		return nil, errors.TraceNew("upstream proxy unsupported")
@@ -1196,7 +1208,9 @@ func MakeInproxyBrokerDialParameters(
 		true,
 		true,
 		skipVerify,
 		skipVerify,
 		config.DisableSystemRootCAs,
 		config.DisableSystemRootCAs,
-		payloadSecure)
+		payloadSecure,
+		tlsCache,
+	)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
@@ -1206,7 +1220,8 @@ func MakeInproxyBrokerDialParameters(
 	err = brokerDialParams.prepareDialConfigs(
 	err = brokerDialParams.prepareDialConfigs(
 		config,
 		config,
 		p,
 		p,
-		false)
+		false,
+		tlsCache)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
@@ -1218,7 +1233,8 @@ func MakeInproxyBrokerDialParameters(
 func (brokerDialParams *InproxyBrokerDialParameters) prepareDialConfigs(
 func (brokerDialParams *InproxyBrokerDialParameters) prepareDialConfigs(
 	config *Config,
 	config *Config,
 	p parameters.ParametersAccessor,
 	p parameters.ParametersAccessor,
-	isReplay bool) error {
+	isReplay bool,
+	tlsCache utls.ClientSessionCache) error {
 
 
 	brokerDialParams.isReplay = isReplay
 	brokerDialParams.isReplay = isReplay
 
 
@@ -1237,7 +1253,7 @@ func (brokerDialParams *InproxyBrokerDialParameters) prepareDialConfigs(
 
 
 		err := brokerDialParams.FrontedHTTPDialParameters.prepareDialConfigs(
 		err := brokerDialParams.FrontedHTTPDialParameters.prepareDialConfigs(
 			config, p, nil, nil, true, skipVerify,
 			config, p, nil, nil, true, skipVerify,
-			config.DisableSystemRootCAs, payloadSecure)
+			config.DisableSystemRootCAs, payloadSecure, tlsCache)
 		if err != nil {
 		if err != nil {
 			return errors.Trace(err)
 			return errors.Trace(err)
 		}
 		}

+ 9 - 6
psiphon/inproxy_test.go

@@ -31,6 +31,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/resolver"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/resolver"
+	utls "github.com/Psiphon-Labs/utls"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
 )
 )
 
 
@@ -110,7 +111,9 @@ func runInproxyBrokerDialParametersTest(t *testing.T) error {
 	}
 	}
 	defer CloseDataStore()
 	defer CloseDataStore()
 
 
-	manager := NewInproxyBrokerClientManager(config, isProxy)
+	tlsCache := utls.NewLRUClientSessionCache(0)
+
+	manager := NewInproxyBrokerClientManager(config, isProxy, tlsCache)
 
 
 	// Test: no broker specs
 	// Test: no broker specs
 
 
@@ -136,7 +139,7 @@ func runInproxyBrokerDialParametersTest(t *testing.T) error {
 	}
 	}
 	config.SetResolver(resolver.NewResolver(&resolver.NetworkConfig{}, networkID))
 	config.SetResolver(resolver.NewResolver(&resolver.NetworkConfig{}, networkID))
 
 
-	manager = NewInproxyBrokerClientManager(config, isProxy)
+	manager = NewInproxyBrokerClientManager(config, isProxy, tlsCache)
 
 
 	brokerClient, brokerDialParams, err := manager.GetBrokerClient(networkID)
 	brokerClient, brokerDialParams, err := manager.GetBrokerClient(networkID)
 	if err != nil {
 	if err != nil {
@@ -170,7 +173,7 @@ func runInproxyBrokerDialParametersTest(t *testing.T) error {
 
 
 	brokerClient.GetBrokerDialCoordinator().BrokerClientRoundTripperSucceeded(roundTripper)
 	brokerClient.GetBrokerDialCoordinator().BrokerClientRoundTripperSucceeded(roundTripper)
 
 
-	manager = NewInproxyBrokerClientManager(config, isProxy)
+	manager = NewInproxyBrokerClientManager(config, isProxy, tlsCache)
 
 
 	brokerClient, brokerDialParams, err = manager.GetBrokerClient(networkID)
 	brokerClient, brokerDialParams, err = manager.GetBrokerClient(networkID)
 	if err != nil {
 	if err != nil {
@@ -247,7 +250,7 @@ func runInproxyBrokerDialParametersTest(t *testing.T) error {
 
 
 	brokerClient.GetBrokerDialCoordinator().BrokerClientRoundTripperFailed(roundTripper)
 	brokerClient.GetBrokerDialCoordinator().BrokerClientRoundTripperFailed(roundTripper)
 
 
-	manager = NewInproxyBrokerClientManager(config, isProxy)
+	manager = NewInproxyBrokerClientManager(config, isProxy, tlsCache)
 
 
 	brokerClient, brokerDialParams, err = manager.GetBrokerClient(networkID)
 	brokerClient, brokerDialParams, err = manager.GetBrokerClient(networkID)
 	if err != nil {
 	if err != nil {
@@ -269,7 +272,7 @@ func runInproxyBrokerDialParametersTest(t *testing.T) error {
 	config.InproxyClientPersonalCompartmentID = personalCompartmentID.String()
 	config.InproxyClientPersonalCompartmentID = personalCompartmentID.String()
 	config.InproxyProxyPersonalCompartmentID = personalCompartmentID.String()
 	config.InproxyProxyPersonalCompartmentID = personalCompartmentID.String()
 
 
-	manager = NewInproxyBrokerClientManager(config, isProxy)
+	manager = NewInproxyBrokerClientManager(config, isProxy, tlsCache)
 
 
 	brokerClient, brokerDialParams, err = manager.GetBrokerClient(networkID)
 	brokerClient, brokerDialParams, err = manager.GetBrokerClient(networkID)
 	if err != nil {
 	if err != nil {
@@ -298,7 +301,7 @@ func runInproxyBrokerDialParametersTest(t *testing.T) error {
 	}
 	}
 	config.SetResolver(resolver.NewResolver(&resolver.NetworkConfig{}, networkID))
 	config.SetResolver(resolver.NewResolver(&resolver.NetworkConfig{}, networkID))
 
 
-	manager = NewInproxyBrokerClientManager(config, isProxy)
+	manager = NewInproxyBrokerClientManager(config, isProxy, tlsCache)
 
 
 	brokerClient, brokerDialParams, err = manager.GetBrokerClient(networkID)
 	brokerClient, brokerDialParams, err = manager.GetBrokerClient(networkID)
 	if err != nil {
 	if err != nil {

+ 14 - 5
psiphon/net.go

@@ -429,11 +429,12 @@ func makeFrontedHTTPClient(
 	useDeviceBinder,
 	useDeviceBinder,
 	skipVerify,
 	skipVerify,
 	disableSystemRootCAs,
 	disableSystemRootCAs,
-	payloadSecure bool) (*http.Client, func() common.APIParameters, error) {
+	payloadSecure bool,
+	tlsCache utls.ClientSessionCache) (*http.Client, func() common.APIParameters, error) {
 
 
 	frontedHTTPClient, err := newFrontedHTTPClientInstance(
 	frontedHTTPClient, err := newFrontedHTTPClientInstance(
 		config, tunnel, frontingSpecs, selectedFrontingProviderID,
 		config, tunnel, frontingSpecs, selectedFrontingProviderID,
-		useDeviceBinder, skipVerify, disableSystemRootCAs, payloadSecure)
+		useDeviceBinder, skipVerify, disableSystemRootCAs, payloadSecure, tlsCache)
 	if err != nil {
 	if err != nil {
 		return nil, nil, errors.Trace(err)
 		return nil, nil, errors.Trace(err)
 	}
 	}
@@ -487,6 +488,7 @@ func MakeUntunneledHTTPClient(
 	ctx context.Context,
 	ctx context.Context,
 	config *Config,
 	config *Config,
 	untunneledDialConfig *DialConfig,
 	untunneledDialConfig *DialConfig,
+	tlsCache utls.ClientSessionCache,
 	skipVerify bool,
 	skipVerify bool,
 	disableSystemRootCAs bool,
 	disableSystemRootCAs bool,
 	payloadSecure bool,
 	payloadSecure bool,
@@ -511,7 +513,9 @@ func MakeUntunneledHTTPClient(
 			frontingUseDeviceBinder,
 			frontingUseDeviceBinder,
 			false,
 			false,
 			disableSystemRootCAs,
 			disableSystemRootCAs,
-			payloadSecure)
+			payloadSecure,
+			tlsCache,
+		)
 		if err != nil {
 		if err != nil {
 			return nil, nil, errors.Trace(err)
 			return nil, nil, errors.Trace(err)
 		}
 		}
@@ -528,7 +532,7 @@ func MakeUntunneledHTTPClient(
 		SkipVerify:                    skipVerify,
 		SkipVerify:                    skipVerify,
 		DisableSystemRootCAs:          disableSystemRootCAs,
 		DisableSystemRootCAs:          disableSystemRootCAs,
 		TrustedCACertificatesFilename: untunneledDialConfig.TrustedCACertificatesFilename,
 		TrustedCACertificatesFilename: untunneledDialConfig.TrustedCACertificatesFilename,
-		ClientSessionCache:            utls.NewLRUClientSessionCache(0),
+		ClientSessionCache:            tlsCache,
 	}
 	}
 
 
 	tlsDialer := NewCustomTLSDialer(tlsConfig)
 	tlsDialer := NewCustomTLSDialer(tlsConfig)
@@ -557,6 +561,7 @@ func MakeTunneledHTTPClient(
 	ctx context.Context,
 	ctx context.Context,
 	config *Config,
 	config *Config,
 	tunnel *Tunnel,
 	tunnel *Tunnel,
+	tlsCache utls.ClientSessionCache,
 	skipVerify,
 	skipVerify,
 	disableSystemRootCAs,
 	disableSystemRootCAs,
 	payloadSecure bool,
 	payloadSecure bool,
@@ -578,7 +583,8 @@ func MakeTunneledHTTPClient(
 			false,
 			false,
 			false,
 			false,
 			disableSystemRootCAs,
 			disableSystemRootCAs,
-			payloadSecure)
+			payloadSecure,
+			tlsCache)
 		if err != nil {
 		if err != nil {
 			return nil, nil, errors.Trace(err)
 			return nil, nil, errors.Trace(err)
 		}
 		}
@@ -627,6 +633,7 @@ func MakeDownloadHTTPClient(
 	config *Config,
 	config *Config,
 	tunnel *Tunnel,
 	tunnel *Tunnel,
 	untunneledDialConfig *DialConfig,
 	untunneledDialConfig *DialConfig,
+	tlsCache utls.ClientSessionCache,
 	skipVerify,
 	skipVerify,
 	disableSystemRootCAs,
 	disableSystemRootCAs,
 	payloadSecure bool,
 	payloadSecure bool,
@@ -646,6 +653,7 @@ func MakeDownloadHTTPClient(
 			ctx,
 			ctx,
 			config,
 			config,
 			tunnel,
 			tunnel,
+			tlsCache,
 			skipVerify || disableSystemRootCAs,
 			skipVerify || disableSystemRootCAs,
 			disableSystemRootCAs,
 			disableSystemRootCAs,
 			payloadSecure,
 			payloadSecure,
@@ -667,6 +675,7 @@ func MakeDownloadHTTPClient(
 			ctx,
 			ctx,
 			config,
 			config,
 			dialConfig,
 			dialConfig,
+			tlsCache,
 			skipVerify,
 			skipVerify,
 			disableSystemRootCAs,
 			disableSystemRootCAs,
 			payloadSecure,
 			payloadSecure,

+ 13 - 3
psiphon/remoteServerList.go

@@ -33,10 +33,11 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/osl"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/osl"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
+	utls "github.com/Psiphon-Labs/utls"
 )
 )
 
 
 type RemoteServerListFetcher func(
 type RemoteServerListFetcher func(
-	ctx context.Context, config *Config, attempt int, tunnel *Tunnel, untunneledDialConfig *DialConfig) error
+	ctx context.Context, config *Config, attempt int, tunnel *Tunnel, untunneledDialConfig *DialConfig, tlsCache utls.ClientSessionCache) error
 
 
 // FetchCommonRemoteServerList downloads the common remote server list from
 // FetchCommonRemoteServerList downloads the common remote server list from
 // config.RemoteServerListURLs. It validates its digital signature using the
 // config.RemoteServerListURLs. It validates its digital signature using the
@@ -50,7 +51,8 @@ func FetchCommonRemoteServerList(
 	config *Config,
 	config *Config,
 	attempt int,
 	attempt int,
 	tunnel *Tunnel,
 	tunnel *Tunnel,
-	untunneledDialConfig *DialConfig) error {
+	untunneledDialConfig *DialConfig,
+	tlsCache utls.ClientSessionCache) error {
 
 
 	NoticeInfo("fetching common remote server list")
 	NoticeInfo("fetching common remote server list")
 
 
@@ -68,6 +70,7 @@ func FetchCommonRemoteServerList(
 		config,
 		config,
 		tunnel,
 		tunnel,
 		untunneledDialConfig,
 		untunneledDialConfig,
+		tlsCache,
 		downloadTimeout,
 		downloadTimeout,
 		downloadURL.URL,
 		downloadURL.URL,
 		canonicalURL,
 		canonicalURL,
@@ -145,7 +148,8 @@ func FetchObfuscatedServerLists(
 	config *Config,
 	config *Config,
 	attempt int,
 	attempt int,
 	tunnel *Tunnel,
 	tunnel *Tunnel,
-	untunneledDialConfig *DialConfig) error {
+	untunneledDialConfig *DialConfig,
+	tlsCache utls.ClientSessionCache) error {
 
 
 	NoticeInfo("fetching obfuscated remote server lists")
 	NoticeInfo("fetching obfuscated remote server lists")
 
 
@@ -192,6 +196,7 @@ func FetchObfuscatedServerLists(
 		config,
 		config,
 		tunnel,
 		tunnel,
 		untunneledDialConfig,
 		untunneledDialConfig,
+		tlsCache,
 		downloadTimeout,
 		downloadTimeout,
 		downloadURL,
 		downloadURL,
 		canonicalURL,
 		canonicalURL,
@@ -272,6 +277,7 @@ func FetchObfuscatedServerLists(
 			config,
 			config,
 			tunnel,
 			tunnel,
 			untunneledDialConfig,
 			untunneledDialConfig,
+			tlsCache,
 			downloadTimeout,
 			downloadTimeout,
 			rootURL,
 			rootURL,
 			canonicalRootURL,
 			canonicalRootURL,
@@ -325,6 +331,7 @@ func downloadOSLFileSpec(
 	config *Config,
 	config *Config,
 	tunnel *Tunnel,
 	tunnel *Tunnel,
 	untunneledDialConfig *DialConfig,
 	untunneledDialConfig *DialConfig,
+	tlsCache utls.ClientSessionCache,
 	downloadTimeout time.Duration,
 	downloadTimeout time.Duration,
 	rootURL *parameters.TransferURL,
 	rootURL *parameters.TransferURL,
 	canonicalRootURL string,
 	canonicalRootURL string,
@@ -349,6 +356,7 @@ func downloadOSLFileSpec(
 		config,
 		config,
 		tunnel,
 		tunnel,
 		untunneledDialConfig,
 		untunneledDialConfig,
+		tlsCache,
 		downloadTimeout,
 		downloadTimeout,
 		downloadURL,
 		downloadURL,
 		canonicalURL,
 		canonicalURL,
@@ -433,6 +441,7 @@ func downloadRemoteServerListFile(
 	config *Config,
 	config *Config,
 	tunnel *Tunnel,
 	tunnel *Tunnel,
 	untunneledDialConfig *DialConfig,
 	untunneledDialConfig *DialConfig,
+	tlsCache utls.ClientSessionCache,
 	downloadTimeout time.Duration,
 	downloadTimeout time.Duration,
 	sourceURL string,
 	sourceURL string,
 	canonicalURL string,
 	canonicalURL string,
@@ -472,6 +481,7 @@ func downloadRemoteServerListFile(
 		config,
 		config,
 		tunnel,
 		tunnel,
 		untunneledDialConfig,
 		untunneledDialConfig,
+		tlsCache,
 		skipVerify,
 		skipVerify,
 		disableSystemRootCAs,
 		disableSystemRootCAs,
 		payloadSecure,
 		payloadSecure,

+ 4 - 1
psiphon/tactics_test.go

@@ -30,6 +30,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
+	utls "github.com/Psiphon-Labs/utls"
 )
 )
 
 
 func TestStandAloneGetTactics(t *testing.T) {
 func TestStandAloneGetTactics(t *testing.T) {
@@ -108,7 +109,9 @@ func TestStandAloneGetTactics(t *testing.T) {
 		UpstreamProxyURL: config.UpstreamProxyURL,
 		UpstreamProxyURL: config.UpstreamProxyURL,
 	}
 	}
 
 
-	err = FetchCommonRemoteServerList(ctx, config, 0, nil, untunneledDialConfig)
+	tlsCache := utls.NewLRUClientSessionCache(0)
+
+	err = FetchCommonRemoteServerList(ctx, config, 0, nil, untunneledDialConfig, tlsCache)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("error fetching remote server list: %s", err)
 		t.Fatalf("error fetching remote server list: %s", err)
 	}
 	}

+ 7 - 0
psiphon/tlsDialer.go

@@ -244,6 +244,12 @@ func CustomTLSDial(
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 
 
+	// If the hard-coded session key is not set (e.g. FRONTED-MEEK-OSSH), SetSessionKey must be called.
+	// The session key is set to the resolved IP address.
+	if wrappedCache, ok := config.ClientSessionCache.(*common.UtlsClientSessionCacheWrapper); ok {
+		wrappedCache.SetSessionKey(underlyingConn.RemoteAddr().String())
+	}
+
 	if config.FragmentClientHello {
 	if config.FragmentClientHello {
 		underlyingConn = NewTLSFragmentorConn(underlyingConn)
 		underlyingConn = NewTLSFragmentorConn(underlyingConn)
 	}
 	}
@@ -358,6 +364,7 @@ func CustomTLSDial(
 		ServerName:             tlsConfigServerName,
 		ServerName:             tlsConfigServerName,
 		VerifyPeerCertificate:  tlsConfigVerifyPeerCertificate,
 		VerifyPeerCertificate:  tlsConfigVerifyPeerCertificate,
 		OmitEmptyPsk:           true,
 		OmitEmptyPsk:           true,
+		AlwaysIncludePSK:       true,
 	}
 	}
 
 
 	var randomizedTLSProfileSeed *prng.Seed
 	var randomizedTLSProfileSeed *prng.Seed

+ 4 - 1
psiphon/upgradeDownload.go

@@ -28,6 +28,7 @@ import (
 
 
 	"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/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
+	utls "github.com/Psiphon-Labs/utls"
 )
 )
 
 
 // DownloadUpgrade performs a resumable download of client upgrade files.
 // DownloadUpgrade performs a resumable download of client upgrade files.
@@ -61,7 +62,8 @@ func DownloadUpgrade(
 	attempt int,
 	attempt int,
 	handshakeVersion string,
 	handshakeVersion string,
 	tunnel *Tunnel,
 	tunnel *Tunnel,
-	untunneledDialConfig *DialConfig) error {
+	untunneledDialConfig *DialConfig,
+	tlsCache utls.ClientSessionCache) error {
 
 
 	// Note: this downloader doesn't use ETags since many client binaries, with
 	// Note: this downloader doesn't use ETags since many client binaries, with
 	// different embedded values, exist for a single version.
 	// different embedded values, exist for a single version.
@@ -94,6 +96,7 @@ func DownloadUpgrade(
 		config,
 		config,
 		tunnel,
 		tunnel,
 		untunneledDialConfig,
 		untunneledDialConfig,
+		tlsCache,
 		downloadURL.SkipVerify,
 		downloadURL.SkipVerify,
 		config.DisableSystemRootCAs,
 		config.DisableSystemRootCAs,
 		payloadSecure,
 		payloadSecure,

+ 7 - 0
vendor/github.com/Psiphon-Labs/utls/common.go

@@ -710,6 +710,13 @@ type Config struct {
 	// this behavior at their own discretion.
 	// this behavior at their own discretion.
 	OmitEmptyPsk bool // [uTLS]
 	OmitEmptyPsk bool // [uTLS]
 
 
+	// [Psiphon]
+	// AlwaysIncludePSK controls whether the PreSharedKey extension is always
+	// included in the ClientHello if there is a cached session, even if not specified
+	// in the selected ClientHelloSpec. If there are no cached sessions, OmitEmptyPsk
+	// controls whether the extension is omitted.
+	AlwaysIncludePSK bool
+
 	// InsecureServerNameToVerify is used to verify the hostname on the returned
 	// InsecureServerNameToVerify is used to verify the hostname on the returned
 	// certificates. It is intended to use with spoofed ServerName.
 	// certificates. It is intended to use with spoofed ServerName.
 	// If InsecureServerNameToVerify is "*", crypto/tls will do normal
 	// If InsecureServerNameToVerify is "*", crypto/tls will do normal

+ 19 - 0
vendor/github.com/Psiphon-Labs/utls/u_parrots.go

@@ -2626,6 +2626,25 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error {
 		return err
 		return err
 	}
 	}
 
 
+	// [Psiphon] SECTION BEGIN
+	// Add PSK extension if not specified in the spec.
+	if uconn.config.AlwaysIncludePSK {
+		supportsPSK := uconn.config.MaxVersion >= VersionTLS13
+		if supportsPSK {
+			hasPskExt := false
+			for _, ext := range p.Extensions {
+				if _, ok := ext.(PreSharedKeyExtension); ok {
+					hasPskExt = true
+				}
+			}
+			if !hasPskExt {
+				// pre_shared_key must be the last extension (RFC 8446, Section 4.2.11).
+				p.Extensions = append(p.Extensions, &UtlsPreSharedKeyExtension{})
+			}
+		}
+	}
+	// [Psiphon] SECTION END
+
 	privateHello, ech, err := uconn.makeClientHelloForApplyPreset()
 	privateHello, ech, err := uconn.makeClientHelloForApplyPreset()
 	if err != nil {
 	if err != nil {
 		return err
 		return err

+ 1 - 1
vendor/modules.txt

@@ -57,7 +57,7 @@ github.com/Psiphon-Labs/quic-go/internal/utils/ringbuffer
 github.com/Psiphon-Labs/quic-go/internal/wire
 github.com/Psiphon-Labs/quic-go/internal/wire
 github.com/Psiphon-Labs/quic-go/logging
 github.com/Psiphon-Labs/quic-go/logging
 github.com/Psiphon-Labs/quic-go/quicvarint
 github.com/Psiphon-Labs/quic-go/quicvarint
-# github.com/Psiphon-Labs/utls v0.0.0-20250429162420-6dbd45ae7ceb
+# github.com/Psiphon-Labs/utls v0.0.0-20250617193811-8e54e1fd2162
 ## explicit; go 1.23
 ## explicit; go 1.23
 github.com/Psiphon-Labs/utls
 github.com/Psiphon-Labs/utls
 github.com/Psiphon-Labs/utls/byteorder
 github.com/Psiphon-Labs/utls/byteorder