Browse Source

Preliminary TLS 1.3 support

- Client uses github.com/cloudflare/tls-tris for
  TLS 1.3 profiles and continues to use utls
  for all other profiles.

- Server now uses tris for both TLS 1.3 and older
  support.
Rod Hynes 7 years ago
parent
commit
a6fab6ab49

+ 2 - 0
README.md

@@ -99,6 +99,8 @@ Psiphon Tunnel Core uses:
 * [codahale/sss](https://github.com/codahale/sss)
 * [marusama/semaphore](https://github.com/marusama/semaphore)
 * [utls](https://github.com/refraction-networking/utls)
+* [quic-go](https://github.com/lucas-clemente/quic-go)
+* [tls-tris](https://github.com/cloudflare/tls-tris)
 
 Licensing
 --------------------------------------------------------------------------------

+ 9 - 7
psiphon/common/protocol/protocol.go

@@ -183,13 +183,14 @@ func UseClientTunnelProtocol(
 }
 
 const (
-	TLS_PROFILE_IOS_1131   = "iOS-Safari-11.3.1"
-	TLS_PROFILE_ANDROID_60 = "Android-6.0"
-	TLS_PROFILE_ANDROID_51 = "Android-5.1"
-	TLS_PROFILE_CHROME_58  = "Chrome-58"
-	TLS_PROFILE_CHROME_57  = "Chrome-57"
-	TLS_PROFILE_FIREFOX_56 = "Firefox-56"
-	TLS_PROFILE_RANDOMIZED = "Randomized"
+	TLS_PROFILE_IOS_1131         = "iOS-Safari-11.3.1"
+	TLS_PROFILE_ANDROID_60       = "Android-6.0"
+	TLS_PROFILE_ANDROID_51       = "Android-5.1"
+	TLS_PROFILE_CHROME_58        = "Chrome-58"
+	TLS_PROFILE_CHROME_57        = "Chrome-57"
+	TLS_PROFILE_FIREFOX_56       = "Firefox-56"
+	TLS_PROFILE_RANDOMIZED       = "Randomized"
+	TLS_PROFILE_TLS13_RANDOMIZED = "TLS-1.3-Randomized"
 )
 
 var SupportedTLSProfiles = TLSProfiles{
@@ -200,6 +201,7 @@ var SupportedTLSProfiles = TLSProfiles{
 	TLS_PROFILE_CHROME_57,
 	TLS_PROFILE_FIREFOX_56,
 	TLS_PROFILE_RANDOMIZED,
+	TLS_PROFILE_TLS13_RANDOMIZED,
 }
 
 type TLSProfiles []string

+ 10 - 0
psiphon/config.go

@@ -154,6 +154,12 @@ type Config struct {
 	// For the default, 0, InitialLimitTunnelProtocols is off.
 	InitialLimitTunnelProtocolsCandidateCount int
 
+	// LimitTLSProfiles indicates which TLS profiles to select from. Valid
+	// values are listed in protocols.SupportedTLSProfiles.
+	// For the default, an empty list, all profiles are candidates for
+	// selection.
+	LimitTLSProfiles []string
+
 	// EstablishTunnelTimeoutSeconds specifies a time limit after which to
 	// halt the core tunnel controller if no tunnel has been established. The
 	// default is parameters.EstablishTunnelTimeoutSeconds.
@@ -819,6 +825,10 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.InitialLimitTunnelProtocolsCandidateCount] = config.InitialLimitTunnelProtocolsCandidateCount
 	}
 
+	if len(config.LimitTLSProfiles) > 0 {
+		applyParameters[parameters.LimitTLSProfiles] = protocol.TunnelProtocols(config.LimitTLSProfiles)
+	}
+
 	if config.EstablishTunnelTimeoutSeconds != nil {
 		applyParameters[parameters.EstablishTunnelTimeout] = fmt.Sprintf("%ds", *config.EstablishTunnelTimeoutSeconds)
 	}

+ 2 - 12
psiphon/meekConn.go

@@ -45,7 +45,6 @@ import (
 	"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/upstreamproxy"
-	utls "github.com/Psiphon-Labs/utls"
 	"golang.org/x/net/http2"
 )
 
@@ -249,8 +248,8 @@ func DialMeek(
 			SkipVerify:                    true,
 			TLSProfile:                    meekConfig.TLSProfile,
 			TrustedCACertificatesFilename: dialConfig.TrustedCACertificatesFilename,
-			ClientSessionCache:            utls.NewLRUClientSessionCache(0),
 		}
+		tlsConfig.EnableClientSessionCache(meekConfig.ClientParameters)
 
 		if meekConfig.UseObfuscatedSessionTickets {
 			tlsConfig.ObfuscatedSessionTicketKey = meekConfig.MeekObfuscatedKey
@@ -302,18 +301,9 @@ func DialMeek(
 			return nil, common.ContextError(err)
 		}
 
-		isHTTP2 := false
-		if tlsConn, ok := preConn.(*utls.UConn); ok {
-			state := tlsConn.ConnectionState()
-			if state.NegotiatedProtocolIsMutual &&
-				state.NegotiatedProtocol == "h2" {
-				isHTTP2 = true
-			}
-		}
-
 		cachedTLSDialer = newCachedTLSDialer(preConn, tlsDialer)
 
-		if isHTTP2 {
+		if IsTLSConnUsingHTTP2(preConn) {
 			NoticeInfo("negotiated HTTP/2 for %s", meekConfig.DialAddress)
 			transport = &http2.Transport{
 				DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {

+ 14 - 14
psiphon/net.go

@@ -35,7 +35,6 @@ import (
 
 	"github.com/Psiphon-Labs/dns"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
-	utls "github.com/Psiphon-Labs/utls"
 )
 
 const DNS_PORT = 53
@@ -268,19 +267,20 @@ func MakeUntunneledHTTPClient(
 
 	dialer := NewTCPDialer(untunneledDialConfig)
 
-	tlsDialer := NewCustomTLSDialer(
-		// Note: when verifyLegacyCertificate is not nil, some
-		// of the other CustomTLSConfig is overridden.
-		&CustomTLSConfig{
-			ClientParameters: config.clientParameters,
-			Dial:             dialer,
-			VerifyLegacyCertificate:       verifyLegacyCertificate,
-			UseDialAddrSNI:                true,
-			SNIServerName:                 "",
-			SkipVerify:                    skipVerify,
-			TrustedCACertificatesFilename: untunneledDialConfig.TrustedCACertificatesFilename,
-			ClientSessionCache:            utls.NewLRUClientSessionCache(0),
-		})
+	// Note: when verifyLegacyCertificate is not nil, some
+	// of the other CustomTLSConfig is overridden.
+	tlsConfig := &CustomTLSConfig{
+		ClientParameters: config.clientParameters,
+		Dial:             dialer,
+		VerifyLegacyCertificate:       verifyLegacyCertificate,
+		UseDialAddrSNI:                true,
+		SNIServerName:                 "",
+		SkipVerify:                    skipVerify,
+		TrustedCACertificatesFilename: untunneledDialConfig.TrustedCACertificatesFilename,
+	}
+	tlsConfig.EnableClientSessionCache(config.clientParameters)
+
+	tlsDialer := NewCustomTLSDialer(tlsConfig)
 
 	transport := &http.Transport{
 		Dial: func(network, addr string) (net.Conn, error) {

+ 26 - 26
psiphon/server/meek.go

@@ -43,7 +43,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/crypto/nacl/box"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
-	utls "github.com/Psiphon-Labs/utls"
+	tris "github.com/Psiphon-Labs/tls-tris"
 )
 
 // MeekServer is based on meek-server.go from Tor and Psiphon:
@@ -96,7 +96,7 @@ const (
 type MeekServer struct {
 	support           *SupportServices
 	listener          net.Listener
-	tlsConfig         *utls.Config
+	tlsConfig         *tris.Config
 	clientHandler     func(clientTunnelProtocol string, clientConn net.Conn)
 	openConns         *common.Conns
 	stopBroadcast     <-chan struct{}
@@ -923,23 +923,23 @@ func (session *meekSession) GetMetrics() LogFields {
 // assuming the peer is an uncensored CDN.
 func makeMeekTLSConfig(
 	support *SupportServices,
-	useObfuscatedSessionTickets bool) (*utls.Config, error) {
+	useObfuscatedSessionTickets bool) (*tris.Config, error) {
 
 	certificate, privateKey, err := common.GenerateWebServerCertificate(common.GenerateHostName())
 	if err != nil {
 		return nil, common.ContextError(err)
 	}
 
-	tlsCertificate, err := utls.X509KeyPair(
+	tlsCertificate, err := tris.X509KeyPair(
 		[]byte(certificate), []byte(privateKey))
 	if err != nil {
 		return nil, common.ContextError(err)
 	}
 
-	config := &utls.Config{
-		Certificates: []utls.Certificate{tlsCertificate},
+	config := &tris.Config{
+		Certificates: []tris.Certificate{tlsCertificate},
 		NextProtos:   []string{"http/1.1"},
-		MinVersion:   utls.VersionTLS10,
+		MinVersion:   tris.VersionTLS10,
 
 		// This is a reordering of the supported CipherSuites in golang 1.6. Non-ephemeral key
 		// CipherSuites greatly reduce server load, and we try to select these since the meek
@@ -948,23 +948,23 @@ func makeMeekTLSConfig(
 		// by ephemeral key CipherSuites.
 		// https://github.com/golang/go/blob/1cb3044c9fcd88e1557eca1bf35845a4108bc1db/src/crypto/tls/cipher_suites.go#L75
 		CipherSuites: []uint16{
-			utls.TLS_RSA_WITH_AES_128_GCM_SHA256,
-			utls.TLS_RSA_WITH_AES_256_GCM_SHA384,
-			utls.TLS_RSA_WITH_RC4_128_SHA,
-			utls.TLS_RSA_WITH_AES_128_CBC_SHA,
-			utls.TLS_RSA_WITH_AES_256_CBC_SHA,
-			utls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
-			utls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
-			utls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
-			utls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
-			utls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
-			utls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
-			utls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
-			utls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
-			utls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
-			utls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
-			utls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
-			utls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+			tris.TLS_RSA_WITH_AES_128_GCM_SHA256,
+			tris.TLS_RSA_WITH_AES_256_GCM_SHA384,
+			tris.TLS_RSA_WITH_RC4_128_SHA,
+			tris.TLS_RSA_WITH_AES_128_CBC_SHA,
+			tris.TLS_RSA_WITH_AES_256_CBC_SHA,
+			tris.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+			tris.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+			tris.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+			tris.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+			tris.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+			tris.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
+			tris.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+			tris.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+			tris.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+			tris.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+			tris.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+			tris.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
 		},
 		PreferServerCipherSuites: true,
 	}
@@ -972,7 +972,7 @@ func makeMeekTLSConfig(
 	if useObfuscatedSessionTickets {
 
 		// See obfuscated session ticket overview
-		// in utls.NewObfuscatedClientSessionCache
+		// in NewObfuscatedClientSessionCache.
 
 		var obfuscatedSessionTicketKey [32]byte
 		key, err := hex.DecodeString(support.Config.MeekObfuscatedKey)
@@ -991,7 +991,7 @@ func makeMeekTLSConfig(
 		}
 
 		// Note: SessionTicketKey needs to be set, or else, it appears,
-		// utls.Config.serverInit() will clobber the value set by
+		// tris.Config.serverInit() will clobber the value set by
 		// SetSessionTicketKeys.
 		config.SessionTicketKey = obfuscatedSessionTicketKey
 		config.SetSessionTicketKeys([][32]byte{

+ 4 - 4
psiphon/server/net.go

@@ -54,7 +54,7 @@ import (
 	"net"
 	"net/http"
 
-	utls "github.com/Psiphon-Labs/utls"
+	tris "github.com/Psiphon-Labs/tls-tris"
 )
 
 // HTTPSServer is a wrapper around http.Server which adds the
@@ -71,9 +71,9 @@ type HTTPSServer struct {
 // shutdown. ListenAndServeTLS also requires the TLS cert and key to be in files
 // and we avoid that here.
 //
-// Note that the http.Server.TLSConfig field is ignored and the utls.Config
+// Note that the http.Server.TLSConfig field is ignored and the tris.Config
 // parameter is used intead.
-func (server *HTTPSServer) ServeTLS(listener net.Listener, config *utls.Config) error {
-	tlsListener := utls.NewListener(listener, config)
+func (server *HTTPSServer) ServeTLS(listener net.Listener, config *tris.Config) error {
+	tlsListener := tris.NewListener(listener, config)
 	return server.Serve(tlsListener)
 }

+ 42 - 1
psiphon/server/server_test.go

@@ -166,6 +166,23 @@ func TestUnfrontedMeekHTTPS(t *testing.T) {
 	runServer(t,
 		&runServerConfig{
 			tunnelProtocol:       "UNFRONTED-MEEK-HTTPS-OSSH",
+			tlsProfile:           protocol.TLS_PROFILE_RANDOMIZED,
+			enableSSHAPIRequests: true,
+			doHotReload:          false,
+			doDefaultSponsorID:   false,
+			denyTrafficRules:     false,
+			requireAuthorization: true,
+			omitAuthorization:    false,
+			doTunneledWebRequest: true,
+			doTunneledNTPRequest: true,
+		})
+}
+
+func TestUnfrontedMeekHTTPSTLS13(t *testing.T) {
+	runServer(t,
+		&runServerConfig{
+			tunnelProtocol:       "UNFRONTED-MEEK-HTTPS-OSSH",
+			tlsProfile:           protocol.TLS_PROFILE_TLS13_RANDOMIZED,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doDefaultSponsorID:   false,
@@ -181,6 +198,23 @@ func TestUnfrontedMeekSessionTicket(t *testing.T) {
 	runServer(t,
 		&runServerConfig{
 			tunnelProtocol:       "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
+			tlsProfile:           protocol.TLS_PROFILE_RANDOMIZED,
+			enableSSHAPIRequests: true,
+			doHotReload:          false,
+			doDefaultSponsorID:   false,
+			denyTrafficRules:     false,
+			requireAuthorization: true,
+			omitAuthorization:    false,
+			doTunneledWebRequest: true,
+			doTunneledNTPRequest: true,
+		})
+}
+
+func TestUnfrontedMeekSessionTicketTLS13(t *testing.T) {
+	runServer(t,
+		&runServerConfig{
+			tunnelProtocol:       "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
+			tlsProfile:           protocol.TLS_PROFILE_TLS13_RANDOMIZED,
 			enableSSHAPIRequests: true,
 			doHotReload:          false,
 			doDefaultSponsorID:   false,
@@ -362,6 +396,7 @@ func TestUDPOnlySLOK(t *testing.T) {
 
 type runServerConfig struct {
 	tunnelProtocol       string
+	tlsProfile           string
 	enableSSHAPIRequests bool
 	doHotReload          bool
 	doDefaultSponsorID   bool
@@ -580,6 +615,11 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		jsonNetworkID = fmt.Sprintf(`,"NetworkID" : "%s-%s"`, prefix, "NETWORK1")
 	}
 
+	jsonLimitTLSProfiles := ""
+	if runConfig.tlsProfile != "" {
+		jsonLimitTLSProfiles = fmt.Sprintf(`,"LimitTLSProfiles" : ["%s"]`, runConfig.tlsProfile)
+	}
+
 	clientConfigJSON := fmt.Sprintf(`
     {
         "ClientPlatform" : "Windows",
@@ -591,7 +631,8 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
         "ConnectionWorkerPoolSize" : %d,
         "LimitTunnelProtocols" : ["%s"]
         %s
-    }`, numTunnels, runConfig.tunnelProtocol, jsonNetworkID)
+        %s
+    }`, numTunnels, runConfig.tunnelProtocol, jsonLimitTLSProfiles, jsonNetworkID)
 
 	clientConfig, err := psiphon.LoadConfig([]byte(clientConfigJSON))
 	if err != nil {

+ 4 - 4
psiphon/server/webServer.go

@@ -32,7 +32,7 @@ import (
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
-	utls "github.com/Psiphon-Labs/utls"
+	tris "github.com/Psiphon-Labs/tls-tris"
 )
 
 const WEB_SERVER_IO_TIMEOUT = 10 * time.Second
@@ -70,15 +70,15 @@ func RunWebServer(
 	serveMux.HandleFunc("/connected", webServer.connectedHandler)
 	serveMux.HandleFunc("/status", webServer.statusHandler)
 
-	certificate, err := utls.X509KeyPair(
+	certificate, err := tris.X509KeyPair(
 		[]byte(support.Config.WebServerCertificate),
 		[]byte(support.Config.WebServerPrivateKey))
 	if err != nil {
 		return common.ContextError(err)
 	}
 
-	tlsConfig := &utls.Config{
-		Certificates: []utls.Certificate{certificate},
+	tlsConfig := &tris.Config{
+		Certificates: []tris.Certificate{certificate},
 	}
 
 	// TODO: inherits global log config?

+ 150 - 56
psiphon/tlsDialer.go

@@ -47,27 +47,9 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
 
-// Fork of https://github.com/getlantern/tlsdialer (http://gopkg.in/getlantern/tlsdialer.v1)
+// Based on https://github.com/getlantern/tlsdialer (http://gopkg.in/getlantern/tlsdialer.v1)
 // which itself is a "Fork of crypto/tls.Dial and DialWithDialer"
 
-// Adds two capabilities to tlsdialer:
-//
-// 1. HTTP proxy support, so the dialer may be used with http.Transport.
-//
-// 2. Support for self-signed Psiphon server certificates, which Go's certificate
-//    verification rejects due to two short comings:
-//    - lack of IP address SANs.
-//      see: "...because it doesn't contain any IP SANs" case in crypto/x509/verify.go
-//    - non-compliant constraint configuration (RFC 5280, 4.2.1.9).
-//      see: CheckSignatureFrom() in crypto/x509/x509.go
-//    Since the client has to be able to handle existing Psiphon server certificates,
-//    we need to be able to perform some form of verification in these cases.
-
-// tlsdialer:
-// package tlsdialer contains a customized version of crypto/tls.Dial that
-// allows control over whether or not to send the ServerName extension in the
-// client handshake.
-
 package psiphon
 
 import (
@@ -82,6 +64,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
+	tris "github.com/Psiphon-Labs/tls-tris"
 	utls "github.com/Psiphon-Labs/utls"
 )
 
@@ -138,14 +121,31 @@ type CustomTLSConfig struct {
 	// using the specified key.
 	ObfuscatedSessionTicketKey string
 
-	// ClientSessionCache specifies a cache to use to persist session
-	// tickets, enabling TLS session resumability across multiple
-	// CustomTLSDial calls or dialers using the same CustomTLSConfig.
-	ClientSessionCache utls.ClientSessionCache
+	utlsClientSessionCache utls.ClientSessionCache
+	trisClientSessionCache tris.ClientSessionCache
+}
+
+// EnableClientSessionCache initializes a cache to use to persist session
+// tickets, enabling TLS session resumability across multiple
+// CustomTLSDial calls or dialers using the same CustomTLSConfig.
+//
+// TLSProfile must be set or will be auto-set via SelectTLSProfile.
+func (config *CustomTLSConfig) EnableClientSessionCache(
+	clientParameters *parameters.ClientParameters) {
+
+	if config.TLSProfile == "" {
+		config.TLSProfile = SelectTLSProfile(config.ClientParameters)
+	}
+
+	if useUTLS(config.TLSProfile) {
+		config.utlsClientSessionCache = utls.NewLRUClientSessionCache(0)
+	} else {
+		config.trisClientSessionCache = tris.NewLRUClientSessionCache(0)
+	}
 }
 
+// SelectTLSProfile picks a random TLS profile from the available candidates.
 func SelectTLSProfile(
-	tunnelProtocol string,
 	clientParameters *parameters.ClientParameters) string {
 
 	limitTLSProfiles := clientParameters.Get().TLSProfiles(parameters.LimitTLSProfiles)
@@ -171,7 +171,11 @@ func SelectTLSProfile(
 	return tlsProfiles[choice]
 }
 
-func getClientHelloID(tlsProfile string) utls.ClientHelloID {
+func useUTLS(tlsProfile string) bool {
+	return tlsProfile != protocol.TLS_PROFILE_TLS13_RANDOMIZED
+}
+
+func getUTLSClientHelloID(tlsProfile string) utls.ClientHelloID {
 	switch tlsProfile {
 	case protocol.TLS_PROFILE_IOS_1131:
 		return utls.HelloiOSSafari_11_3_1
@@ -192,6 +196,52 @@ func getClientHelloID(tlsProfile string) utls.ClientHelloID {
 	}
 }
 
+// tlsConn provides a common interface for calling utls and tris methods. Both
+// utls and tris are derived from crypto/tls and have identical functions but
+// different types for return values etc.
+type tlsConn interface {
+	net.Conn
+	Handshake() error
+	GetPeerCertificates() []*x509.Certificate
+	IsHTTP2() bool
+}
+
+type utlsConn struct {
+	*utls.UConn
+}
+
+func (conn *utlsConn) GetPeerCertificates() []*x509.Certificate {
+	return conn.UConn.ConnectionState().PeerCertificates
+}
+
+func (conn *utlsConn) IsHTTP2() bool {
+	state := conn.UConn.ConnectionState()
+	return state.NegotiatedProtocolIsMutual &&
+		state.NegotiatedProtocol == "h2"
+}
+
+type trisConn struct {
+	*tris.Conn
+}
+
+func (conn *trisConn) GetPeerCertificates() []*x509.Certificate {
+	return conn.Conn.ConnectionState().PeerCertificates
+}
+
+func (conn *trisConn) IsHTTP2() bool {
+	state := conn.Conn.ConnectionState()
+	return state.NegotiatedProtocolIsMutual &&
+		state.NegotiatedProtocol == "h2"
+}
+
+func IsTLSConnUsingHTTP2(conn net.Conn) bool {
+	if c, ok := conn.(tlsConn); ok {
+		return c.IsHTTP2()
+	}
+	return false
+}
+
+// NewCustomTLSDialer creates a new dialer based on CustomTLSDial.
 func NewCustomTLSDialer(config *CustomTLSConfig) Dialer {
 	return func(ctx context.Context, network, addr string) (net.Conn, error) {
 		return CustomTLSDial(ctx, network, addr, config)
@@ -239,43 +289,37 @@ func CustomTLSDial(
 	selectedTLSProfile := config.TLSProfile
 
 	if selectedTLSProfile == "" {
-		selectedTLSProfile = SelectTLSProfile("", config.ClientParameters)
+		selectedTLSProfile = SelectTLSProfile(config.ClientParameters)
 	}
 
-	clientSessionCache := config.ClientSessionCache
-	if clientSessionCache == nil {
-		clientSessionCache = utls.NewLRUClientSessionCache(0)
-	}
-
-	tlsConfig := &utls.Config{
-		ClientSessionCache: clientSessionCache,
-	}
+	tlsConfigInsecureSkipVerify := false
+	tlsConfigServerName := ""
 
 	if config.SkipVerify {
-		tlsConfig.InsecureSkipVerify = true
+		tlsConfigInsecureSkipVerify = true
 	}
 
 	if config.UseDialAddrSNI {
-		tlsConfig.ServerName = hostname
+		tlsConfigServerName = hostname
 	} else if config.SNIServerName != "" && config.VerifyLegacyCertificate == nil {
 		// Set the ServerName and rely on the usual logic in
 		// tls.Conn.Handshake() to do its verification.
 		// Note: Go TLS will automatically omit this ServerName when it's an IP address
-		tlsConfig.ServerName = config.SNIServerName
+		tlsConfigServerName = config.SNIServerName
 	} else {
 		// No SNI.
 		// Disable verification in tls.Conn.Handshake().  We'll verify manually
 		// after handshaking
-		tlsConfig.InsecureSkipVerify = true
+		tlsConfigInsecureSkipVerify = true
 	}
 
-	tlsConn := utls.UClient(rawConn, tlsConfig, getClientHelloID(selectedTLSProfile))
+	var obfuscatedSessionTicketKey [32]byte
 
 	if config.ObfuscatedSessionTicketKey != "" {
 
-		// See obfuscated session ticket overview in NewObfuscatedClientSessionCache
+		// See obfuscated session ticket overview in
+		// NewObfuscatedClientSessionCache.
 
-		var obfuscatedSessionTicketKey [32]byte
 		key, err := hex.DecodeString(config.ObfuscatedSessionTicketKey)
 		if err == nil && len(key) != 32 {
 			err = errors.New("invalid obfuscated session key length")
@@ -284,20 +328,70 @@ func CustomTLSDial(
 			return nil, common.ContextError(err)
 		}
 		copy(obfuscatedSessionTicketKey[:], key)
+	}
 
-		sessionState, err := utls.NewObfuscatedClientSessionState(
-			obfuscatedSessionTicketKey)
-		if err != nil {
-			return nil, common.ContextError(err)
+	// Depending on the selected TLS profile, the TLS provider will be tris
+	// (TLS 1.3) or utls (all other profiles).
+
+	var conn tlsConn
+
+	if useUTLS(selectedTLSProfile) {
+
+		clientSessionCache := config.utlsClientSessionCache
+		if clientSessionCache == nil {
+			clientSessionCache = utls.NewLRUClientSessionCache(0)
+		}
+
+		tlsConfig := &utls.Config{
+			InsecureSkipVerify: tlsConfigInsecureSkipVerify,
+			ServerName:         tlsConfigServerName,
+			ClientSessionCache: clientSessionCache,
+		}
+
+		uconn := utls.UClient(rawConn, tlsConfig, getUTLSClientHelloID(selectedTLSProfile))
+
+		if config.ObfuscatedSessionTicketKey != "" {
+			sessionState, err := utls.NewObfuscatedClientSessionState(
+				obfuscatedSessionTicketKey)
+			if err != nil {
+				return nil, common.ContextError(err)
+			}
+			uconn.SetSessionState(sessionState)
+		}
+
+		conn = &utlsConn{
+			UConn: uconn,
+		}
+
+	} else {
+
+		var clientSessionCache tris.ClientSessionCache
+		if config.ObfuscatedSessionTicketKey != "" {
+			clientSessionCache = tris.NewObfuscatedClientSessionCache(
+				obfuscatedSessionTicketKey)
+		} else {
+			clientSessionCache = config.trisClientSessionCache
+			if clientSessionCache == nil {
+				clientSessionCache = tris.NewLRUClientSessionCache(0)
+			}
+		}
+
+		tlsConfig := &tris.Config{
+			InsecureSkipVerify: tlsConfigInsecureSkipVerify,
+			ServerName:         tlsConfigServerName,
+			ClientSessionCache: clientSessionCache,
+		}
+
+		conn = &trisConn{
+			Conn: tris.Client(rawConn, tlsConfig),
 		}
 
-		tlsConn.SetSessionState(sessionState)
 	}
 
 	resultChannel := make(chan error)
 
 	go func() {
-		resultChannel <- tlsConn.Handshake()
+		resultChannel <- conn.Handshake()
 	}()
 
 	select {
@@ -309,13 +403,13 @@ func CustomTLSDial(
 		<-resultChannel
 	}
 
-	if err == nil && !config.SkipVerify && tlsConfig.InsecureSkipVerify {
+	if err == nil && !config.SkipVerify && tlsConfigInsecureSkipVerify {
 
 		if config.VerifyLegacyCertificate != nil {
-			err = verifyLegacyCertificate(tlsConn, config.VerifyLegacyCertificate)
+			err = verifyLegacyCertificate(conn, config.VerifyLegacyCertificate)
 		} else {
 			// Manually verify certificates
-			err = verifyServerCerts(tlsConn, hostname, tlsConfig)
+			err = verifyServerCerts(conn, hostname)
 		}
 	}
 
@@ -324,11 +418,11 @@ func CustomTLSDial(
 		return nil, common.ContextError(err)
 	}
 
-	return tlsConn, nil
+	return conn, nil
 }
 
-func verifyLegacyCertificate(tlsConn *utls.UConn, expectedCertificate *x509.Certificate) error {
-	certs := tlsConn.ConnectionState().PeerCertificates
+func verifyLegacyCertificate(conn tlsConn, expectedCertificate *x509.Certificate) error {
+	certs := conn.GetPeerCertificates()
 	if len(certs) < 1 {
 		return common.ContextError(errors.New("no certificate to verify"))
 	}
@@ -338,11 +432,11 @@ func verifyLegacyCertificate(tlsConn *utls.UConn, expectedCertificate *x509.Cert
 	return nil
 }
 
-func verifyServerCerts(tlsConn *utls.UConn, hostname string, tlsConfig *utls.Config) error {
-	certs := tlsConn.ConnectionState().PeerCertificates
+func verifyServerCerts(conn tlsConn, hostname string) error {
+	certs := conn.GetPeerCertificates()
 
 	opts := x509.VerifyOptions{
-		Roots:         tlsConfig.RootCAs,
+		Roots:         nil, // Use host's root CAs
 		CurrentTime:   time.Now(),
 		DNSName:       hostname,
 		Intermediates: x509.NewCertPool(),

+ 1 - 3
psiphon/tunnel.go

@@ -671,9 +671,7 @@ func initMeekConfig(
 	// Pin the TLS profile for the entire meek connection.
 	selectedTLSProfile := ""
 	if protocol.TunnelProtocolUsesMeekHTTPS(selectedProtocol) {
-		selectedTLSProfile = SelectTLSProfile(
-			selectedProtocol,
-			config.clientParameters)
+		selectedTLSProfile = SelectTLSProfile(config.clientParameters)
 	}
 
 	return &MeekConfig{