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

Add new TLS tactics

- CustomTLSProfiles enables shipping
  TLS profiles as tactics data.

- NoDefaultTLSSessionIDProbability enables
  randomly omitting the SessionID from
  some TLS ClientHellos.
Rod Hynes 6 лет назад
Родитель
Сommit
011e86b329

+ 41 - 0
psiphon/common/parameters/clientParameters.go

@@ -93,7 +93,9 @@ const (
 	LimitTunnelProtocols                             = "LimitTunnelProtocols"
 	LimitTunnelProtocols                             = "LimitTunnelProtocols"
 	LimitTLSProfilesProbability                      = "LimitTLSProfilesProbability"
 	LimitTLSProfilesProbability                      = "LimitTLSProfilesProbability"
 	LimitTLSProfiles                                 = "LimitTLSProfiles"
 	LimitTLSProfiles                                 = "LimitTLSProfiles"
+	CustomTLSProfiles                                = "CustomTLSProfiles"
 	SelectRandomizedTLSProfileProbability            = "SelectRandomizedTLSProfileProbability"
 	SelectRandomizedTLSProfileProbability            = "SelectRandomizedTLSProfileProbability"
+	NoDefaultTLSSessionIDProbability                 = "NoDefaultTLSSessionIDProbability"
 	LimitQUICVersionsProbability                     = "LimitQUICVersionsProbability"
 	LimitQUICVersionsProbability                     = "LimitQUICVersionsProbability"
 	LimitQUICVersions                                = "LimitQUICVersions"
 	LimitQUICVersions                                = "LimitQUICVersions"
 	FragmentorProbability                            = "FragmentorProbability"
 	FragmentorProbability                            = "FragmentorProbability"
@@ -276,7 +278,9 @@ var defaultClientParameters = map[string]struct {
 
 
 	LimitTLSProfilesProbability:           {value: 1.0, minimum: 0.0},
 	LimitTLSProfilesProbability:           {value: 1.0, minimum: 0.0},
 	LimitTLSProfiles:                      {value: protocol.TLSProfiles{}},
 	LimitTLSProfiles:                      {value: protocol.TLSProfiles{}},
+	CustomTLSProfiles:                     {value: protocol.CustomTLSProfiles{}},
 	SelectRandomizedTLSProfileProbability: {value: 0.25, minimum: 0.0},
 	SelectRandomizedTLSProfileProbability: {value: 0.25, minimum: 0.0},
+	NoDefaultTLSSessionIDProbability:      {value: 0.5, minimum: 0.0},
 
 
 	LimitQUICVersionsProbability: {value: 1.0, minimum: 0.0},
 	LimitQUICVersionsProbability: {value: 1.0, minimum: 0.0},
 	LimitQUICVersions:            {value: protocol.QUICVersions{}},
 	LimitQUICVersions:            {value: protocol.QUICVersions{}},
@@ -632,6 +636,14 @@ func (p *ClientParameters) Set(
 						return nil, common.ContextError(err)
 						return nil, common.ContextError(err)
 					}
 					}
 				}
 				}
+			case protocol.CustomTLSProfiles:
+				err := v.Validate()
+				if err != nil {
+					if skipOnError {
+						continue
+					}
+					return nil, common.ContextError(err)
+				}
 			}
 			}
 
 
 			// Enforce any minimums. Assumes defaultClientParameters[name]
 			// Enforce any minimums. Assumes defaultClientParameters[name]
@@ -916,3 +928,32 @@ func (p *ClientParametersSnapshot) HTTPHeaders(name string) http.Header {
 	p.getValue(name, &value)
 	p.getValue(name, &value)
 	return value
 	return value
 }
 }
+
+// CustomTLSProfileNames returns the CustomTLSProfile.Name fields for
+// each profile in the CustomTLSProfiles parameter value.
+func (p *ClientParametersSnapshot) CustomTLSProfileNames() []string {
+	value := protocol.CustomTLSProfiles{}
+	p.getValue(CustomTLSProfiles, &value)
+	names := make([]string, len(value))
+	for i := 0; i < len(value); i++ {
+		names[i] = value[i].Name
+	}
+	return names
+}
+
+// CustomTLSProfile returns the CustomTLSProfile fields with the specified
+// Name field if it exists in the CustomTLSProfiles parameter value.
+// Returns nil if not found.
+func (p *ClientParametersSnapshot) CustomTLSProfile(name string) *protocol.CustomTLSProfile {
+	value := protocol.CustomTLSProfiles{}
+	p.getValue(CustomTLSProfiles, &value)
+
+	// Note: linear lookup -- assumes a short list
+
+	for i := 0; i < len(value); i++ {
+		if value[i].Name == name {
+			return value[i]
+		}
+	}
+	return nil
+}

+ 61 - 11
psiphon/common/parameters/clientParameters_test.go

@@ -41,57 +41,66 @@ func TestGetDefaultParameters(t *testing.T) {
 		case string:
 		case string:
 			g := p.Get().String(name)
 			g := p.Get().String(name)
 			if v != g {
 			if v != g {
-				t.Fatalf("String returned %+v expected %+v", v, g)
+				t.Fatalf("String returned %+v expected %+v", g, v)
 			}
 			}
 		case int:
 		case int:
 			g := p.Get().Int(name)
 			g := p.Get().Int(name)
 			if v != g {
 			if v != g {
-				t.Fatalf("Int returned %+v expected %+v", v, g)
+				t.Fatalf("Int returned %+v expected %+v", g, v)
 			}
 			}
 		case float64:
 		case float64:
 			g := p.Get().Float(name)
 			g := p.Get().Float(name)
 			if v != g {
 			if v != g {
-				t.Fatalf("Float returned %+v expected %+v", v, g)
+				t.Fatalf("Float returned %+v expected %+v", g, v)
 			}
 			}
 		case bool:
 		case bool:
 			g := p.Get().Bool(name)
 			g := p.Get().Bool(name)
 			if v != g {
 			if v != g {
-				t.Fatalf("Bool returned %+v expected %+v", v, g)
+				t.Fatalf("Bool returned %+v expected %+v", g, v)
 			}
 			}
 		case time.Duration:
 		case time.Duration:
 			g := p.Get().Duration(name)
 			g := p.Get().Duration(name)
 			if v != g {
 			if v != g {
-				t.Fatalf("Duration returned %+v expected %+v", v, g)
+				t.Fatalf("Duration returned %+v expected %+v", g, v)
 			}
 			}
 		case protocol.TunnelProtocols:
 		case protocol.TunnelProtocols:
 			g := p.Get().TunnelProtocols(name)
 			g := p.Get().TunnelProtocols(name)
 			if !reflect.DeepEqual(v, g) {
 			if !reflect.DeepEqual(v, g) {
-				t.Fatalf("TunnelProtocols returned %+v expected %+v", v, g)
+				t.Fatalf("TunnelProtocols returned %+v expected %+v", g, v)
 			}
 			}
 		case protocol.TLSProfiles:
 		case protocol.TLSProfiles:
 			g := p.Get().TLSProfiles(name)
 			g := p.Get().TLSProfiles(name)
 			if !reflect.DeepEqual(v, g) {
 			if !reflect.DeepEqual(v, g) {
-				t.Fatalf("TLSProfiles returned %+v expected %+v", v, g)
+				t.Fatalf("TLSProfiles returned %+v expected %+v", g, v)
 			}
 			}
 		case protocol.QUICVersions:
 		case protocol.QUICVersions:
 			g := p.Get().QUICVersions(name)
 			g := p.Get().QUICVersions(name)
 			if !reflect.DeepEqual(v, g) {
 			if !reflect.DeepEqual(v, g) {
-				t.Fatalf("QUICVersions returned %+v expected %+v", v, g)
+				t.Fatalf("QUICVersions returned %+v expected %+v", g, v)
 			}
 			}
 		case DownloadURLs:
 		case DownloadURLs:
 			g := p.Get().DownloadURLs(name)
 			g := p.Get().DownloadURLs(name)
 			if !reflect.DeepEqual(v, g) {
 			if !reflect.DeepEqual(v, g) {
-				t.Fatalf("DownloadURLs returned %+v expected %+v", v, g)
+				t.Fatalf("DownloadURLs returned %+v expected %+v", g, v)
 			}
 			}
 		case common.RateLimits:
 		case common.RateLimits:
 			g := p.Get().RateLimits(name)
 			g := p.Get().RateLimits(name)
 			if !reflect.DeepEqual(v, g) {
 			if !reflect.DeepEqual(v, g) {
-				t.Fatalf("RateLimits returned %+v expected %+v", v, g)
+				t.Fatalf("RateLimits returned %+v expected %+v", g, v)
 			}
 			}
 		case http.Header:
 		case http.Header:
 			g := p.Get().HTTPHeaders(name)
 			g := p.Get().HTTPHeaders(name)
 			if !reflect.DeepEqual(v, g) {
 			if !reflect.DeepEqual(v, g) {
-				t.Fatalf("HTTPHeaders returned %+v expected %+v", v, g)
+				t.Fatalf("HTTPHeaders returned %+v expected %+v", g, v)
+			}
+		case protocol.CustomTLSProfiles:
+			g := p.Get().CustomTLSProfileNames()
+			names := make([]string, len(v))
+			for i, profile := range v {
+				names[i] = profile.Name
+			}
+			if !reflect.DeepEqual(names, g) {
+				t.Fatalf("CustomTLSProfileNames returned %+v expected %+v", g, names)
 			}
 			}
 		default:
 		default:
 			t.Fatalf("Unhandled default type: %s", name)
 			t.Fatalf("Unhandled default type: %s", name)
@@ -260,3 +269,44 @@ func TestLimitTunnelProtocolProbability(t *testing.T) {
 		t.Fatalf("Unexpected probability result: %d", matchCount)
 		t.Fatalf("Unexpected probability result: %d", matchCount)
 	}
 	}
 }
 }
+
+func TestCustomTLSProfiles(t *testing.T) {
+	p, err := NewClientParameters(nil)
+	if err != nil {
+		t.Fatalf("NewClientParameters failed: %s", err)
+	}
+
+	customTLSProfiles := protocol.CustomTLSProfiles{
+		&protocol.CustomTLSProfile{Name: "Profile1"},
+		&protocol.CustomTLSProfile{Name: "Profile2"},
+	}
+
+	applyParameters := map[string]interface{}{
+		"CustomTLSProfiles": customTLSProfiles}
+
+	_, err = p.Set("", false, applyParameters)
+	if err != nil {
+		t.Fatalf("Set failed: %s", err)
+	}
+
+	names := p.Get().CustomTLSProfileNames()
+
+	if len(names) != 2 || names[0] != "Profile1" || names[1] != "Profile2" {
+		t.Fatalf("Unexpected CustomTLSProfileNames: %+v", names)
+	}
+
+	profile := p.Get().CustomTLSProfile("Profile1")
+	if profile == nil || profile.Name != "Profile1" {
+		t.Fatalf("Unexpected profile")
+	}
+
+	profile = p.Get().CustomTLSProfile("Profile2")
+	if profile == nil || profile.Name != "Profile2" {
+		t.Fatalf("Unexpected profile")
+	}
+
+	profile = p.Get().CustomTLSProfile("Profile3")
+	if profile != nil {
+		t.Fatalf("Unexpected profile")
+	}
+}

+ 217 - 0
psiphon/common/protocol/customTLSProfiles.go

@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2019, 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 protocol
+
+import (
+	"crypto/sha256"
+	"encoding/json"
+	"fmt"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
+	utls "github.com/refraction-networking/utls"
+)
+
+// CustomTLSProfile specifies custom TLS profile. This is used to deploy
+// custom ClientHellos as tactics data.
+type CustomTLSProfile struct {
+	Name     string
+	UTLSSpec *UTLSSpec
+}
+
+type CustomTLSProfiles []*CustomTLSProfile
+
+// Validate checks that the profiles in CustomTLSProfiles have no name conflicts.
+func (profiles CustomTLSProfiles) Validate() error {
+	names := make(map[string]bool)
+	for _, p := range profiles {
+		if p.Name == "" || common.Contains(SupportedTLSProfiles, p.Name) {
+			return common.ContextError(fmt.Errorf("invalid custom TLS profile: %s", p.Name))
+		}
+		if _, ok := names[p.Name]; ok {
+			return common.ContextError(fmt.Errorf("duplicate custom TLS profile: %s", p.Name))
+		}
+		names[p.Name] = true
+	}
+	return nil
+}
+
+// GetClientHelloSpec creates a new utls.ClientHelloSpec from the ClientHello
+// definition in UTLSpec.
+//
+// A new utls.ClientHelloSpec, with no shared data, is created for each call,
+// as per:
+// https://github.com/refraction-networking/utls/blob/4da67951864128358459681399dd208c49d5d001/u_parrots.go#L483
+func (profile *CustomTLSProfile) GetClientHelloSpec() (*utls.ClientHelloSpec, error) {
+
+	spec := &utls.ClientHelloSpec{}
+
+	spec.TLSVersMin = profile.UTLSSpec.TLSVersMin
+	spec.TLSVersMax = profile.UTLSSpec.TLSVersMax
+	spec.CipherSuites = append([]uint16(nil), profile.UTLSSpec.CipherSuites...)
+	spec.CompressionMethods = append([]uint8(nil), profile.UTLSSpec.CompressionMethods...)
+
+	spec.Extensions = make([]utls.TLSExtension, len(profile.UTLSSpec.Extensions))
+	for i, extension := range profile.UTLSSpec.Extensions {
+		var err error
+		spec.Extensions[i], err = extension.GetUTLSExtension()
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+	}
+
+	if profile.UTLSSpec.GetSessionID == "SHA-256" {
+		spec.GetSessionID = sha256.Sum256
+	}
+
+	return spec, nil
+}
+
+// UTLSSpec is a parallel data structure mirroring utls.ClientHelloSpec. Note
+// that utls.ClientHelloSpec cannot be directly marshaled with encoding/json
+// nor encoding/gob due to various type restrictions which
+// utls.ClientHelloSpec does not meet. Nor can we simply transmit a static,
+// raw ClientHello since concrete utls extension types must be instantiaed in
+// order for related functionality to be enabled.
+
+// UTLSSpec specifies a utls.ClientHelloSpec.
+type UTLSSpec struct {
+	TLSVersMin         uint16
+	TLSVersMax         uint16
+	CipherSuites       []uint16
+	CompressionMethods []uint8
+	Extensions         []*UTLSExtension
+	GetSessionID       string
+}
+
+// UTLSExtension specifies one of the several utls.TLSExtension concrete
+// implementations.
+type UTLSExtension struct {
+	Name string
+	Data json.RawMessage
+}
+
+// GetUTLSExtension instantiates the specified utls.TLSExtension concrete
+// implementation.
+func (e *UTLSExtension) GetUTLSExtension() (utls.TLSExtension, error) {
+	switch e.Name {
+	case "NPN":
+		var extension *utls.NPNExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "SNI":
+		return &utls.SNIExtension{}, nil
+	case "StatusRequest":
+		return &utls.StatusRequestExtension{}, nil
+	case "SupportedCurves":
+		var extension *utls.SupportedCurvesExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "SupportedPoints":
+		var extension *utls.SupportedPointsExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "SignatureAlgorithms":
+		var extension *utls.SignatureAlgorithmsExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "RenegotiationInfo":
+		var extension *utls.RenegotiationInfoExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "ALPN":
+		var extension *utls.ALPNExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "SCT":
+		return &utls.SCTExtension{}, nil
+	case "SessionTicket":
+		return &utls.SessionTicketExtension{}, nil
+	case "Generic":
+		var extension *utls.GenericExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "ExtendedMasterSecret":
+		return &utls.UtlsExtendedMasterSecretExtension{}, nil
+	case "GREASE":
+		return &utls.UtlsGREASEExtension{}, nil
+	case "BoringPadding":
+		return &utls.UtlsPaddingExtension{GetPaddingLen: utls.BoringPaddingStyle}, nil
+	case "KeyShare":
+		var extension *utls.KeyShareExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "PSKKeyExchangeModes":
+		var extension *utls.PSKKeyExchangeModesExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "SupportedVersions":
+		var extension *utls.SupportedVersionsExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "ChannelID":
+		return &utls.FakeChannelIDExtension{}, nil
+	case "CertCompressionAlgs":
+		var extension *utls.FakeCertCompressionAlgsExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	case "RecordSizeLimit":
+		var extension *utls.FakeRecordSizeLimitExtension
+		err := json.Unmarshal(e.Data, &extension)
+		if err != nil {
+			return nil, common.ContextError(nil)
+		}
+		return extension, nil
+	}
+
+	return nil, common.ContextError(fmt.Errorf("unknown utls extension: %s", e.Name))
+}

+ 189 - 0
psiphon/common/protocol/customTLSProfiles_test.go

@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2019, 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 protocol
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"encoding/json"
+	"testing"
+
+	utls "github.com/refraction-networking/utls"
+)
+
+func TestCustomTLSProfiles(t *testing.T) {
+
+	// Based on utls.HelloChrome_62. Some attributes have been removed to
+	// eliminate randomness; and additional extensions have been added for extra
+	// test coverage.
+
+	utlsClientHelloSpec := &utls.ClientHelloSpec{
+		TLSVersMax: utls.VersionTLS12,
+		TLSVersMin: utls.VersionTLS10,
+		CipherSuites: []uint16{
+			utls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+			utls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+			utls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+			utls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+			utls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+			utls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+			utls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+			utls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+			utls.TLS_RSA_WITH_AES_128_GCM_SHA256,
+			utls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+			utls.TLS_RSA_WITH_AES_128_CBC_SHA,
+			utls.TLS_RSA_WITH_AES_256_CBC_SHA,
+			utls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+		},
+		CompressionMethods: []byte{0},
+		Extensions: []utls.TLSExtension{
+			&utls.RenegotiationInfoExtension{Renegotiation: utls.RenegotiateOnceAsClient},
+			&utls.SNIExtension{},
+			&utls.UtlsExtendedMasterSecretExtension{},
+			&utls.SessionTicketExtension{},
+			&utls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []utls.SignatureScheme{
+				utls.ECDSAWithP256AndSHA256,
+				utls.PSSWithSHA256,
+				utls.PKCS1WithSHA256,
+				utls.ECDSAWithP384AndSHA384,
+				utls.PSSWithSHA384,
+				utls.PKCS1WithSHA384,
+				utls.PSSWithSHA512,
+				utls.PKCS1WithSHA512,
+				utls.PKCS1WithSHA1},
+			},
+			&utls.StatusRequestExtension{},
+			&utls.SCTExtension{},
+			&utls.ALPNExtension{AlpnProtocols: []string{"h2", "http/1.1"}},
+			&utls.FakeChannelIDExtension{},
+			&utls.SupportedPointsExtension{SupportedPoints: []byte{0}},
+			&utls.SupportedCurvesExtension{[]utls.CurveID{
+				utls.X25519, utls.CurveP256, utls.CurveP384}},
+			&utls.UtlsPaddingExtension{GetPaddingLen: utls.BoringPaddingStyle},
+
+			// Additional extensions for test coverage
+			&utls.NPNExtension{NextProtos: []string{"http/1.1"}},
+			&utls.GenericExtension{Id: 9999, Data: []byte("generic extension")},
+			&utls.KeyShareExtension{[]utls.KeyShare{
+				{Group: utls.X25519, Data: []byte{9, 9, 9, 9}},
+			}},
+			&utls.PSKKeyExchangeModesExtension{[]uint8{
+				utls.PskModeDHE,
+			}},
+			&utls.SupportedVersionsExtension{[]uint16{
+				utls.VersionTLS13,
+				utls.VersionTLS12,
+				utls.VersionTLS11,
+				utls.VersionTLS10,
+			}},
+			&utls.FakeCertCompressionAlgsExtension{[]utls.CertCompressionAlgo{
+				utls.CertCompressionBrotli,
+			}},
+			&utls.FakeChannelIDExtension{},
+			&utls.FakeRecordSizeLimitExtension{Limit: 9999},
+		},
+		GetSessionID: sha256.Sum256,
+	}
+
+	customTLSProfilesJSON := []byte(`
+    [
+      {
+        "Name": "CustomProfile",
+        "UTLSSpec": {
+          "TLSVersMax": 771,
+          "TLSVersMin": 769,
+          "CipherSuites": [49195, 49199, 49196, 49200, 52393, 52392, 49171, 49172, 156, 157, 47, 53, 10],
+          "CompressionMethods": [0],
+          "Extensions" : [
+            {"Name": "RenegotiationInfo", "Data": {"Renegotiation": 1}},
+            {"Name": "SNI"},
+            {"Name": "ExtendedMasterSecret"},
+            {"Name": "SessionTicket"},
+            {"Name": "SignatureAlgorithms", "Data": {"SupportedSignatureAlgorithms": [1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513]}},
+            {"Name": "StatusRequest"},
+            {"Name": "SCT"},
+            {"Name": "ALPN", "Data": {"AlpnProtocols": ["h2", "http/1.1"]}},
+            {"Name": "ChannelID"},
+            {"Name": "SupportedPoints", "Data": {"SupportedPoints": [0]}},
+            {"Name": "SupportedCurves", "Data": {"Curves": [29, 23, 24]}},
+            {"Name": "BoringPadding"},
+            {"Name": "NPN", "Data": {"NextProtos": ["h2", "http/1.1"]}},
+            {"Name": "Generic", "Data": {"Id": 9999, "Data": [103, 101, 110, 101, 114, 105, 99, 32, 101, 120, 116, 101, 110, 115, 105, 111, 110]}},
+            {"Name": "KeyShare", "Data": {"KeyShares": [{"Group": 29, "Data": [9, 9, 9, 9]}]}},
+            {"Name": "PSKKeyExchangeModes", "Data": {"Modes": [1]}},
+            {"Name": "SupportedVersions", "Data": {"Versions": [772, 771, 770, 769]}},
+            {"Name": "CertCompressionAlgs", "Data": {"Methods": [2]}},
+            {"Name": "ChannelID"},
+            {"Name": "RecordSizeLimit", "Data": {"Limit": 9999}}],
+          "GetSessionID" : "SHA-256"
+        }
+      }
+    ]`)
+
+	var customTLSProfiles CustomTLSProfiles
+
+	err := json.Unmarshal(customTLSProfilesJSON, &customTLSProfiles)
+	if err != nil {
+		t.Fatalf("Unmarshal failed: %s", err)
+	}
+
+	err = customTLSProfiles.Validate()
+	if err != nil {
+		t.Fatalf("Validate failed: %s", err)
+	}
+
+	profile := customTLSProfiles[0]
+	profileClientHelloSpec, err := profile.GetClientHelloSpec()
+	if err != nil {
+		t.Fatalf("GetClientHelloSpec failed: %s", err)
+	}
+
+	zeroes := make([]byte, 32)
+
+	conn1 := utls.UClient(nil, &utls.Config{InsecureSkipVerify: true}, utls.HelloCustom)
+	conn1.ApplyPreset(utlsClientHelloSpec)
+	conn1.SetClientRandom(zeroes)
+	conn1.HandshakeState.Hello.SessionId = zeroes
+	err = conn1.BuildHandshakeState()
+	if err != nil {
+		t.Fatalf("BuildHandshakeState failed: %s", err)
+	}
+
+	conn2 := utls.UClient(nil, &utls.Config{InsecureSkipVerify: true}, utls.HelloCustom)
+	conn2.ApplyPreset(profileClientHelloSpec)
+	conn2.SetClientRandom(zeroes)
+	conn2.HandshakeState.Hello.SessionId = zeroes
+	err = conn2.BuildHandshakeState()
+	if err != nil {
+		t.Fatalf("BuildHandshakeState failed: %s", err)
+	}
+
+	if len(conn1.HandshakeState.Hello.Raw) == 0 {
+		t.Fatalf("Missing raw ClientHello")
+	}
+
+	if len(conn2.HandshakeState.Hello.Raw) == 0 {
+		t.Fatalf("Missing raw ClientHello")
+	}
+
+	if bytes.Compare(conn1.HandshakeState.Hello.Raw, conn2.HandshakeState.Hello.Raw) != 0 {
+		t.Fatalf("Unidentical raw ClientHellos")
+	}
+}

+ 24 - 7
psiphon/dialParameters.go

@@ -94,6 +94,7 @@ type DialParameters struct {
 
 
 	SelectedTLSProfile       bool
 	SelectedTLSProfile       bool
 	TLSProfile               string
 	TLSProfile               string
+	NoDefaultTLSSessionID    bool
 	TLSVersion               string
 	TLSVersion               string
 	RandomizedTLSProfileSeed *prng.Seed
 	RandomizedTLSProfileSeed *prng.Seed
 
 
@@ -317,6 +318,8 @@ func MakeDialParameters(
 
 
 		dialParams.SelectedTLSProfile = true
 		dialParams.SelectedTLSProfile = true
 		dialParams.TLSProfile = SelectTLSProfile(p)
 		dialParams.TLSProfile = SelectTLSProfile(p)
+		dialParams.NoDefaultTLSSessionID = p.WeightedCoinFlip(
+			parameters.NoDefaultTLSSessionIDProbability)
 	}
 	}
 
 
 	if (!isReplay || !replayRandomizedTLSProfile) &&
 	if (!isReplay || !replayRandomizedTLSProfile) &&
@@ -332,20 +335,25 @@ func MakeDialParameters(
 	if (!isReplay || !replayTLSProfile) &&
 	if (!isReplay || !replayTLSProfile) &&
 		protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol) {
 		protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol) {
 
 
-		// Since "Randomized-v2" may be TLS 1.2 or TLS 1.3, construct the
-		// ClientHello to determine if it's TLS 1.3. This test also covers
-		// non-randomized TLS 1.3 profiles. This check must come after
-		// dialParams.TLSProfile and dialParams.RandomizedTLSProfileSeed are set.
-		// No actual dial is made here.
+		// Since "Randomized-v2"/CustomTLSProfiles may be TLS 1.2 or TLS 1.3,
+		// construct the ClientHello to determine if it's TLS 1.3. This test also
+		// covers non-randomized TLS 1.3 profiles. This check must come after
+		// dialParams.TLSProfile and dialParams.RandomizedTLSProfileSeed are set. No
+		// actual dial is made here.
 
 
-		utlsClientHelloID := getUTLSClientHelloID(dialParams.TLSProfile)
+		utlsClientHelloID, utlsClientHelloSpec, err := getUTLSClientHelloID(
+			p, dialParams.TLSProfile)
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
 
 
 		if protocol.TLSProfileIsRandomized(dialParams.TLSProfile) {
 		if protocol.TLSProfileIsRandomized(dialParams.TLSProfile) {
 			utlsClientHelloID.Seed = new(utls.PRNGSeed)
 			utlsClientHelloID.Seed = new(utls.PRNGSeed)
 			*utlsClientHelloID.Seed = [32]byte(*dialParams.RandomizedTLSProfileSeed)
 			*utlsClientHelloID.Seed = [32]byte(*dialParams.RandomizedTLSProfileSeed)
 		}
 		}
 
 
-		dialParams.TLSVersion, err = getClientHelloVersion(utlsClientHelloID)
+		dialParams.TLSVersion, err = getClientHelloVersion(
+			utlsClientHelloID, utlsClientHelloSpec)
 		if err != nil {
 		if err != nil {
 			return nil, common.ContextError(err)
 			return nil, common.ContextError(err)
 		}
 		}
@@ -582,6 +590,7 @@ func MakeDialParameters(
 			QUICVersion:                   dialParams.QUICVersion,
 			QUICVersion:                   dialParams.QUICVersion,
 			UseHTTPS:                      protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol),
 			UseHTTPS:                      protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol),
 			TLSProfile:                    dialParams.TLSProfile,
 			TLSProfile:                    dialParams.TLSProfile,
+			NoDefaultTLSSessionID:         dialParams.NoDefaultTLSSessionID,
 			RandomizedTLSProfileSeed:      dialParams.RandomizedTLSProfileSeed,
 			RandomizedTLSProfileSeed:      dialParams.RandomizedTLSProfileSeed,
 			UseObfuscatedSessionTickets:   dialParams.TunnelProtocol == protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET,
 			UseObfuscatedSessionTickets:   dialParams.TunnelProtocol == protocol.TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET,
 			SNIServerName:                 dialParams.MeekSNIServerName,
 			SNIServerName:                 dialParams.MeekSNIServerName,
@@ -658,6 +667,14 @@ func (dialParams *DialParameters) Failed(config *Config) {
 	}
 	}
 }
 }
 
 
+func (dialParams *DialParameters) GetTLSVersionForMetrics() string {
+	tlsVersion := dialParams.TLSVersion
+	if dialParams.NoDefaultTLSSessionID {
+		tlsVersion += "-no_def_id"
+	}
+	return tlsVersion
+}
+
 // ExchangedDialParameters represents the subset of DialParameters that is
 // ExchangedDialParameters represents the subset of DialParameters that is
 // shared in a client-to-client exchange of server connection info.
 // shared in a client-to-client exchange of server connection info.
 //
 //

+ 6 - 0
psiphon/meekConn.go

@@ -92,6 +92,11 @@ type MeekConfig struct {
 	// underlying TLS connections created by this meek connection.
 	// underlying TLS connections created by this meek connection.
 	TLSProfile string
 	TLSProfile string
 
 
+	// NoDefaultTLSSessionID specifies the value for
+	// CustomTLSConfig.NoDefaultTLSSessionID for all underlying TLS connections
+	// created by this meek connection.
+	NoDefaultTLSSessionID bool
+
 	// RandomizedTLSProfileSeed specifies the value for
 	// RandomizedTLSProfileSeed specifies the value for
 	// CustomTLSConfig.RandomizedTLSProfileSeed for all underlying TLS
 	// CustomTLSConfig.RandomizedTLSProfileSeed for all underlying TLS
 	// connections created by this meek connection.
 	// connections created by this meek connection.
@@ -293,6 +298,7 @@ func DialMeek(
 			SNIServerName:                 meekConfig.SNIServerName,
 			SNIServerName:                 meekConfig.SNIServerName,
 			SkipVerify:                    true,
 			SkipVerify:                    true,
 			TLSProfile:                    meekConfig.TLSProfile,
 			TLSProfile:                    meekConfig.TLSProfile,
+			NoDefaultTLSSessionID:         &meekConfig.NoDefaultTLSSessionID,
 			RandomizedTLSProfileSeed:      meekConfig.RandomizedTLSProfileSeed,
 			RandomizedTLSProfileSeed:      meekConfig.RandomizedTLSProfileSeed,
 			TrustedCACertificatesFilename: dialConfig.TrustedCACertificatesFilename,
 			TrustedCACertificatesFilename: dialConfig.TrustedCACertificatesFilename,
 		}
 		}

+ 1 - 1
psiphon/notice.go

@@ -475,7 +475,7 @@ func noticeWithDialParameters(noticeType string, dialParams *DialParameters) {
 
 
 		if dialParams.SelectedTLSProfile {
 		if dialParams.SelectedTLSProfile {
 			args = append(args, "TLSProfile", dialParams.TLSProfile)
 			args = append(args, "TLSProfile", dialParams.TLSProfile)
-			args = append(args, "TLSVersion", dialParams.TLSVersion)
+			args = append(args, "TLSVersion", dialParams.GetTLSVersionForMetrics())
 		}
 		}
 
 
 		if dialParams.DialPortNumber != "" {
 		if dialParams.DialPortNumber != "" {

+ 4 - 3
psiphon/server/server_test.go

@@ -32,6 +32,7 @@ import (
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"strconv"
 	"strconv"
+	"strings"
 	"sync"
 	"sync"
 	"syscall"
 	"syscall"
 	"testing"
 	"testing"
@@ -1232,9 +1233,9 @@ func checkExpectedLogFields(runConfig *runServerConfig, fields map[string]interf
 			return fmt.Errorf("unexpected tls_profile '%s'", fields["tls_profile"])
 			return fmt.Errorf("unexpected tls_profile '%s'", fields["tls_profile"])
 		}
 		}
 
 
-		if !common.Contains(
-			[]string{protocol.TLS_VERSION_12, protocol.TLS_VERSION_13},
-			fields["tls_version"].(string)) {
+		tlsVersion := fields["tls_version"].(string)
+		if !strings.HasPrefix(tlsVersion, protocol.TLS_VERSION_12) &&
+			!strings.HasPrefix(tlsVersion, protocol.TLS_VERSION_13) {
 			return fmt.Errorf("unexpected tls_version '%s'", fields["tls_version"])
 			return fmt.Errorf("unexpected tls_version '%s'", fields["tls_version"])
 		}
 		}
 	}
 	}

+ 1 - 1
psiphon/serverApi.go

@@ -838,7 +838,7 @@ func getBaseAPIParameters(
 
 
 	if dialParams.SelectedTLSProfile {
 	if dialParams.SelectedTLSProfile {
 		params["tls_profile"] = dialParams.TLSProfile
 		params["tls_profile"] = dialParams.TLSProfile
-		params["tls_version"] = dialParams.TLSVersion
+		params["tls_version"] = dialParams.GetTLSVersionForMetrics()
 	}
 	}
 
 
 	if dialParams.ServerEntry.Region != "" {
 	if dialParams.ServerEntry.Region != "" {

+ 12 - 1
psiphon/sessionTicket_test.go

@@ -33,6 +33,7 @@ import (
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
+	"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"
 	tris "github.com/Psiphon-Labs/tls-tris"
 	tris "github.com/Psiphon-Labs/tls-tris"
 	utls "github.com/refraction-networking/utls"
 	utls "github.com/refraction-networking/utls"
@@ -55,6 +56,11 @@ func TestObfuscatedSessionTicket(t *testing.T) {
 
 
 func runObfuscatedSessionTicket(t *testing.T, tlsProfile string) {
 func runObfuscatedSessionTicket(t *testing.T, tlsProfile string) {
 
 
+	clientParameters, err := parameters.NewClientParameters(nil)
+	if err != nil {
+		t.Fatalf("NewClientParameters failed: %s\n", err)
+	}
+
 	var standardSessionTicketKey [32]byte
 	var standardSessionTicketKey [32]byte
 	rand.Read(standardSessionTicketKey[:])
 	rand.Read(standardSessionTicketKey[:])
 
 
@@ -142,7 +148,12 @@ func runObfuscatedSessionTicket(t *testing.T, tlsProfile string) {
 			}
 			}
 			defer tcpConn.Close()
 			defer tcpConn.Close()
 
 
-			utlsClientHelloID := getUTLSClientHelloID(tlsProfile)
+			utlsClientHelloID, _, err := getUTLSClientHelloID(
+				clientParameters.Get(), tlsProfile)
+			if err != nil {
+				report(err)
+				return
+			}
 
 
 			tlsConn := utls.UClient(tcpConn, clientConfig, utlsClientHelloID)
 			tlsConn := utls.UClient(tcpConn, clientConfig, utlsClientHelloID)
 
 

+ 149 - 36
psiphon/tlsDialer.go

@@ -58,6 +58,7 @@ import (
 	"crypto/x509"
 	"crypto/x509"
 	"encoding/hex"
 	"encoding/hex"
 	"errors"
 	"errors"
+	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net"
 	"net"
 	"time"
 	"time"
@@ -115,6 +116,11 @@ type CustomTLSConfig struct {
 	// compatibility constraints.
 	// compatibility constraints.
 	TLSProfile string
 	TLSProfile string
 
 
+	// NoDefaultTLSSessionID specifies whether to set a TLS session ID by
+	// default, for a new TLS connection that is not resuming a session.
+	// When nil, the parameter is set randomly.
+	NoDefaultTLSSessionID *bool
+
 	// RandomizedTLSProfileSeed specifies the PRNG seed to use when generating
 	// RandomizedTLSProfileSeed specifies the PRNG seed to use when generating
 	// a randomized TLS ClientHello, which applies to TLS profiles where
 	// a randomized TLS ClientHello, which applies to TLS profiles where
 	// protocol.TLSProfileIsRandomized is true. The PRNG seed allows for
 	// protocol.TLSProfileIsRandomized is true. The PRNG seed allows for
@@ -147,14 +153,20 @@ func (config *CustomTLSConfig) EnableClientSessionCache(
 func SelectTLSProfile(
 func SelectTLSProfile(
 	p *parameters.ClientParametersSnapshot) string {
 	p *parameters.ClientParametersSnapshot) string {
 
 
-	// Two TLS profile lists are constructed, subject to limit constraints: fixed
-	// parrots and randomized. If one list is empty, the non-empty list is used.
-	// Otherwise SelectRandomizedTLSProfileProbability determines which list is used.
+	// Two TLS profile lists are constructed, subject to limit constraints:
+	// stock, fixed parrots (non-randomized SupportedTLSProfiles) and custom
+	// parrots (CustomTLSProfileNames); and randomized. If one list is empty, the
+	// non-empty list is used. Otherwise SelectRandomizedTLSProfileProbability
+	// determines which list is used.
+	//
+	// Note that LimitTLSProfiles is not applied to CustomTLSProfiles; the
+	// presence of a candidate in CustomTLSProfiles is treated as explicit
+	// enabling.
 
 
 	limitTLSProfiles := p.TLSProfiles(parameters.LimitTLSProfiles)
 	limitTLSProfiles := p.TLSProfiles(parameters.LimitTLSProfiles)
 
 
 	randomizedTLSProfiles := make([]string, 0)
 	randomizedTLSProfiles := make([]string, 0)
-	parrotTLSProfiles := make([]string, 0)
+	parrotTLSProfiles := p.CustomTLSProfileNames()
 
 
 	for _, tlsProfile := range protocol.SupportedTLSProfiles {
 	for _, tlsProfile := range protocol.SupportedTLSProfiles {
 
 
@@ -184,42 +196,79 @@ func SelectTLSProfile(
 	return parrotTLSProfiles[prng.Intn(len(parrotTLSProfiles))]
 	return parrotTLSProfiles[prng.Intn(len(parrotTLSProfiles))]
 }
 }
 
 
-func getUTLSClientHelloID(tlsProfile string) utls.ClientHelloID {
+func getUTLSClientHelloID(
+	p *parameters.ClientParametersSnapshot,
+	tlsProfile string) (utls.ClientHelloID, *utls.ClientHelloSpec, error) {
+
 	switch tlsProfile {
 	switch tlsProfile {
 	case protocol.TLS_PROFILE_IOS_111:
 	case protocol.TLS_PROFILE_IOS_111:
-		return utls.HelloIOS_11_1
+		return utls.HelloIOS_11_1, nil, nil
 	case protocol.TLS_PROFILE_IOS_121:
 	case protocol.TLS_PROFILE_IOS_121:
-		return utls.HelloIOS_12_1
+		return utls.HelloIOS_12_1, nil, nil
 	case protocol.TLS_PROFILE_CHROME_58:
 	case protocol.TLS_PROFILE_CHROME_58:
-		return utls.HelloChrome_58
+		return utls.HelloChrome_58, nil, nil
 	case protocol.TLS_PROFILE_CHROME_62:
 	case protocol.TLS_PROFILE_CHROME_62:
-		return utls.HelloChrome_62
+		return utls.HelloChrome_62, nil, nil
 	case protocol.TLS_PROFILE_CHROME_70:
 	case protocol.TLS_PROFILE_CHROME_70:
-		return utls.HelloChrome_70
+		return utls.HelloChrome_70, nil, nil
 	case protocol.TLS_PROFILE_CHROME_72:
 	case protocol.TLS_PROFILE_CHROME_72:
-		return utls.HelloChrome_72
+		return utls.HelloChrome_72, nil, nil
 	case protocol.TLS_PROFILE_FIREFOX_55:
 	case protocol.TLS_PROFILE_FIREFOX_55:
-		return utls.HelloFirefox_55
+		return utls.HelloFirefox_55, nil, nil
 	case protocol.TLS_PROFILE_FIREFOX_56:
 	case protocol.TLS_PROFILE_FIREFOX_56:
-		return utls.HelloFirefox_56
+		return utls.HelloFirefox_56, nil, nil
 	case protocol.TLS_PROFILE_FIREFOX_65:
 	case protocol.TLS_PROFILE_FIREFOX_65:
-		return utls.HelloFirefox_65
+		return utls.HelloFirefox_65, nil, nil
 	case protocol.TLS_PROFILE_RANDOMIZED:
 	case protocol.TLS_PROFILE_RANDOMIZED:
-		return utls.HelloRandomized
-	default:
-		return utls.HelloGolang
+		return utls.HelloRandomized, nil, nil
+	}
+
+	// utls.HelloCustom with a utls.ClientHelloSpec is used for
+	// CustomTLSProfiles.
+
+	customTLSProfile := p.CustomTLSProfile(tlsProfile)
+	if customTLSProfile == nil {
+		return utls.HelloCustom,
+			nil,
+			common.ContextError(fmt.Errorf("unknown TLS profile: %s", tlsProfile))
+	}
+
+	utlsClientHelloSpec, err := customTLSProfile.GetClientHelloSpec()
+	if err != nil {
+		return utls.ClientHelloID{}, nil, common.ContextError(err)
 	}
 	}
+
+	return utls.HelloCustom, utlsClientHelloSpec, nil
 }
 }
 
 
-func getClientHelloVersion(utlsClientHelloID utls.ClientHelloID) (string, error) {
+func getClientHelloVersion(
+	utlsClientHelloID utls.ClientHelloID,
+	utlsClientHelloSpec *utls.ClientHelloSpec) (string, error) {
 
 
-	// Assumes utlsClientHelloID.Seed has been set; otherwise the result is
-	// ephemeral.
+	switch utlsClientHelloID {
+
+	case utls.HelloIOS_11_1, utls.HelloIOS_12_1, utls.HelloChrome_58,
+		utls.HelloChrome_62, utls.HelloFirefox_55, utls.HelloFirefox_56:
+		return protocol.TLS_VERSION_12, nil
+
+	case utls.HelloChrome_70, utls.HelloChrome_72, utls.HelloFirefox_65,
+		utls.HelloGolang:
+		return protocol.TLS_VERSION_13, nil
+
+	case utls.HelloCustom:
+		if utlsClientHelloSpec.TLSVersMax == utls.VersionTLS12 {
+			return protocol.TLS_VERSION_12, nil
+		}
+		return protocol.TLS_VERSION_13, nil
+	}
 
 
 	// As utls.HelloRandomized may be either TLS 1.2 or TLS 1.3, we cannot
 	// As utls.HelloRandomized may be either TLS 1.2 or TLS 1.3, we cannot
 	// perform a simple ClientHello ID check. BuildHandshakeState is run, which
 	// perform a simple ClientHello ID check. BuildHandshakeState is run, which
 	// constructs the entire ClientHello.
 	// constructs the entire ClientHello.
 	//
 	//
+	// Assumes utlsClientHelloID.Seed has been set; otherwise the result is
+	// ephemeral.
+	//
 	// BenchmarkRandomizedGetClientHelloVersion indicates that this operation
 	// BenchmarkRandomizedGetClientHelloVersion indicates that this operation
 	// takes on the order of 0.05ms and allocates ~8KB for randomized client
 	// takes on the order of 0.05ms and allocates ~8KB for randomized client
 	// hellos.
 	// hellos.
@@ -243,6 +292,21 @@ func getClientHelloVersion(utlsClientHelloID utls.ClientHelloID) (string, error)
 	return protocol.TLS_VERSION_12, nil
 	return protocol.TLS_VERSION_12, nil
 }
 }
 
 
+func isNoDefaultSessionIDCandidate(utlsClientHelloID utls.ClientHelloID) bool {
+
+	// Either TLS 1.2 parrots or any randomized ClientHello is a candidate. This
+	// check doesn't incur the overhead of invoking BuildHandshakeState.
+
+	switch utlsClientHelloID {
+
+	case utls.HelloIOS_11_1, utls.HelloIOS_12_1, utls.HelloChrome_58,
+		utls.HelloChrome_62, utls.HelloFirefox_55, utls.HelloFirefox_56:
+		return true
+	}
+
+	return utlsClientHelloID.Client == "Randomized"
+}
+
 func IsTLSConnUsingHTTP2(conn net.Conn) bool {
 func IsTLSConnUsingHTTP2(conn net.Conn) bool {
 	if c, ok := conn.(*utls.UConn); ok {
 	if c, ok := conn.(*utls.UConn); ok {
 		state := c.ConnectionState()
 		state := c.ConnectionState()
@@ -274,6 +338,8 @@ func CustomTLSDial(
 	network, addr string,
 	network, addr string,
 	config *CustomTLSConfig) (net.Conn, error) {
 	config *CustomTLSConfig) (net.Conn, error) {
 
 
+	p := config.ClientParameters.Get()
+
 	dialAddr := addr
 	dialAddr := addr
 	if config.DialAddr != "" {
 	if config.DialAddr != "" {
 		dialAddr = config.DialAddr
 		dialAddr = config.DialAddr
@@ -293,7 +359,7 @@ func CustomTLSDial(
 	selectedTLSProfile := config.TLSProfile
 	selectedTLSProfile := config.TLSProfile
 
 
 	if selectedTLSProfile == "" {
 	if selectedTLSProfile == "" {
-		selectedTLSProfile = SelectTLSProfile(config.ClientParameters.Get())
+		selectedTLSProfile = SelectTLSProfile(p)
 	}
 	}
 
 
 	tlsConfigInsecureSkipVerify := false
 	tlsConfigInsecureSkipVerify := false
@@ -337,12 +403,14 @@ func CustomTLSDial(
 		ServerName:         tlsConfigServerName,
 		ServerName:         tlsConfigServerName,
 	}
 	}
 
 
-	utlsClientHelloID := getUTLSClientHelloID(selectedTLSProfile)
-
-	isRandomized := protocol.TLSProfileIsRandomized(selectedTLSProfile)
+	utlsClientHelloID, utlsClientHelloSpec, err := getUTLSClientHelloID(
+		p, selectedTLSProfile)
+	if err != nil {
+		return nil, common.ContextError(err)
+	}
 
 
 	var randomizedTLSProfileSeed *prng.Seed
 	var randomizedTLSProfileSeed *prng.Seed
-
+	isRandomized := protocol.TLSProfileIsRandomized(selectedTLSProfile)
 	if isRandomized {
 	if isRandomized {
 
 
 		randomizedTLSProfileSeed = config.RandomizedTLSProfileSeed
 		randomizedTLSProfileSeed = config.RandomizedTLSProfileSeed
@@ -362,8 +430,8 @@ func CustomTLSDial(
 	// As noted here,
 	// As noted here,
 	// https://gitlab.com/yawning/obfs4/commit/ca6765e3e3995144df2b1ca9f0e9d823a7f8a47c,
 	// https://gitlab.com/yawning/obfs4/commit/ca6765e3e3995144df2b1ca9f0e9d823a7f8a47c,
 	// the dynamic record sizing optimization in crypto/tls is not commonly
 	// the dynamic record sizing optimization in crypto/tls is not commonly
-	// implemented in browsers. Disable it for all non-Golang utls parrots and
-	// select it randomly when using the randomized client hello.
+	// implemented in browsers. Disable it for all utls parrots and select it
+	// randomly when using the randomized client hello.
 	if isRandomized {
 	if isRandomized {
 		PRNG, err := prng.NewPRNGWithSaltedSeed(randomizedTLSProfileSeed, "tls-dynamic-record-sizing")
 		PRNG, err := prng.NewPRNGWithSaltedSeed(randomizedTLSProfileSeed, "tls-dynamic-record-sizing")
 		if err != nil {
 		if err != nil {
@@ -371,11 +439,18 @@ func CustomTLSDial(
 		}
 		}
 		tlsConfig.DynamicRecordSizingDisabled = PRNG.FlipCoin()
 		tlsConfig.DynamicRecordSizingDisabled = PRNG.FlipCoin()
 	} else {
 	} else {
-		tlsConfig.DynamicRecordSizingDisabled = (utlsClientHelloID != utls.HelloGolang)
+		tlsConfig.DynamicRecordSizingDisabled = true
 	}
 	}
 
 
 	conn := utls.UClient(rawConn, tlsConfig, utlsClientHelloID)
 	conn := utls.UClient(rawConn, tlsConfig, utlsClientHelloID)
 
 
+	if utlsClientHelloSpec != nil {
+		err = conn.ApplyPreset(utlsClientHelloSpec)
+		if err != nil {
+			return nil, common.ContextError(err)
+		}
+	}
+
 	clientSessionCache := config.clientSessionCache
 	clientSessionCache := config.clientSessionCache
 	if clientSessionCache == nil {
 	if clientSessionCache == nil {
 		clientSessionCache = utls.NewLRUClientSessionCache(0)
 		clientSessionCache = utls.NewLRUClientSessionCache(0)
@@ -383,20 +458,25 @@ func CustomTLSDial(
 
 
 	conn.SetSessionCache(clientSessionCache)
 	conn.SetSessionCache(clientSessionCache)
 
 
-	// Obfuscated session tickets are not currently supported in TLS 1.3, but we
-	// allow UNFRONTED-MEEK-SESSION-TICKET-OSSH to use TLS 1.3 profiles for
-	// additional diversity/capacity; TLS 1.3 encrypts the server certificate,
-	// so the desired obfuscated session tickets property of obfuscating server
-	// certificates is satisfied. We know that when the ClientHello offers TLS
-	// 1.3, the Psiphon server, in these direct protocol cases, will negotiate
-	// it.
 	if config.ObfuscatedSessionTicketKey != "" {
 	if config.ObfuscatedSessionTicketKey != "" {
 
 
-		tlsVersion, err := getClientHelloVersion(utlsClientHelloID)
+		// Since getClientHelloVersion may incur some overhead, only invoke it when
+		// necessary.
+		tlsVersion, err := getClientHelloVersion(utlsClientHelloID, utlsClientHelloSpec)
 		if err != nil {
 		if err != nil {
 			return nil, common.ContextError(err)
 			return nil, common.ContextError(err)
 		}
 		}
 
 
+		// Add the obfuscated session ticket only when using TLS 1.2.
+		//
+		// Obfuscated session tickets are not currently supported in TLS 1.3, but we
+		// allow UNFRONTED-MEEK-SESSION-TICKET-OSSH to use TLS 1.3 profiles for
+		// additional diversity/capacity; TLS 1.3 encrypts the server certificate,
+		// so the desired obfuscated session tickets property of obfuscating server
+		// certificates is satisfied. We know that when the ClientHello offers TLS
+		// 1.3, the Psiphon server, in these direct protocol cases, will negotiate
+		// it.
+
 		if tlsVersion == protocol.TLS_VERSION_12 {
 		if tlsVersion == protocol.TLS_VERSION_12 {
 
 
 			var obfuscatedSessionTicketKey [32]byte
 			var obfuscatedSessionTicketKey [32]byte
@@ -448,6 +528,39 @@ func CustomTLSDial(
 		}
 		}
 	}
 	}
 
 
+	// Build the ClientHello and inspect to apply NoDefaultSessionID logic.
+
+	if len(conn.HandshakeState.Hello.SessionTicket) == 0 &&
+		isNoDefaultSessionIDCandidate(utlsClientHelloID) {
+
+		if !conn.ClientHelloBuilt {
+			err = conn.BuildHandshakeState()
+			if err != nil {
+				return nil, common.ContextError(err)
+			}
+		}
+
+		var noDefaultSessionID bool
+		if config.NoDefaultTLSSessionID != nil {
+			noDefaultSessionID = *config.NoDefaultTLSSessionID
+		} else {
+			noDefaultSessionID = config.ClientParameters.Get().WeightedCoinFlip(
+				parameters.NoDefaultTLSSessionIDProbability)
+		}
+
+		if noDefaultSessionID {
+
+			conn.HandshakeState.Hello.SessionId = nil
+
+			err = conn.MarshalClientHello()
+			if err != nil {
+				return nil, common.ContextError(err)
+			}
+		}
+	}
+
+	// Perform the TLS Handshake.
+
 	resultChannel := make(chan error)
 	resultChannel := make(chan error)
 
 
 	go func() {
 	go func() {

+ 102 - 14
psiphon/tlsDialer_test.go

@@ -21,6 +21,7 @@ package psiphon
 
 
 import (
 import (
 	"context"
 	"context"
+	"encoding/json"
 	"fmt"
 	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"net"
 	"net"
@@ -119,12 +120,12 @@ func testTLSDialerCompatibility(t *testing.T, address string) {
 		return d.DialContext(ctx, network, address)
 		return d.DialContext(ctx, network, address)
 	}
 	}
 
 
-	clientParameters, err := parameters.NewClientParameters(nil)
-	if err != nil {
-		t.Fatalf("%s\n", err)
-	}
+	clientParameters := makeCustomTLSProfilesClientParameters(t)
 
 
-	for _, tlsProfile := range protocol.SupportedTLSProfiles {
+	profiles := append([]string(nil), protocol.SupportedTLSProfiles...)
+	profiles = append(profiles, clientParameters.Get().CustomTLSProfileNames()...)
+
+	for _, tlsProfile := range profiles {
 
 
 		repeats := 1
 		repeats := 1
 		if protocol.TLSProfileIsRandomized(tlsProfile) {
 		if protocol.TLSProfileIsRandomized(tlsProfile) {
@@ -169,10 +170,10 @@ func testTLSDialerCompatibility(t *testing.T, address string) {
 
 
 func TestSelectTLSProfile(t *testing.T) {
 func TestSelectTLSProfile(t *testing.T) {
 
 
-	clientParameters, err := parameters.NewClientParameters(nil)
-	if err != nil {
-		t.Fatalf("%s\n", err)
-	}
+	clientParameters := makeCustomTLSProfilesClientParameters(t)
+
+	profiles := append([]string(nil), protocol.SupportedTLSProfiles...)
+	profiles = append(profiles, clientParameters.Get().CustomTLSProfileNames()...)
 
 
 	selected := make(map[string]int)
 	selected := make(map[string]int)
 
 
@@ -185,7 +186,7 @@ func TestSelectTLSProfile(t *testing.T) {
 
 
 	// All TLS profiles should be selected at least once.
 	// All TLS profiles should be selected at least once.
 
 
-	for _, profile := range protocol.SupportedTLSProfiles {
+	for _, profile := range profiles {
 		if selected[profile] < 1 {
 		if selected[profile] < 1 {
 			t.Errorf("TLS profile %s not selected", profile)
 			t.Errorf("TLS profile %s not selected", profile)
 		}
 		}
@@ -214,9 +215,35 @@ func TestSelectTLSProfile(t *testing.T) {
 
 
 	// getUTLSClientHelloID should map each TLS profile to a utls ClientHelloID.
 	// getUTLSClientHelloID should map each TLS profile to a utls ClientHelloID.
 
 
-	for _, profile := range protocol.SupportedTLSProfiles {
-		if getUTLSClientHelloID(profile) == utls.HelloGolang {
-			t.Errorf("TLS profile %s has no utls ClientHelloID", profile)
+	for i, profile := range profiles {
+		utlsClientHelloID, utlsClientHelloSpec, err :=
+			getUTLSClientHelloID(clientParameters.Get(), profile)
+		if err != nil {
+			t.Fatalf("getUTLSClientHelloID failed: %s\n", err)
+		}
+
+		var unexpectedClientHelloID, unexpectedClientHelloSpec bool
+		if i < len(protocol.SupportedTLSProfiles) {
+			if utlsClientHelloID == utls.HelloCustom {
+				unexpectedClientHelloID = true
+			}
+			if utlsClientHelloSpec != nil {
+				unexpectedClientHelloSpec = true
+			}
+		} else {
+			if utlsClientHelloID != utls.HelloCustom {
+				unexpectedClientHelloID = true
+			}
+			if utlsClientHelloSpec == nil {
+				unexpectedClientHelloSpec = true
+			}
+		}
+
+		if unexpectedClientHelloID {
+			t.Errorf("Unexpected ClientHelloID for TLS profile %s", profile)
+		}
+		if unexpectedClientHelloSpec {
+			t.Errorf("Unexpected ClientHelloSpec for TLS profile %s", profile)
 		}
 		}
 	}
 	}
 }
 }
@@ -225,6 +252,67 @@ func BenchmarkRandomizedGetClientHelloVersion(b *testing.B) {
 	for n := 0; n < b.N; n++ {
 	for n := 0; n < b.N; n++ {
 		utlsClientHelloID := utls.HelloRandomized
 		utlsClientHelloID := utls.HelloRandomized
 		utlsClientHelloID.Seed, _ = utls.NewPRNGSeed()
 		utlsClientHelloID.Seed, _ = utls.NewPRNGSeed()
-		getClientHelloVersion(utlsClientHelloID)
+		getClientHelloVersion(utlsClientHelloID, nil)
+	}
+}
+
+func makeCustomTLSProfilesClientParameters(
+	t *testing.T) *parameters.ClientParameters {
+
+	clientParameters, err := parameters.NewClientParameters(nil)
+	if err != nil {
+		t.Fatalf("NewClientParameters failed: %s\n", err)
+	}
+
+	// Equivilent to utls.HelloChrome_62
+	customTLSProfilesJSON := []byte(`
+    [
+      {
+        "Name": "CustomProfile",
+        "UTLSSpec": {
+          "TLSVersMax": 771,
+          "TLSVersMin": 769,
+          "CipherSuites": [2570, 49195, 49199, 49196, 49200, 52393, 52392, 49171, 49172, 156, 157, 47, 53, 10],
+          "CompressionMethods": [0],
+          "Extensions" : [
+            {"Name": "GREASE"},
+            {"Name": "SNI"},
+            {"Name": "ExtendedMasterSecret"},
+            {"Name": "SessionTicket"},
+            {"Name": "SignatureAlgorithms", "Data": {"SupportedSignatureAlgorithms": [1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513]}},
+            {"Name": "StatusRequest"},
+            {"Name": "SCT"},
+            {"Name": "ALPN", "Data": {"AlpnProtocols": ["h2", "http/1.1"]}},
+            {"Name": "ChannelID"},
+            {"Name": "SupportedPoints", "Data": {"SupportedPoints": [0]}},
+            {"Name": "SupportedCurves", "Data": {"Curves": [2570, 29, 23, 24]}},
+            {"Name": "BoringPadding"},
+            {"Name": "GREASE"}],
+          "GetSessionID" : "SHA-256"
+        }
+      }
+    ]`)
+
+	var customTLSProfiles protocol.CustomTLSProfiles
+
+	err = json.Unmarshal(customTLSProfilesJSON, &customTLSProfiles)
+	if err != nil {
+		t.Fatalf("Unmarshal failed: %s", err)
 	}
 	}
+
+	applyParameters := make(map[string]interface{})
+
+	applyParameters[parameters.CustomTLSProfiles] = customTLSProfiles
+
+	_, err = clientParameters.Set("", false, applyParameters)
+	if err != nil {
+		t.Fatalf("Set failed: %s", err)
+	}
+
+	customTLSProfileNames := clientParameters.Get().CustomTLSProfileNames()
+	if len(customTLSProfileNames) != 1 {
+		t.Fatalf("Unexpected CustomTLSProfileNames count")
+	}
+
+	return clientParameters
 }
 }

+ 8 - 8
vendor/github.com/refraction-networking/utls/u_parrots.go

@@ -39,7 +39,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
 			CompressionMethods: []byte{compressionNone},
 			CompressionMethods: []byte{compressionNone},
 			Extensions: []TLSExtension{
 			Extensions: []TLSExtension{
 				&UtlsGREASEExtension{},
 				&UtlsGREASEExtension{},
-				&RenegotiationInfoExtension{renegotiation: RenegotiateOnceAsClient},
+				&RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient},
 				&SNIExtension{},
 				&SNIExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&SessionTicketExtension{},
 				&SessionTicketExtension{},
@@ -94,7 +94,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
 			},
 			},
 			Extensions: []TLSExtension{
 			Extensions: []TLSExtension{
 				&UtlsGREASEExtension{},
 				&UtlsGREASEExtension{},
-				&RenegotiationInfoExtension{renegotiation: RenegotiateOnceAsClient},
+				&RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient},
 				&SNIExtension{},
 				&SNIExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&SessionTicketExtension{},
 				&SessionTicketExtension{},
@@ -166,7 +166,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
 				&UtlsGREASEExtension{},
 				&UtlsGREASEExtension{},
 				&SNIExtension{},
 				&SNIExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&UtlsExtendedMasterSecretExtension{},
-				&RenegotiationInfoExtension{renegotiation: RenegotiateOnceAsClient},
+				&RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient},
 				&SupportedCurvesExtension{[]CurveID{
 				&SupportedCurvesExtension{[]CurveID{
 					CurveID(GREASE_PLACEHOLDER),
 					CurveID(GREASE_PLACEHOLDER),
 					X25519,
 					X25519,
@@ -237,7 +237,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
 			Extensions: []TLSExtension{
 			Extensions: []TLSExtension{
 				&SNIExtension{},
 				&SNIExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&UtlsExtendedMasterSecretExtension{},
-				&RenegotiationInfoExtension{renegotiation: RenegotiateOnceAsClient},
+				&RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient},
 				&SupportedCurvesExtension{[]CurveID{X25519, CurveP256, CurveP384, CurveP521}},
 				&SupportedCurvesExtension{[]CurveID{X25519, CurveP256, CurveP384, CurveP521}},
 				&SupportedPointsExtension{SupportedPoints: []byte{pointFormatUncompressed}},
 				&SupportedPointsExtension{SupportedPoints: []byte{pointFormatUncompressed}},
 				&SessionTicketExtension{},
 				&SessionTicketExtension{},
@@ -290,7 +290,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
 			Extensions: []TLSExtension{
 			Extensions: []TLSExtension{
 				&SNIExtension{},
 				&SNIExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&UtlsExtendedMasterSecretExtension{},
-				&RenegotiationInfoExtension{renegotiation: RenegotiateOnceAsClient},
+				&RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient},
 				&SupportedCurvesExtension{[]CurveID{
 				&SupportedCurvesExtension{[]CurveID{
 					X25519,
 					X25519,
 					CurveP256,
 					CurveP256,
@@ -361,7 +361,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
 				compressionNone,
 				compressionNone,
 			},
 			},
 			Extensions: []TLSExtension{
 			Extensions: []TLSExtension{
-				&RenegotiationInfoExtension{renegotiation: RenegotiateOnceAsClient},
+				&RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient},
 				&SNIExtension{},
 				&SNIExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []SignatureScheme{
 				&SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []SignatureScheme{
@@ -421,7 +421,7 @@ func utlsIdToSpec(id ClientHelloID) (ClientHelloSpec, error) {
 				compressionNone,
 				compressionNone,
 			},
 			},
 			Extensions: []TLSExtension{
 			Extensions: []TLSExtension{
-				&RenegotiationInfoExtension{renegotiation: RenegotiateOnceAsClient},
+				&RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient},
 				&SNIExtension{},
 				&SNIExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&UtlsExtendedMasterSecretExtension{},
 				&SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []SignatureScheme{
 				&SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []SignatureScheme{
@@ -722,7 +722,7 @@ func (uconn *UConn) generateRandomizedSpec() (ClientHelloSpec, error) {
 	curves := SupportedCurvesExtension{curveIDs}
 	curves := SupportedCurvesExtension{curveIDs}
 
 
 	padding := UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle}
 	padding := UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle}
-	reneg := RenegotiationInfoExtension{renegotiation: RenegotiateOnceAsClient}
+	reneg := RenegotiationInfoExtension{Renegotiation: RenegotiateOnceAsClient}
 
 
 	p.Extensions = []TLSExtension{
 	p.Extensions = []TLSExtension{
 		&sni,
 		&sni,

+ 26 - 49
vendor/github.com/refraction-networking/utls/u_tls_extensions.go

@@ -194,20 +194,18 @@ func (e *SignatureAlgorithmsExtension) Read(b []byte) (int, error) {
 }
 }
 
 
 type RenegotiationInfoExtension struct {
 type RenegotiationInfoExtension struct {
-	renegotiation       RenegotiationSupport
-	SecureRenegotiation []byte // if empty, default []byte{0} is assumed
+	// Renegotiation field limits how many times client will perform renegotiation: no limit, once, or never.
+	// The extension still will be sent, even if Renegotiation is set to RenegotiateNever.
+	Renegotiation RenegotiationSupport
 }
 }
 
 
 func (e *RenegotiationInfoExtension) writeToUConn(uc *UConn) error {
 func (e *RenegotiationInfoExtension) writeToUConn(uc *UConn) error {
-	uc.config.Renegotiation = e.renegotiation
-	switch e.renegotiation {
+	uc.config.Renegotiation = e.Renegotiation
+	switch e.Renegotiation {
 	case RenegotiateOnceAsClient:
 	case RenegotiateOnceAsClient:
 		fallthrough
 		fallthrough
 	case RenegotiateFreelyAsClient:
 	case RenegotiateFreelyAsClient:
 		uc.HandshakeState.Hello.SecureRenegotiationSupported = true
 		uc.HandshakeState.Hello.SecureRenegotiationSupported = true
-		// Note that if we manage to use this in renegotiation(currently only in initial handshake), we'd have to point
-		// uc.ClientHelloMsg.SecureRenegotiation = chs.C.clientFinished
-		// and probably do something else. It's a mess.
 	case RenegotiateNever:
 	case RenegotiateNever:
 	default:
 	default:
 	}
 	}
@@ -215,47 +213,25 @@ func (e *RenegotiationInfoExtension) writeToUConn(uc *UConn) error {
 }
 }
 
 
 func (e *RenegotiationInfoExtension) Len() int {
 func (e *RenegotiationInfoExtension) Len() int {
-	switch e.renegotiation {
-	case RenegotiateOnceAsClient:
-		fallthrough
-	case RenegotiateFreelyAsClient:
-		extBodyLen := len(e.SecureRenegotiation)
-		if extBodyLen == 0 {
-			extBodyLen = 1
-		}
-		return 4 + extBodyLen
-	case RenegotiateNever:
-	default:
-	}
-	return 0
+	return 5
 }
 }
 
 
 func (e *RenegotiationInfoExtension) Read(b []byte) (int, error) {
 func (e *RenegotiationInfoExtension) Read(b []byte) (int, error) {
 	if len(b) < e.Len() {
 	if len(b) < e.Len() {
 		return 0, io.ErrShortBuffer
 		return 0, io.ErrShortBuffer
 	}
 	}
-	switch e.renegotiation {
-	case RenegotiateOnceAsClient:
-		fallthrough
-	case RenegotiateFreelyAsClient:
-		secureRenegBody := e.SecureRenegotiation
-		if len(secureRenegBody) == 0 {
-			secureRenegBody = []byte{0}
-		}
-		extBodyLen := len(secureRenegBody)
 
 
-		b[0] = byte(extensionRenegotiationInfo >> 8)
-		b[1] = byte(extensionRenegotiationInfo & 0xff)
-		b[2] = byte(extBodyLen >> 8)
-		b[3] = byte(extBodyLen)
-		copy(b[4:], secureRenegBody)
+	var extInnerBody []byte // inner body is empty
+	innerBodyLen := len(extInnerBody)
+	extBodyLen := innerBodyLen + 1
+
+	b[0] = byte(extensionRenegotiationInfo >> 8)
+	b[1] = byte(extensionRenegotiationInfo & 0xff)
+	b[2] = byte(extBodyLen >> 8)
+	b[3] = byte(extBodyLen)
+	b[4] = byte(innerBodyLen)
+	copy(b[5:], extInnerBody)
 
 
-		if len(e.SecureRenegotiation) != 0 {
-			copy(b[5:], e.SecureRenegotiation)
-		}
-	case RenegotiateNever:
-	default:
-	}
 	return e.Len(), io.EOF
 	return e.Len(), io.EOF
 }
 }
 
 
@@ -364,9 +340,10 @@ func (e *SessionTicketExtension) Read(b []byte) (int, error) {
 	return e.Len(), io.EOF
 	return e.Len(), io.EOF
 }
 }
 
 
+// GenericExtension allows to include in ClientHello arbitrary unsupported extensions.
 type GenericExtension struct {
 type GenericExtension struct {
-	id   uint16
-	data []byte
+	Id   uint16
+	Data []byte
 }
 }
 
 
 func (e *GenericExtension) writeToUConn(uc *UConn) error {
 func (e *GenericExtension) writeToUConn(uc *UConn) error {
@@ -374,7 +351,7 @@ func (e *GenericExtension) writeToUConn(uc *UConn) error {
 }
 }
 
 
 func (e *GenericExtension) Len() int {
 func (e *GenericExtension) Len() int {
-	return 4 + len(e.data)
+	return 4 + len(e.Data)
 }
 }
 
 
 func (e *GenericExtension) Read(b []byte) (int, error) {
 func (e *GenericExtension) Read(b []byte) (int, error) {
@@ -382,12 +359,12 @@ func (e *GenericExtension) Read(b []byte) (int, error) {
 		return 0, io.ErrShortBuffer
 		return 0, io.ErrShortBuffer
 	}
 	}
 
 
-	b[0] = byte(e.id >> 8)
-	b[1] = byte(e.id)
-	b[2] = byte(len(e.data) >> 8)
-	b[3] = byte(len(e.data))
-	if len(e.data) > 0 {
-		copy(b[4:], e.data)
+	b[0] = byte(e.Id >> 8)
+	b[1] = byte(e.Id)
+	b[2] = byte(len(e.Data) >> 8)
+	b[3] = byte(len(e.Data))
+	if len(e.Data) > 0 {
+		copy(b[4:], e.Data)
 	}
 	}
 	return e.Len(), io.EOF
 	return e.Len(), io.EOF
 }
 }

+ 3 - 3
vendor/vendor.json

@@ -488,10 +488,10 @@
 			"tree": true
 			"tree": true
 		},
 		},
 		{
 		{
-			"checksumSHA1": "wfvFeuJFOZ358JKzBIU5h899M/c=",
+			"checksumSHA1": "S5FV5qBbMksx0vxajWD94J2KeHw=",
 			"path": "github.com/refraction-networking/utls",
 			"path": "github.com/refraction-networking/utls",
-			"revision": "4da67951864128358459681399dd208c49d5d001",
-			"revisionTime": "2019-08-12T21:06:06Z"
+			"revision": "1552a980cef9b4c9b088a1c8fe33c774c031c295",
+			"revisionTime": "2019-08-22T22:33:47Z"
 		},
 		},
 		{
 		{
 			"checksumSHA1": "Fn9JW8u40ABN9Uc9wuvquuyOB+8=",
 			"checksumSHA1": "Fn9JW8u40ABN9Uc9wuvquuyOB+8=",