瀏覽代碼

Add CustomHostName mechanism

Rod Hynes 5 年之前
父節點
當前提交
c94bff94b3

+ 22 - 0
psiphon/common/parameters/parameters.go

@@ -273,6 +273,9 @@ const (
 	ClientBurstDownstreamTargetBytes                 = "ClientBurstDownstreamTargetBytes"
 	ConjureDecoyRegistrarWidth                       = "ConjureDecoyRegistrarWidth"
 	ConjureTransportObfs4Probability                 = "ConjureTransportObfs4Probability"
+	CustomHostNameRegexes                            = "CustomHostNameRegexes"
+	CustomHostNameProbability                        = "CustomHostNameProbability"
+	CustomHostNameLimitProtocols                     = "CustomHostNameLimitProtocols"
 )
 
 const (
@@ -567,6 +570,10 @@ var defaultParameters = map[string]struct {
 
 	ConjureDecoyRegistrarWidth:       {value: 5, minimum: 1},
 	ConjureTransportObfs4Probability: {value: 0.0, minimum: 0.0},
+
+	CustomHostNameRegexes:        {value: RegexStrings{}},
+	CustomHostNameProbability:    {value: 0.0, minimum: 0.0},
+	CustomHostNameLimitProtocols: {value: protocol.TunnelProtocols{}},
 }
 
 // IsServerSideOnly indicates if the parameter specified by name is used
@@ -868,6 +875,14 @@ func (p *Parameters) Set(
 					}
 					return nil, errors.Trace(err)
 				}
+			case RegexStrings:
+				err := v.Validate()
+				if err != nil {
+					if skipOnError {
+						continue
+					}
+					return nil, errors.Trace(err)
+				}
 			}
 
 			// Enforce any minimums. Assumes defaultParameters[name]
@@ -1312,3 +1327,10 @@ func (p ParametersAccessor) ProtocolPacketManipulations(name string) ProtocolPac
 	p.snapshot.getValue(name, &value)
 	return value
 }
+
+// RegexStrings returns a RegexStrings parameter value.
+func (p ParametersAccessor) RegexStrings(name string) RegexStrings {
+	value := RegexStrings{}
+	p.snapshot.getValue(name, &value)
+	return value
+}

+ 5 - 0
psiphon/common/parameters/parameters_test.go

@@ -139,6 +139,11 @@ func TestGetDefaultParameters(t *testing.T) {
 			if !reflect.DeepEqual(v, g) {
 				t.Fatalf("ProtocolPacketManipulations returned %+v expected %+v", g, v)
 			}
+		case RegexStrings:
+			g := p.Get().RegexStrings(name)
+			if !reflect.DeepEqual(v, g) {
+				t.Fatalf("RegexStrings returned %+v expected %+v", g, v)
+			}
 		default:
 			t.Fatalf("Unhandled default type: %s", name)
 		}

+ 40 - 0
psiphon/common/parameters/regex.go

@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2021, 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 parameters
+
+import (
+	"regexp"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
+)
+
+// RegexStrings is a list of regex values.
+type RegexStrings []string
+
+// Validate checks that the regex values are well-formed.
+func (regexes RegexStrings) Validate() error {
+	for _, regex := range regexes {
+		_, err := regexp.Compile(regex)
+		if err != nil {
+			return errors.Trace(err)
+		}
+	}
+	return nil
+}

+ 18 - 0
psiphon/config.go

@@ -735,6 +735,12 @@ type Config struct {
 	// ApplicationParameters is for testing purposes.
 	ApplicationParameters parameters.KeyValues
 
+	// CustomHostNameRegexes and other custom host name fields are for testing
+	// purposes.
+	CustomHostNameRegexes        []string
+	CustomHostNameProbability    *float64
+	CustomHostNameLimitProtocols []string
+
 	// params is the active parameters.Parameters with defaults, config values,
 	// and, optionally, tactics applied.
 	//
@@ -1621,6 +1627,18 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.ApplicationParameters] = config.ApplicationParameters
 	}
 
+	if config.CustomHostNameRegexes != nil {
+		applyParameters[parameters.CustomHostNameRegexes] = parameters.RegexStrings(config.CustomHostNameRegexes)
+	}
+
+	if config.CustomHostNameProbability != nil {
+		applyParameters[parameters.CustomHostNameProbability] = *config.CustomHostNameProbability
+	}
+
+	if config.CustomHostNameLimitProtocols != nil {
+		applyParameters[parameters.CustomHostNameLimitProtocols] = protocol.TunnelProtocols(config.CustomHostNameLimitProtocols)
+	}
+
 	return applyParameters
 }
 

+ 31 - 3
psiphon/dialParameters.go

@@ -463,7 +463,7 @@ func MakeDialParameters(
 
 			dialParams.MeekSNIServerName = ""
 			if p.WeightedCoinFlip(parameters.TransformHostNameProbability) {
-				dialParams.MeekSNIServerName = values.GetHostName()
+				dialParams.MeekSNIServerName = selectHostName(dialParams.TunnelProtocol, p)
 				dialParams.MeekTransformedHostName = true
 			}
 
@@ -472,7 +472,7 @@ func MakeDialParameters(
 			dialParams.MeekHostHeader = ""
 			hostname := serverEntry.IpAddress
 			if p.WeightedCoinFlip(parameters.TransformHostNameProbability) {
-				hostname = values.GetHostName()
+				hostname = selectHostName(dialParams.TunnelProtocol, p)
 				dialParams.MeekTransformedHostName = true
 			}
 			if serverEntry.MeekServerPort == 80 {
@@ -483,7 +483,9 @@ func MakeDialParameters(
 		} else if protocol.TunnelProtocolUsesQUIC(dialParams.TunnelProtocol) {
 
 			dialParams.QUICDialSNIAddress = fmt.Sprintf(
-				"%s:%d", values.GetHostName(), serverEntry.SshObfuscatedQUICPort)
+				"%s:%d",
+				selectHostName(dialParams.TunnelProtocol, p),
+				serverEntry.SshObfuscatedQUICPort)
 		}
 	}
 
@@ -1056,3 +1058,29 @@ func makeDialCustomHeaders(
 	}
 	return dialCustomHeaders
 }
+
+func selectHostName(
+	tunnelProtocol string, p parameters.ParametersAccessor) string {
+
+	limitProtocols := p.TunnelProtocols(parameters.CustomHostNameLimitProtocols)
+	if len(limitProtocols) > 0 && !common.Contains(limitProtocols, tunnelProtocol) {
+		return values.GetHostName()
+	}
+
+	if !p.WeightedCoinFlip(parameters.CustomHostNameProbability) {
+		return values.GetHostName()
+	}
+
+	regexStrings := p.RegexStrings(parameters.CustomHostNameRegexes)
+	if len(regexStrings) == 0 {
+		return values.GetHostName()
+	}
+
+	choice := prng.Intn(len(regexStrings))
+	hostName, err := regen.Generate(regexStrings[choice])
+	if err != nil {
+		return values.GetHostName()
+	}
+
+	return hostName
+}

+ 26 - 5
psiphon/server/server_test.go

@@ -32,6 +32,7 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strconv"
 	"strings"
 	"sync"
@@ -607,9 +608,10 @@ type runServerConfig struct {
 }
 
 var (
-	testSSHClientVersions = []string{"SSH-2.0-A", "SSH-2.0-B", "SSH-2.0-C"}
-	testUserAgents        = []string{"ua1", "ua2", "ua3"}
-	testNetworkType       = "WIFI"
+	testSSHClientVersions   = []string{"SSH-2.0-A", "SSH-2.0-B", "SSH-2.0-C"}
+	testUserAgents          = []string{"ua1", "ua2", "ua3"}
+	testNetworkType         = "WIFI"
+	testCustomHostNameRegex = `[a-z0-9]{5,10}\.example\.org`
 )
 
 var serverRuns = 0
@@ -1394,6 +1396,15 @@ func checkExpectedServerTunnelLogFields(
 			}
 		}
 
+		hostName := fields["meek_host_header"].(string)
+		dialPortNumber := int(fields["dial_port_number"].(float64))
+		if dialPortNumber != 80 {
+			hostName, _, _ = net.SplitHostPort(hostName)
+		}
+		if regexp.MustCompile(testCustomHostNameRegex).FindString(hostName) != hostName {
+			return fmt.Errorf("unexpected meek_host_header '%s'", fields["meek_host_header"])
+		}
+
 		for _, name := range []string{
 			"meek_dial_ip_address",
 			"meek_resolved_ip_address",
@@ -1416,6 +1427,11 @@ func checkExpectedServerTunnelLogFields(
 			}
 		}
 
+		hostName := fields["meek_sni_server_name"].(string)
+		if regexp.MustCompile(testCustomHostNameRegex).FindString(hostName) != hostName {
+			return fmt.Errorf("unexpected meek_sni_server_name '%s'", fields["meek_sni_server_name"])
+		}
+
 		for _, name := range []string{
 			"meek_dial_ip_address",
 			"meek_resolved_ip_address",
@@ -2106,7 +2122,10 @@ func paveTacticsConfigFile(
                 "AppFlag1" : true,
                 "AppConfig1" : {"Option1" : "A", "Option2" : "B"},
                 "AppSwitches1" : [1, 2, 3, 4]
-              }
+              },
+              "CustomHostNameRegexes": ["%s"],
+              "CustomHostNameProbability": 1.0,
+              "CustomHostNameLimitProtocols": ["%s"]
             }
           }
         }
@@ -2136,7 +2155,9 @@ func paveTacticsConfigFile(
 		tunnelProtocol,
 		tunnelProtocol,
 		livenessTestSize, livenessTestSize, livenessTestSize, livenessTestSize,
-		propagationChannelID)
+		propagationChannelID,
+		strings.ReplaceAll(testCustomHostNameRegex, `\`, `\\`),
+		tunnelProtocol)
 
 	err := ioutil.WriteFile(tacticsConfigFilename, []byte(tacticsConfigJSON), 0600)
 	if err != nil {