Browse Source

Add support for alternate cookie names and content types

Rod Hynes 3 years ago
parent
commit
d83b2a4954

+ 12 - 3
psiphon/common/logger.go

@@ -54,10 +54,19 @@ func (a LogFields) Add(b LogFields) {
 	}
 }
 
-// MetricsSource is an object that provides metrics to be logged
+// MetricsSource is an object that provides metrics to be logged.
 type MetricsSource interface {
 
-	// GetMetrics returns a LogFields populated with
-	// metrics from the MetricsSource
+	// GetMetrics returns a LogFields populated with metrics from the
+	// MetricsSource.
 	GetMetrics() LogFields
 }
+
+// NoticeMetricsSource is an object that provides metrics to be logged
+// only in notices, for inclusion in diagnostics.
+type NoticeMetricsSource interface {
+
+	// GetNoticeMetrics returns a LogFields populated with metrics from
+	// the NoticeMetricsSource.
+	GetNoticeMetrics() LogFields
+}

+ 11 - 8
psiphon/common/parameters/parameters.go

@@ -207,6 +207,8 @@ const (
 	MeekMinLimitRequestPayloadLength                 = "MeekMinLimitRequestPayloadLength"
 	MeekMaxLimitRequestPayloadLength                 = "MeekMaxLimitRequestPayloadLength"
 	MeekRedialTLSProbability                         = "MeekRedialTLSProbability"
+	MeekAlternateCookieNameProbability               = "MeekAlternateCookieNameProbability"
+	MeekAlternateContentTypeProbability              = "MeekAlternateContentTypeProbability"
 	TransformHostNameProbability                     = "TransformHostNameProbability"
 	PickUserAgentProbability                         = "PickUserAgentProbability"
 	LivenessTestMinUpstreamBytes                     = "LivenessTestMinUpstreamBytes"
@@ -530,14 +532,15 @@ var defaultParameters = map[string]struct {
 	MeekRoundTripRetryMaxDelay:                 {value: 1 * time.Second, minimum: time.Duration(0)},
 	MeekRoundTripRetryMultiplier:               {value: 2.0, minimum: 0.0},
 	MeekRoundTripTimeout:                       {value: 20 * time.Second, minimum: 1 * time.Second, flags: useNetworkLatencyMultiplier},
-
-	MeekTrafficShapingProbability:    {value: 1.0, minimum: 0.0},
-	MeekTrafficShapingLimitProtocols: {value: protocol.TunnelProtocols{}},
-	MeekMinTLSPadding:                {value: 0, minimum: 0},
-	MeekMaxTLSPadding:                {value: 0, minimum: 0},
-	MeekMinLimitRequestPayloadLength: {value: 65536, minimum: 1},
-	MeekMaxLimitRequestPayloadLength: {value: 65536, minimum: 1},
-	MeekRedialTLSProbability:         {value: 0.0, minimum: 0.0},
+	MeekTrafficShapingProbability:              {value: 1.0, minimum: 0.0},
+	MeekTrafficShapingLimitProtocols:           {value: protocol.TunnelProtocols{}},
+	MeekMinTLSPadding:                          {value: 0, minimum: 0},
+	MeekMaxTLSPadding:                          {value: 0, minimum: 0},
+	MeekMinLimitRequestPayloadLength:           {value: 65536, minimum: 1},
+	MeekMaxLimitRequestPayloadLength:           {value: 65536, minimum: 1},
+	MeekRedialTLSProbability:                   {value: 0.0, minimum: 0.0},
+	MeekAlternateCookieNameProbability:         {value: 0.5, minimum: 0.0},
+	MeekAlternateContentTypeProbability:        {value: 0.5, minimum: 0.0},
 
 	TransformHostNameProbability: {value: 0.5, minimum: 0.0},
 	PickUserAgentProbability:     {value: 0.5, minimum: 0.0},

+ 45 - 4
psiphon/common/values/values.go

@@ -55,11 +55,10 @@ func NewPickOneSpec(items []string) *ValueSpec {
 
 // GetValue selects a value according to the spec. An optional seed may
 // be specified to support replay.
-func (spec *ValueSpec) GetValue(seed *prng.Seed) string {
+func (spec *ValueSpec) GetValue(PRNG *prng.PRNG) string {
 	rangeFunc := prng.Range
 	intnFunc := prng.Intn
-	if seed != nil {
-		PRNG := prng.NewPRNGWithSeed(seed)
+	if PRNG != nil {
 		rangeFunc = PRNG.Range
 		intnFunc = PRNG.Intn
 	}
@@ -126,6 +125,8 @@ var (
 	sshServerVersionsSpec atomic.Value
 	userAgentsSpec        atomic.Value
 	hostNamesSpec         atomic.Value
+	cookieNamesSpec       atomic.Value
+	contentTypeSpec       atomic.Value
 )
 
 // SetRevision set the revision value, which may be used to track which value
@@ -173,11 +174,15 @@ func SetSSHServerVersionsSpec(spec *ValueSpec) {
 // GetSSHServerVersion selects a value based on the previously set spec, or
 // returns a default when no spec is set.
 func GetSSHServerVersion(seed *prng.Seed) string {
+	var PRNG *prng.PRNG
+	if seed != nil {
+		PRNG = prng.NewPRNGWithSeed(seed)
+	}
 	spec, ok := sshServerVersionsSpec.Load().(*ValueSpec)
 	if !ok {
 		return ""
 	}
-	return spec.GetValue(seed)
+	return spec.GetValue(PRNG)
 }
 
 // SetUserAgentsSpec sets the corresponding value spec.
@@ -215,3 +220,39 @@ func GetHostName() string {
 	}
 	return spec.GetValue(nil)
 }
+
+// SetCookieNamesSpec sets the corresponding value spec.
+func SetCookieNamesSpec(spec *ValueSpec) {
+	if spec == nil {
+		return
+	}
+	cookieNamesSpec.Store(spec)
+}
+
+// GetCookieName selects a value based on the previously set spec, or
+// returns a default when no spec is set.
+func GetCookieName(PRNG *prng.PRNG) string {
+	spec, ok := cookieNamesSpec.Load().(*ValueSpec)
+	if !ok {
+		return "c"
+	}
+	return spec.GetValue(PRNG)
+}
+
+// SetContentTypesSpec sets the corresponding value spec.
+func SetContentTypesSpec(spec *ValueSpec) {
+	if spec == nil {
+		return
+	}
+	contentTypeSpec.Store(spec)
+}
+
+// GetContentType selects a value based on the previously set spec, or
+// returns a default when no spec is set.
+func GetContentType(PRNG *prng.PRNG) string {
+	spec, ok := contentTypeSpec.Load().(*ValueSpec)
+	if !ok {
+		return "application/octet-stream"
+	}
+	return spec.GetValue(PRNG)
+}

+ 17 - 7
psiphon/config.go

@@ -687,13 +687,15 @@ type Config struct {
 
 	// MeekTrafficShapingProbability and associated fields are for testing
 	// purposes.
-	MeekTrafficShapingProbability    *float64
-	MeekTrafficShapingLimitProtocols []string
-	MeekMinTLSPadding                *int
-	MeekMaxTLSPadding                *int
-	MeekMinLimitRequestPayloadLength *int
-	MeekMaxLimitRequestPayloadLength *int
-	MeekRedialTLSProbability         *float64
+	MeekTrafficShapingProbability       *float64
+	MeekTrafficShapingLimitProtocols    []string
+	MeekMinTLSPadding                   *int
+	MeekMaxTLSPadding                   *int
+	MeekMinLimitRequestPayloadLength    *int
+	MeekMaxLimitRequestPayloadLength    *int
+	MeekRedialTLSProbability            *float64
+	MeekAlternateCookieNameProbability  *float64
+	MeekAlternateContentTypeProbability *float64
 
 	// ObfuscatedSSHAlgorithms and associated ObfuscatedSSH fields are for
 	// testing purposes. If specified, ObfuscatedSSHAlgorithms must have 4 SSH
@@ -1635,6 +1637,14 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.MeekRedialTLSProbability] = *config.MeekRedialTLSProbability
 	}
 
+	if config.MeekAlternateCookieNameProbability != nil {
+		applyParameters[parameters.MeekAlternateCookieNameProbability] = *config.MeekAlternateCookieNameProbability
+	}
+
+	if config.MeekAlternateContentTypeProbability != nil {
+		applyParameters[parameters.MeekAlternateContentTypeProbability] = *config.MeekAlternateContentTypeProbability
+	}
+
 	if config.ObfuscatedSSHMinPadding != nil {
 		applyParameters[parameters.ObfuscatedSSHMinPadding] = *config.ObfuscatedSSHMinPadding
 	}

+ 3 - 2
psiphon/dialParameters.go

@@ -134,8 +134,9 @@ type DialParameters struct {
 
 	HoldOffTunnelDuration time.Duration
 
-	DialConnMetrics          common.MetricsSource `json:"-"`
-	ObfuscatedSSHConnMetrics common.MetricsSource `json:"-"`
+	DialConnMetrics          common.MetricsSource       `json:"-"`
+	DialConnNoticeMetrics    common.NoticeMetricsSource `json:"-"`
+	ObfuscatedSSHConnMetrics common.MetricsSource       `json:"-"`
 
 	DialDuration time.Duration `json:"-"`
 

+ 54 - 20
psiphon/meekConn.go

@@ -44,6 +44,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/values"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/upstreamproxy"
 	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/net/http2"
@@ -211,6 +212,7 @@ type MeekConn struct {
 	url                       *url.URL
 	additionalHeaders         http.Header
 	cookie                    *http.Cookie
+	contentType               string
 	cookieSize                int
 	tlsPadding                int
 	limitRequestPayloadLength int
@@ -314,6 +316,7 @@ func DialMeek(
 	if meek.mode == MeekModeRelay {
 		var err error
 		meek.cookie,
+			meek.contentType,
 			meek.tlsPadding,
 			meek.limitRequestPayloadLength,
 			meek.redialTLSProbability,
@@ -806,6 +809,19 @@ func (meek *MeekConn) GetMetrics() common.LogFields {
 	return logFields
 }
 
+// GetNoticeMetrics implements the common.NoticeMetricsSource interface.
+func (meek *MeekConn) GetNoticeMetrics() common.LogFields {
+
+	// These fields are logged only in notices, for diagnostics. The server
+	// will log the same values, but derives them from HTTP headers, so they
+	// don't need to be sent in the API request.
+
+	logFields := make(common.LogFields)
+	logFields["meek_cookie_name"] = meek.cookie.Name
+	logFields["meek_content_type"] = meek.contentType
+	return logFields
+}
+
 // ObfuscatedRoundTrip makes a request to the meek server and returns the
 // response. A new, obfuscated meek cookie is created for every request. The
 // specified end point is recorded in the cookie and is not exposed as
@@ -823,7 +839,7 @@ func (meek *MeekConn) ObfuscatedRoundTrip(
 		return nil, errors.TraceNew("operation unsupported")
 	}
 
-	cookie, _, _, _, err := makeMeekObfuscationValues(
+	cookie, contentType, _, _, _, err := makeMeekObfuscationValues(
 		meek.getCustomParameters(),
 		meek.meekCookieEncryptionPublicKey,
 		meek.meekObfuscatedKey,
@@ -843,7 +859,7 @@ func (meek *MeekConn) ObfuscatedRoundTrip(
 	// the concurrency constraints are satisfied.
 
 	request, err := meek.newRequest(
-		requestCtx, cookie, bytes.NewReader(requestBody), 0)
+		requestCtx, cookie, contentType, bytes.NewReader(requestBody), 0)
 	if err != nil {
 		return nil, errors.Trace(err)
 	}
@@ -1178,6 +1194,7 @@ func (r *readCloseSignaller) AwaitClosed() bool {
 func (meek *MeekConn) newRequest(
 	requestCtx context.Context,
 	cookie *http.Cookie,
+	contentType string,
 	body io.Reader,
 	contentLength int) (*http.Request, error) {
 
@@ -1199,7 +1216,7 @@ func (meek *MeekConn) newRequest(
 
 	meek.addAdditionalHeaders(request)
 
-	request.Header.Set("Content-Type", "application/octet-stream")
+	request.Header.Set("Content-Type", contentType)
 
 	if cookie == nil {
 		cookie = meek.cookie
@@ -1320,6 +1337,7 @@ func (meek *MeekConn) relayRoundTrip(sendBuffer *bytes.Buffer) (int64, error) {
 		request, err := meek.newRequest(
 			requestCtx,
 			nil,
+			meek.contentType,
 			requestBody,
 			contentLength)
 		if err != nil {
@@ -1535,13 +1553,14 @@ func makeMeekObfuscationValues(
 	endPoint string,
 
 ) (cookie *http.Cookie,
+	contentType string,
 	tlsPadding int,
 	limitRequestPayloadLength int,
 	redialTLSProbability float64,
 	err error) {
 
 	if meekCookieEncryptionPublicKey == "" {
-		return nil, 0, 0, 0.0, errors.TraceNew("missing public key")
+		return nil, "", 0, 0, 0.0, errors.TraceNew("missing public key")
 	}
 
 	cookieData := &protocol.MeekCookieData{
@@ -1551,7 +1570,7 @@ func makeMeekObfuscationValues(
 	}
 	serializedCookie, err := json.Marshal(cookieData)
 	if err != nil {
-		return nil, 0, 0, 0.0, errors.Trace(err)
+		return nil, "", 0, 0, 0.0, errors.Trace(err)
 	}
 
 	// Encrypt the JSON data
@@ -1565,12 +1584,12 @@ func makeMeekObfuscationValues(
 	var publicKey [32]byte
 	decodedPublicKey, err := base64.StdEncoding.DecodeString(meekCookieEncryptionPublicKey)
 	if err != nil {
-		return nil, 0, 0, 0.0, errors.Trace(err)
+		return nil, "", 0, 0, 0.0, errors.Trace(err)
 	}
 	copy(publicKey[:], decodedPublicKey)
 	ephemeralPublicKey, ephemeralPrivateKey, err := box.GenerateKey(rand.Reader)
 	if err != nil {
-		return nil, 0, 0, 0.0, errors.Trace(err)
+		return nil, "", 0, 0, 0.0, errors.Trace(err)
 	}
 	box := box.Seal(nil, serializedCookie, &nonce, &publicKey, ephemeralPrivateKey)
 	encryptedCookie := make([]byte, 32+len(box))
@@ -1587,7 +1606,7 @@ func makeMeekObfuscationValues(
 			PaddingPRNGSeed: meekObfuscatorPaddingPRNGSeed,
 			MaxPadding:      &maxPadding})
 	if err != nil {
-		return nil, 0, 0, 0.0, errors.Trace(err)
+		return nil, "", 0, 0, 0.0, errors.Trace(err)
 	}
 	obfuscatedCookie := obfuscator.SendSeedMessage()
 	seedLen := len(obfuscatedCookie)
@@ -1596,20 +1615,35 @@ func makeMeekObfuscationValues(
 
 	cookieNamePRNG, err := obfuscator.GetDerivedPRNG("meek-cookie-name")
 	if err != nil {
-		return nil, 0, 0, 0.0, errors.Trace(err)
+		return nil, "", 0, 0, 0.0, errors.Trace(err)
+	}
+	var cookieName string
+	if cookieNamePRNG.FlipWeightedCoin(p.Float(parameters.MeekAlternateCookieNameProbability)) {
+		cookieName = values.GetCookieName(cookieNamePRNG)
+	} else {
+		// Format the HTTP cookie
+		// The format is <random letter 'A'-'Z'>=<base64 data>, which is intended to match common cookie formats.
+		A := int('A')
+		Z := int('Z')
+		// letterIndex is integer in range [int('A'), int('Z')]
+		letterIndex := cookieNamePRNG.Intn(Z - A + 1)
+		cookieName = string(byte(A + letterIndex))
 	}
-
-	// Format the HTTP cookie
-	// The format is <random letter 'A'-'Z'>=<base64 data>, which is intended to match common cookie formats.
-	A := int('A')
-	Z := int('Z')
-	// letterIndex is integer in range [int('A'), int('Z')]
-	letterIndex := cookieNamePRNG.Intn(Z - A + 1)
 
 	cookie = &http.Cookie{
-		Name:  string(byte(A + letterIndex)),
+		Name:  cookieName,
 		Value: base64.StdEncoding.EncodeToString(obfuscatedCookie)}
 
+	contentTypePRNG, err := obfuscator.GetDerivedPRNG("meek-content-type")
+	if err != nil {
+		return nil, "", 0, 0, 0.0, errors.Trace(err)
+	}
+	if contentTypePRNG.FlipWeightedCoin(p.Float(parameters.MeekAlternateContentTypeProbability)) {
+		contentType = values.GetContentType(contentTypePRNG)
+	} else {
+		contentType = "application/octet-stream"
+	}
+
 	tlsPadding = 0
 	limitRequestPayloadLength = MEEK_MAX_REQUEST_PAYLOAD_LENGTH
 	redialTLSProbability = 0.0
@@ -1622,7 +1656,7 @@ func makeMeekObfuscationValues(
 		limitRequestPayloadLengthPRNG, err := obfuscator.GetDerivedPRNG(
 			"meek-limit-request-payload-length")
 		if err != nil {
-			return nil, 0, 0, 0.0, errors.Trace(err)
+			return nil, "", 0, 0, 0.0, errors.Trace(err)
 		}
 
 		minLength := p.Int(parameters.MeekMinLimitRequestPayloadLength)
@@ -1649,7 +1683,7 @@ func makeMeekObfuscationValues(
 			tlsPaddingPRNG, err := obfuscator.GetDerivedPRNG(
 				"meek-tls-padding")
 			if err != nil {
-				return nil, 0, 0, 0.0, errors.Trace(err)
+				return nil, "", 0, 0, 0.0, errors.Trace(err)
 			}
 
 			tlsPadding = tlsPaddingPRNG.Range(minPadding, maxPadding)
@@ -1658,5 +1692,5 @@ func makeMeekObfuscationValues(
 		redialTLSProbability = p.Float(parameters.MeekRedialTLSProbability)
 	}
 
-	return cookie, tlsPadding, limitRequestPayloadLength, redialTLSProbability, nil
+	return cookie, contentType, tlsPadding, limitRequestPayloadLength, redialTLSProbability, nil
 }

+ 7 - 0
psiphon/notice.go

@@ -588,6 +588,13 @@ func noticeWithDialParameters(noticeType string, dialParams *DialParameters, pos
 			}
 		}
 
+		if dialParams.DialConnNoticeMetrics != nil {
+			metrics := dialParams.DialConnNoticeMetrics.GetNoticeMetrics()
+			for name, value := range metrics {
+				args = append(args, name, value)
+			}
+		}
+
 		if dialParams.ObfuscatedSSHConnMetrics != nil {
 			metrics := dialParams.ObfuscatedSSHConnMetrics.GetMetrics()
 			for name, value := range metrics {

+ 6 - 0
psiphon/server/meek.go

@@ -790,6 +790,8 @@ func (server *MeekServer) getSessionOrEndpoint(
 		meekProtocolVersion: clientSessionData.MeekProtocolVersion,
 		sessionIDSent:       false,
 		cachedResponse:      cachedResponse,
+		cookieName:          meekCookie.Name,
+		contentType:         r.Header.Get("Content-Type"),
 	}
 
 	session.touch()
@@ -1278,6 +1280,8 @@ type meekSession struct {
 	meekProtocolVersion              int
 	sessionIDSent                    bool
 	cachedResponse                   *CachedResponse
+	cookieName                       string
+	contentType                      string
 }
 
 func (session *meekSession) touch() {
@@ -1346,6 +1350,8 @@ func (session *meekSession) GetMetrics() common.LogFields {
 	logFields["meek_peak_cached_response_hit_size"] = atomic.LoadInt64(&session.metricPeakCachedResponseHitSize)
 	logFields["meek_cached_response_miss_position"] = atomic.LoadInt64(&session.metricCachedResponseMissPosition)
 	logFields["meek_underlying_connection_count"] = atomic.LoadInt64(&session.metricUnderlyingConnCount)
+	logFields["meek_cookie_name"] = session.cookieName
+	logFields["meek_content_type"] = session.contentType
 	return logFields
 }
 

+ 4 - 0
psiphon/tunnel.go

@@ -945,6 +945,10 @@ func dialTunnel(
 		dialParams.DialConnMetrics = metricsSource
 	}
 
+	if noticeMetricsSource, ok := dialConn.(common.NoticeMetricsSource); ok {
+		dialParams.DialConnNoticeMetrics = noticeMetricsSource
+	}
+
 	// If dialConn is not a Closer, tunnel failure detection may be slower
 	if _, ok := dialConn.(common.Closer); !ok {
 		NoticeWarning("tunnel.dialTunnel: dialConn is not a Closer")