Преглед изворни кода

Add fronting provider ID and related tactics

- Allows disabling incompatible TLS/QUIC versions by
  fronting provider.

- Provides a simpler method by which to query metrics
  by fronting provider.
Rod Hynes пре 6 година
родитељ
комит
43f105c861

+ 62 - 3
psiphon/common/parameters/clientParameters.go

@@ -101,8 +101,10 @@ const (
 	CustomTLSProfiles                                = "CustomTLSProfiles"
 	SelectRandomizedTLSProfileProbability            = "SelectRandomizedTLSProfileProbability"
 	NoDefaultTLSSessionIDProbability                 = "NoDefaultTLSSessionIDProbability"
+	DisableFrontingProviderTLSProfiles               = "DisableFrontingProviderTLSProfiles"
 	LimitQUICVersionsProbability                     = "LimitQUICVersionsProbability"
 	LimitQUICVersions                                = "LimitQUICVersions"
+	DisableFrontingProviderQUICVersions              = "DisableFrontingProviderQUICVersions"
 	FragmentorProbability                            = "FragmentorProbability"
 	FragmentorLimitProtocols                         = "FragmentorLimitProtocols"
 	FragmentorMinTotalBytes                          = "FragmentorMinTotalBytes"
@@ -302,9 +304,11 @@ var defaultClientParameters = map[string]struct {
 	CustomTLSProfiles:                     {value: protocol.CustomTLSProfiles{}},
 	SelectRandomizedTLSProfileProbability: {value: 0.25, minimum: 0.0},
 	NoDefaultTLSSessionIDProbability:      {value: 0.5, minimum: 0.0},
+	DisableFrontingProviderTLSProfiles:    {value: protocol.LabeledTLSProfiles{}},
 
-	LimitQUICVersionsProbability: {value: 1.0, minimum: 0.0},
-	LimitQUICVersions:            {value: protocol.QUICVersions{}},
+	LimitQUICVersionsProbability:        {value: 1.0, minimum: 0.0},
+	LimitQUICVersions:                   {value: protocol.QUICVersions{}},
+	DisableFrontingProviderQUICVersions: {value: protocol.LabeledQUICVersions{}},
 
 	FragmentorProbability:              {value: 0.5, minimum: 0.0},
 	FragmentorLimitProtocols:           {value: protocol.TunnelProtocols{}},
@@ -572,6 +576,24 @@ func (p *ClientParameters) Set(
 		return nil, errors.Trace(err)
 	}
 
+	// Special case: TLSProfiles/LabeledTLSProfiles may reference CustomTLSProfiles names.
+	// Inspect the CustomTLSProfiles parameter and extract its names. Do not
+	// call Get().CustomTLSProfilesNames() as CustomTLSProfiles may not yet be
+	// validated.
+
+	var customTLSProfileNames []string
+
+	customTLSProfilesValue := parameters[CustomTLSProfiles]
+	for i := 0; i < len(applyParameters); i++ {
+		customTLSProfilesValue = applyParameters[i][CustomTLSProfiles]
+	}
+	if customTLSProfiles, ok := customTLSProfilesValue.(protocol.CustomTLSProfiles); ok {
+		customTLSProfileNames := make([]string, len(customTLSProfiles))
+		for i := 0; i < len(customTLSProfiles); i++ {
+			customTLSProfileNames[i] = customTLSProfiles[i].Name
+		}
+	}
+
 	for i := 0; i < len(applyParameters); i++ {
 
 		count := 0
@@ -644,6 +666,25 @@ func (p *ClientParameters) Set(
 					}
 				}
 			case protocol.TLSProfiles:
+				if skipOnError {
+					newValue = v.PruneInvalid(customTLSProfileNames)
+				} else {
+					err := v.Validate(customTLSProfileNames)
+					if err != nil {
+						return nil, errors.Trace(err)
+					}
+				}
+			case protocol.LabeledTLSProfiles:
+
+				if skipOnError {
+					newValue = v.PruneInvalid(customTLSProfileNames)
+				} else {
+					err := v.Validate(customTLSProfileNames)
+					if err != nil {
+						return nil, errors.Trace(err)
+					}
+				}
+			case protocol.QUICVersions:
 				if skipOnError {
 					newValue = v.PruneInvalid()
 				} else {
@@ -652,7 +693,7 @@ func (p *ClientParameters) Set(
 						return nil, errors.Trace(err)
 					}
 				}
-			case protocol.QUICVersions:
+			case protocol.LabeledQUICVersions:
 				if skipOnError {
 					newValue = v.PruneInvalid()
 				} else {
@@ -984,6 +1025,15 @@ func (p ClientParametersAccessor) TLSProfiles(name string) protocol.TLSProfiles
 	return value
 }
 
+// LabeledTLSProfiles returns a protocol.TLSProfiles parameter value
+// corresponding to the specified labeled set and label value. The return
+// value is nil when no set is found.
+func (p ClientParametersAccessor) LabeledTLSProfiles(name, label string) protocol.TLSProfiles {
+	var value protocol.LabeledTLSProfiles
+	p.snapshot.getValue(name, &value)
+	return value[label]
+}
+
 // QUICVersions returns a protocol.QUICVersions parameter value.
 // If there is a corresponding Probability value, a weighted coin flip
 // will be performed and, depending on the result, the value or the
@@ -1013,6 +1063,15 @@ func (p ClientParametersAccessor) QUICVersions(name string) protocol.QUICVersion
 	return value
 }
 
+// LabeledQUICVersions returns a protocol.QUICVersions parameter value
+// corresponding to the specified labeled set and label value. The return
+// value is nil when no set is found.
+func (p ClientParametersAccessor) LabeledQUICVersions(name, label string) protocol.QUICVersions {
+	var value protocol.LabeledQUICVersions
+	p.snapshot.getValue(name, &value)
+	return value[label]
+}
+
 // DownloadURLs returns a DownloadURLs parameter value.
 func (p ClientParametersAccessor) DownloadURLs(name string) DownloadURLs {
 	value := DownloadURLs{}

+ 65 - 0
psiphon/common/parameters/clientParameters_test.go

@@ -74,11 +74,25 @@ func TestGetDefaultParameters(t *testing.T) {
 			if !reflect.DeepEqual(v, g) {
 				t.Fatalf("TLSProfiles returned %+v expected %+v", g, v)
 			}
+		case protocol.LabeledTLSProfiles:
+			for label, profiles := range v {
+				g := p.Get().LabeledTLSProfiles(name, label)
+				if !reflect.DeepEqual(profiles, g) {
+					t.Fatalf("LabeledTLSProfiles returned %+v expected %+v", g, profiles)
+				}
+			}
 		case protocol.QUICVersions:
 			g := p.Get().QUICVersions(name)
 			if !reflect.DeepEqual(v, g) {
 				t.Fatalf("QUICVersions returned %+v expected %+v", g, v)
 			}
+		case protocol.LabeledQUICVersions:
+			for label, versions := range v {
+				g := p.Get().LabeledTLSProfiles(name, label)
+				if !reflect.DeepEqual(versions, g) {
+					t.Fatalf("LabeledQUICVersions returned %+v expected %+v", g, versions)
+				}
+			}
 		case DownloadURLs:
 			g := p.Get().DownloadURLs(name)
 			if !reflect.DeepEqual(v, g) {
@@ -305,6 +319,57 @@ func TestLimitTunnelProtocolProbability(t *testing.T) {
 	}
 }
 
+func TestLabeledLists(t *testing.T) {
+	p, err := NewClientParameters(nil)
+	if err != nil {
+		t.Fatalf("NewClientParameters failed: %s", err)
+	}
+
+	tlsProfiles := make(protocol.TLSProfiles, 0)
+	for i, tlsProfile := range protocol.SupportedTLSProfiles {
+		if i%2 == 0 {
+			tlsProfiles = append(tlsProfiles, tlsProfile)
+		}
+	}
+
+	quicVersions := make(protocol.QUICVersions, 0)
+	for i, quicVersion := range protocol.SupportedQUICVersions {
+		if i%2 == 0 {
+			quicVersions = append(quicVersions, quicVersion)
+		}
+	}
+
+	applyParameters := map[string]interface{}{
+		"DisableFrontingProviderTLSProfiles":  protocol.LabeledTLSProfiles{"validLabel": tlsProfiles},
+		"DisableFrontingProviderQUICVersions": protocol.LabeledQUICVersions{"validLabel": quicVersions},
+	}
+
+	_, err = p.Set("", false, applyParameters)
+	if err != nil {
+		t.Fatalf("Set failed: %s", err)
+	}
+
+	disableTLSProfiles := p.Get().LabeledTLSProfiles(DisableFrontingProviderTLSProfiles, "validLabel")
+	if !reflect.DeepEqual(disableTLSProfiles, tlsProfiles) {
+		t.Fatalf("LabeledTLSProfiles returned %+v expected %+v", disableTLSProfiles, tlsProfiles)
+	}
+
+	disableTLSProfiles = p.Get().LabeledTLSProfiles(DisableFrontingProviderTLSProfiles, "invalidLabel")
+	if disableTLSProfiles != nil {
+		t.Fatalf("LabeledTLSProfiles returned unexpected non-empty list %+v", disableTLSProfiles)
+	}
+
+	disableQUICVersions := p.Get().LabeledQUICVersions(DisableFrontingProviderQUICVersions, "validLabel")
+	if !reflect.DeepEqual(disableQUICVersions, quicVersions) {
+		t.Fatalf("LabeledQUICVersions returned %+v expected %+v", disableQUICVersions, quicVersions)
+	}
+
+	disableQUICVersions = p.Get().LabeledQUICVersions(DisableFrontingProviderQUICVersions, "invalidLabel")
+	if disableQUICVersions != nil {
+		t.Fatalf("LabeledQUICVersions returned unexpected non-empty list %+v", disableQUICVersions)
+	}
+}
+
 func TestCustomTLSProfiles(t *testing.T) {
 	p, err := NewClientParameters(nil)
 	if err != nil {

+ 47 - 4
psiphon/common/protocol/protocol.go

@@ -281,26 +281,49 @@ func TLSProfileIsRandomized(tlsProfile string) bool {
 
 type TLSProfiles []string
 
-func (profiles TLSProfiles) Validate() error {
+func (profiles TLSProfiles) Validate(customTLSProfiles []string) error {
 
 	for _, p := range profiles {
-		if !common.Contains(SupportedTLSProfiles, p) && !common.Contains(legacyTLSProfiles, p) {
+		if !common.Contains(SupportedTLSProfiles, p) &&
+			!common.Contains(customTLSProfiles, p) &&
+			!common.Contains(legacyTLSProfiles, p) {
 			return errors.Tracef("invalid TLS profile: %s", p)
 		}
 	}
 	return nil
 }
 
-func (profiles TLSProfiles) PruneInvalid() TLSProfiles {
+func (profiles TLSProfiles) PruneInvalid(customTLSProfiles []string) TLSProfiles {
 	q := make(TLSProfiles, 0)
 	for _, p := range profiles {
-		if common.Contains(SupportedTLSProfiles, p) {
+		if common.Contains(SupportedTLSProfiles, p) ||
+			common.Contains(customTLSProfiles, p) {
 			q = append(q, p)
 		}
 	}
 	return q
 }
 
+type LabeledTLSProfiles map[string]TLSProfiles
+
+func (labeledProfiles LabeledTLSProfiles) Validate(customTLSProfiles []string) error {
+	for _, profiles := range labeledProfiles {
+		err := profiles.Validate(customTLSProfiles)
+		if err != nil {
+			return errors.Trace(err)
+		}
+	}
+	return nil
+}
+
+func (labeledProfiles LabeledTLSProfiles) PruneInvalid(customTLSProfiles []string) LabeledTLSProfiles {
+	l := make(LabeledTLSProfiles)
+	for label, profiles := range labeledProfiles {
+		l[label] = profiles.PruneInvalid(customTLSProfiles)
+	}
+	return l
+}
+
 const (
 	QUIC_VERSION_GQUIC39      = "gQUICv39"
 	QUIC_VERSION_GQUIC43      = "gQUICv43"
@@ -342,6 +365,26 @@ func (versions QUICVersions) PruneInvalid() QUICVersions {
 	return u
 }
 
+type LabeledQUICVersions map[string]QUICVersions
+
+func (labeledVersions LabeledQUICVersions) Validate() error {
+	for _, versions := range labeledVersions {
+		err := versions.Validate()
+		if err != nil {
+			return errors.Trace(err)
+		}
+	}
+	return nil
+}
+
+func (labeledVersions LabeledQUICVersions) PruneInvalid() LabeledQUICVersions {
+	l := make(LabeledQUICVersions)
+	for label, versions := range labeledVersions {
+		l[label] = versions.PruneInvalid()
+	}
+	return l
+}
+
 type HandshakeResponse struct {
 	SSHSessionID           string              `json:"ssh_session_id"`
 	Homepages              []string            `json:"homepages"`

+ 30 - 4
psiphon/common/protocol/protocol_test.go

@@ -54,17 +54,33 @@ func TestTunnelProtocolValidation(t *testing.T) {
 
 func TestTLSProfileValidation(t *testing.T) {
 
-	err := SupportedTLSProfiles.Validate()
+	// Test: valid profiles
+
+	err := SupportedTLSProfiles.Validate(nil)
 	if err != nil {
 		t.Errorf("unexpected Validate error: %s", err)
 	}
 
-	invalidProfiles := TLSProfiles{"OSSH", "INVALID-PROTOCOL"}
-	err = invalidProfiles.Validate()
+	// Test: invalid profile
+
+	profiles := TLSProfiles{TLS_PROFILE_RANDOMIZED, "INVALID-TLS-PROFILE"}
+	err = profiles.Validate(nil)
 	if err == nil {
 		t.Errorf("unexpected Validate success")
 	}
 
+	// Test: valid custom profile
+
+	customProfiles := []string{"CUSTOM-TLS-PROFILE"}
+
+	profiles = TLSProfiles{TLS_PROFILE_RANDOMIZED, "CUSTOM-TLS-PROFILE"}
+	err = profiles.Validate(customProfiles)
+	if err != nil {
+		t.Errorf("unexpected Validate error: %s", err)
+	}
+
+	// Test: prune invalid profiles
+
 	pruneProfiles := make(TLSProfiles, 0)
 	for i, p := range SupportedTLSProfiles {
 		pruneProfiles = append(pruneProfiles, fmt.Sprintf("INVALID-PROFILE-%d", i))
@@ -72,9 +88,19 @@ func TestTLSProfileValidation(t *testing.T) {
 	}
 	pruneProfiles = append(pruneProfiles, fmt.Sprintf("INVALID-PROFILE-%d", len(SupportedTLSProfiles)))
 
-	prunedProfiles := pruneProfiles.PruneInvalid()
+	prunedProfiles := pruneProfiles.PruneInvalid(nil)
 
 	if !reflect.DeepEqual(prunedProfiles, SupportedTLSProfiles) {
 		t.Errorf("unexpected %+v != %+v", prunedProfiles, SupportedTLSProfiles)
 	}
+
+	// Test: don't prune valid custom profiles
+
+	pruneProfiles = TLSProfiles{TLS_PROFILE_RANDOMIZED, "CUSTOM-TLS-PROFILE"}
+
+	prunedProfiles = pruneProfiles.PruneInvalid(customProfiles)
+
+	if !reflect.DeepEqual(prunedProfiles, pruneProfiles) {
+		t.Errorf("unexpected %+v != %+v", prunedProfiles, pruneProfiles)
+	}
 }

+ 1 - 0
psiphon/common/protocol/serverEntry.go

@@ -59,6 +59,7 @@ type ServerEntry struct {
 	SshObfuscatedKey              string   `json:"sshObfuscatedKey"`
 	Capabilities                  []string `json:"capabilities"`
 	Region                        string   `json:"region"`
+	FrontingProviderID            string   `json:"frontingProviderID"`
 	MeekServerPort                int      `json:"meekServerPort"`
 	MeekCookieEncryptionPublicKey string   `json:"meekCookieEncryptionPublicKey"`
 	MeekObfuscatedKey             string   `json:"meekObfuscatedKey"`

+ 27 - 8
psiphon/dialParameters.go

@@ -87,6 +87,8 @@ type DialParameters struct {
 
 	FragmentorSeed *prng.Seed
 
+	FrontingProviderID string
+
 	MeekFrontingDialAddress   string
 	MeekFrontingHost          string
 	MeekDialAddress           string
@@ -370,8 +372,10 @@ func MakeDialParameters(
 	if (!isReplay || !replayTLSProfile) &&
 		protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol) {
 
+		isFronted := protocol.TunnelProtocolUsesFrontedMeek(dialParams.TunnelProtocol)
+
 		dialParams.SelectedTLSProfile = true
-		dialParams.TLSProfile = SelectTLSProfile(p)
+		dialParams.TLSProfile = SelectTLSProfile(isFronted, serverEntry.FrontingProviderID, p)
 		dialParams.NoDefaultTLSSessionID = p.WeightedCoinFlip(
 			parameters.NoDefaultTLSSessionIDProbability)
 	}
@@ -416,6 +420,8 @@ func MakeDialParameters(
 	if (!isReplay || !replayFronting) &&
 		protocol.TunnelProtocolUsesFrontedMeek(dialParams.TunnelProtocol) {
 
+		dialParams.FrontingProviderID = serverEntry.FrontingProviderID
+
 		dialParams.MeekFrontingDialAddress, dialParams.MeekFrontingHost, err =
 			selectFrontingParameters(serverEntry)
 		if err != nil {
@@ -457,8 +463,8 @@ func MakeDialParameters(
 	if (!isReplay || !replayQUICVersion) &&
 		protocol.TunnelProtocolUsesQUIC(dialParams.TunnelProtocol) {
 
-		allowObfuscatedQUIC := !protocol.TunnelProtocolUsesFrontedMeekQUIC(dialParams.TunnelProtocol)
-		dialParams.QUICVersion = selectQUICVersion(allowObfuscatedQUIC, p)
+		isFronted := protocol.TunnelProtocolUsesFrontedMeekQUIC(dialParams.TunnelProtocol)
+		dialParams.QUICVersion = selectQUICVersion(isFronted, serverEntry.FrontingProviderID, p)
 	}
 
 	if (!isReplay || !replayObfuscatedQUIC) &&
@@ -890,10 +896,25 @@ func selectFrontingParameters(serverEntry *protocol.ServerEntry) (string, string
 	return frontingDialHost, frontingHost, nil
 }
 
-func selectQUICVersion(allowObfuscatedQUIC bool, p parameters.ClientParametersAccessor) string {
+func selectQUICVersion(
+	isFronted bool,
+	frontingProviderID string,
+	p parameters.ClientParametersAccessor) string {
 
 	limitQUICVersions := p.QUICVersions(parameters.LimitQUICVersions)
 
+	var disableQUICVersions protocol.QUICVersions
+
+	if isFronted {
+		if frontingProviderID == "" {
+			// Legacy server entry case
+			disableQUICVersions = protocol.QUICVersions{protocol.QUIC_VERSION_IETF_DRAFT24}
+		} else {
+			disableQUICVersions = p.LabeledQUICVersions(
+				parameters.DisableFrontingProviderQUICVersions, frontingProviderID)
+		}
+	}
+
 	quicVersions := make([]string, 0)
 
 	for _, quicVersion := range protocol.SupportedQUICVersions {
@@ -903,14 +924,12 @@ func selectQUICVersion(allowObfuscatedQUIC bool, p parameters.ClientParametersAc
 			continue
 		}
 
-		if !allowObfuscatedQUIC &&
+		if !isFronted &&
 			protocol.QUICVersionIsObfuscated(quicVersion) {
 			continue
 		}
 
-		// Temporary: disallow IETF QUIC where OBFUSCATED is disallowed.
-		if !allowObfuscatedQUIC &&
-			quicVersion == protocol.QUIC_VERSION_IETF_DRAFT24 {
+		if common.Contains(disableQUICVersions, quicVersion) {
 			continue
 		}
 

+ 4 - 0
psiphon/notice.go

@@ -453,6 +453,10 @@ func noticeWithDialParameters(noticeType string, dialParams *DialParameters) {
 			args = append(args, "upstreamProxyCustomHeaderNames", strings.Join(dialParams.UpstreamProxyCustomHeaderNames, ","))
 		}
 
+		if dialParams.FrontingProviderID != "" {
+			args = append(args, "frontingProviderID", dialParams.FrontingProviderID)
+		}
+
 		if dialParams.MeekDialAddress != "" {
 			args = append(args, "meekDialAddress", dialParams.MeekDialAddress)
 		}

+ 1 - 0
psiphon/server/api.go

@@ -716,6 +716,7 @@ var baseRequestParams = []requestParamSpec{
 	{"ssh_client_version", isAnyString, requestParamOptional},
 	{"upstream_proxy_type", isUpstreamProxyType, requestParamOptional},
 	{"upstream_proxy_custom_header_names", isAnyString, requestParamOptional | requestParamArray},
+	{"fronting_provider_id", isAnyString, requestParamOptional | requestParamLogOnlyForFrontedMeek},
 	{"meek_dial_address", isDialAddress, requestParamOptional | requestParamLogOnlyForFrontedMeek},
 	{"meek_resolved_ip_address", isIPAddress, requestParamOptional | requestParamLogOnlyForFrontedMeek},
 	{"meek_sni_server_name", isDomain, requestParamOptional},

+ 4 - 0
psiphon/serverApi.go

@@ -825,6 +825,10 @@ func getBaseAPIParameters(
 		params["upstream_proxy_custom_header_names"] = dialParams.UpstreamProxyCustomHeaderNames
 	}
 
+	if dialParams.FrontingProviderID != "" {
+		params["fronting_provider_id"] = dialParams.FrontingProviderID
+	}
+
 	if dialParams.MeekDialAddress != "" {
 		params["meek_dial_address"] = dialParams.MeekDialAddress
 	}

+ 34 - 8
psiphon/tlsDialer.go

@@ -107,13 +107,15 @@ type CustomTLSConfig struct {
 	// specified certificate. SNI is disbled when this is set.
 	VerifyLegacyCertificate *x509.Certificate
 
-	// TLSProfile specifies a particular indistinguishable TLS profile to use
-	// for the TLS dial. When TLSProfile is "", a profile is selected at
-	// random. Setting TLSProfile allows the caller to pin the selection so
-	// all TLS connections in a certain context (e.g. a single meek
-	// connection) use a consistent value. The value should be selected by
-	// calling SelectTLSProfile, which will pick a value at random, subject to
+	// TLSProfile specifies a particular indistinguishable TLS profile to use for
+	// the TLS dial. Setting TLSProfile allows the caller to pin the selection so
+	// all TLS connections in a certain context (e.g. a single meek connection)
+	// use a consistent value. The value should be selected by calling
+	// SelectTLSProfile, which will pick a value at random, subject to
 	// compatibility constraints.
+	//
+	// When TLSProfile is "", a profile is selected at random and
+	// DisableFrontingProviderTLSProfiles is ignored.
 	TLSProfile string
 
 	// NoDefaultTLSSessionID specifies whether to set a TLS session ID by
@@ -154,6 +156,8 @@ func (config *CustomTLSConfig) EnableClientSessionCache() {
 
 // SelectTLSProfile picks a TLS profile at random from the available candidates.
 func SelectTLSProfile(
+	isFronted bool,
+	frontingProviderID string,
 	p parameters.ClientParametersAccessor) string {
 
 	// Two TLS profile lists are constructed, subject to limit constraints:
@@ -169,11 +173,29 @@ func SelectTLSProfile(
 	// UseOnlyCustomTLSProfiles may be used to disable all stock TLS profiles and
 	// use only CustomTLSProfiles; UseOnlyCustomTLSProfiles is ignored if
 	// CustomTLSProfiles is empty.
+	//
+	// For fronted servers, DisableFrontingProviderTLSProfiles may be used
+	// to disable TLS profiles which are incompatible with the TLS stack used
+	// by the front. For example, if a utls parrot doesn't fully support all
+	// of the capabilities in the ClientHello. Unlike the LimitTLSProfiles case,
+	// DisableFrontingProviderTLSProfiles may disable CustomTLSProfiles.
 
 	limitTLSProfiles := p.TLSProfiles(parameters.LimitTLSProfiles)
+	var disableTLSProfiles protocol.TLSProfiles
+
+	if isFronted && frontingProviderID != "" {
+		disableTLSProfiles = p.LabeledTLSProfiles(
+			parameters.DisableFrontingProviderTLSProfiles, frontingProviderID)
+	}
 
 	randomizedTLSProfiles := make([]string, 0)
-	parrotTLSProfiles := p.CustomTLSProfileNames()
+	parrotTLSProfiles := make([]string, 0)
+
+	for _, tlsProfile := range p.CustomTLSProfileNames() {
+		if !common.Contains(disableTLSProfiles, tlsProfile) {
+			parrotTLSProfiles = append(parrotTLSProfiles, tlsProfile)
+		}
+	}
 
 	useOnlyCustomTLSProfiles := p.Bool(parameters.UseOnlyCustomTLSProfiles)
 	if useOnlyCustomTLSProfiles && len(parrotTLSProfiles) == 0 {
@@ -188,6 +210,10 @@ func SelectTLSProfile(
 				continue
 			}
 
+			if common.Contains(disableTLSProfiles, tlsProfile) {
+				continue
+			}
+
 			if protocol.TLSProfileIsRandomized(tlsProfile) {
 				randomizedTLSProfiles = append(randomizedTLSProfiles, tlsProfile)
 			} else {
@@ -359,7 +385,7 @@ func CustomTLSDial(
 	selectedTLSProfile := config.TLSProfile
 
 	if selectedTLSProfile == "" {
-		selectedTLSProfile = SelectTLSProfile(p)
+		selectedTLSProfile = SelectTLSProfile(false, "", p)
 	}
 
 	tlsConfigInsecureSkipVerify := false

+ 39 - 6
psiphon/tlsDialer_test.go

@@ -121,7 +121,7 @@ func testTLSDialerCompatibility(t *testing.T, address string) {
 		return d.DialContext(ctx, network, address)
 	}
 
-	clientParameters := makeCustomTLSProfilesClientParameters(t, false)
+	clientParameters := makeCustomTLSProfilesClientParameters(t, false, "")
 
 	profiles := append([]string(nil), protocol.SupportedTLSProfiles...)
 	profiles = append(profiles, clientParameters.Get().CustomTLSProfileNames()...)
@@ -197,7 +197,7 @@ func testTLSDialerCompatibility(t *testing.T, address string) {
 
 func TestSelectTLSProfile(t *testing.T) {
 
-	clientParameters := makeCustomTLSProfilesClientParameters(t, false)
+	clientParameters := makeCustomTLSProfilesClientParameters(t, false, "")
 
 	profiles := append([]string(nil), protocol.SupportedTLSProfiles...)
 	profiles = append(profiles, clientParameters.Get().CustomTLSProfileNames()...)
@@ -207,7 +207,7 @@ func TestSelectTLSProfile(t *testing.T) {
 	numSelections := 10000
 
 	for i := 0; i < numSelections; i++ {
-		profile := SelectTLSProfile(clientParameters.Get())
+		profile := SelectTLSProfile(false, "", clientParameters.Get())
 		selected[profile] += 1
 	}
 
@@ -282,15 +282,34 @@ func TestSelectTLSProfile(t *testing.T) {
 
 	// Only custom TLS profiles should be selected
 
-	clientParameters = makeCustomTLSProfilesClientParameters(t, true)
+	clientParameters = makeCustomTLSProfilesClientParameters(t, true, "")
 	customTLSProfileNames := clientParameters.Get().CustomTLSProfileNames()
 
 	for i := 0; i < numSelections; i++ {
-		profile := SelectTLSProfile(clientParameters.Get())
+		profile := SelectTLSProfile(false, "", clientParameters.Get())
 		if !common.Contains(customTLSProfileNames, profile) {
 			t.Errorf("unexpected non-custom TLS profile selected")
 		}
 	}
+
+	// Disabled TLS profiles should not be selected
+
+	frontingProviderID := "frontingProviderID"
+
+	clientParameters = makeCustomTLSProfilesClientParameters(t, true, frontingProviderID)
+	disableTLSProfiles := clientParameters.Get().LabeledTLSProfiles(
+		parameters.DisableFrontingProviderTLSProfiles, frontingProviderID)
+
+	if len(disableTLSProfiles) < 1 {
+		t.Errorf("unexpected disabled TLS profiles count")
+	}
+
+	for i := 0; i < numSelections; i++ {
+		profile := SelectTLSProfile(true, frontingProviderID, clientParameters.Get())
+		if common.Contains(disableTLSProfiles, profile) {
+			t.Errorf("unexpected disabled TLS profile selected")
+		}
+	}
 }
 
 func BenchmarkRandomizedGetClientHelloVersion(b *testing.B) {
@@ -302,7 +321,7 @@ func BenchmarkRandomizedGetClientHelloVersion(b *testing.B) {
 }
 
 func makeCustomTLSProfilesClientParameters(
-	t *testing.T, useOnlyCustomTLSProfiles bool) *parameters.ClientParameters {
+	t *testing.T, useOnlyCustomTLSProfiles bool, frontingProviderID string) *parameters.ClientParameters {
 
 	clientParameters, err := parameters.NewClientParameters(nil)
 	if err != nil {
@@ -350,6 +369,20 @@ func makeCustomTLSProfilesClientParameters(
 	applyParameters[parameters.UseOnlyCustomTLSProfiles] = useOnlyCustomTLSProfiles
 	applyParameters[parameters.CustomTLSProfiles] = customTLSProfiles
 
+	if frontingProviderID != "" {
+		tlsProfiles := make(protocol.TLSProfiles, 0)
+		tlsProfiles = append(tlsProfiles, "CustomProfile")
+		for i, tlsProfile := range protocol.SupportedTLSProfiles {
+			if i%2 == 0 {
+				tlsProfiles = append(tlsProfiles, tlsProfile)
+			}
+		}
+		disabledTLSProfiles := make(protocol.LabeledTLSProfiles)
+		disabledTLSProfiles[frontingProviderID] = tlsProfiles
+
+		applyParameters[parameters.DisableFrontingProviderTLSProfiles] = disabledTLSProfiles
+	}
+
 	_, err = clientParameters.Set("", false, applyParameters)
 	if err != nil {
 		t.Fatalf("Set failed: %s", err)