Ver Fonte

Add tactics payload compression

- Use CBOR encoding for API responses containing compressed tactics
- Add JSON omitempty for all API response fields
Rod Hynes há 5 meses atrás
pai
commit
a8cfb5dd92

+ 7 - 2
psiphon/common/authPackage.go

@@ -108,7 +108,12 @@ func WriteAuthenticatedDataPackage(
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 
 
-	return Compress(packageJSON), nil
+	compressedPackage, err := Compress(CompressionZlib, packageJSON)
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
+
+	return compressedPackage, nil
 }
 }
 
 
 // ReadAuthenticatedDataPackage extracts and verifies authenticated
 // ReadAuthenticatedDataPackage extracts and verifies authenticated
@@ -123,7 +128,7 @@ func ReadAuthenticatedDataPackage(
 	var err error
 	var err error
 
 
 	if isCompressed {
 	if isCompressed {
-		packageJSON, err = Decompress(dataPackage)
+		packageJSON, err = Decompress(CompressionZlib, dataPackage)
 		if err != nil {
 		if err != nil {
 			return "", errors.Trace(err)
 			return "", errors.Trace(err)
 		}
 		}

+ 5 - 2
psiphon/common/authPackage_test.go

@@ -59,7 +59,7 @@ func TestAuthenticatedPackage(t *testing.T) {
 		t.Fatalf("GenerateAuthenticatedDataPackageKeys failed: %s", err)
 		t.Fatalf("GenerateAuthenticatedDataPackageKeys failed: %s", err)
 	}
 	}
 
 
-	packageJSON, err := Decompress(packagePayload)
+	packageJSON, err := Decompress(CompressionZlib, packagePayload)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("Uncompress failed: %s", err)
 		t.Fatalf("Uncompress failed: %s", err)
 	}
 	}
@@ -76,7 +76,10 @@ func TestAuthenticatedPackage(t *testing.T) {
 		t.Fatalf("Marshal failed: %s", err)
 		t.Fatalf("Marshal failed: %s", err)
 	}
 	}
 
 
-	tamperedPackagePayload := Compress(tamperedPackageJSON)
+	tamperedPackagePayload, err := Compress(CompressionZlib, tamperedPackageJSON)
+	if err != nil {
+		t.Fatalf("Compress failed: %s", err)
+	}
 
 
 	tamperedTempFileName, err := makeTempFile(tamperedPackagePayload)
 	tamperedTempFileName, err := makeTempFile(tamperedPackagePayload)
 	if err != nil {
 	if err != nil {

+ 0 - 5
psiphon/common/dsl/api.go

@@ -206,11 +206,6 @@ type RelayedRequest struct {
 	Request     []byte `cbor:"3,keyasint,omitempty"`
 	Request     []byte `cbor:"3,keyasint,omitempty"`
 }
 }
 
 
-const (
-	relayedResponseNoCompression   = 0
-	relayedResponseZlibCompression = 1
-)
-
 // RelayedResponse wraps a DSL response value or error.
 // RelayedResponse wraps a DSL response value or error.
 type RelayedResponse struct {
 type RelayedResponse struct {
 	Error       int32  `cbor:"1,keyasint,omitempty"`
 	Error       int32  `cbor:"1,keyasint,omitempty"`

+ 4 - 25
psiphon/common/dsl/fetcher.go

@@ -21,9 +21,7 @@ package dsl
 
 
 import (
 import (
 	"bytes"
 	"bytes"
-	"compress/zlib"
 	"context"
 	"context"
-	"io"
 	"time"
 	"time"
 
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
@@ -765,29 +763,10 @@ func (f *Fetcher) doRelayedRequest(
 			"RelayedResponse.Error: %d", relayedResponse.Error)
 			"RelayedResponse.Error: %d", relayedResponse.Error)
 	}
 	}
 
 
-	var uncompressedResponse []byte
-
-	switch relayedResponse.Compression {
-
-	case relayedResponseNoCompression:
-		uncompressedResponse = relayedResponse.Response
-
-	case relayedResponseZlibCompression:
-		r, err := zlib.NewReader(bytes.NewReader(relayedResponse.Response))
-		if err != nil {
-			return false, errors.Trace(err)
-		}
-		defer r.Close()
-		var b bytes.Buffer
-		_, err = io.Copy(&b, r)
-		if err != nil {
-			return false, errors.Trace(err)
-		}
-		uncompressedResponse = b.Bytes()
-
-	default:
-		return false, errors.Tracef(
-			"unknown RelayedResponse.Compression: %d", relayedResponse.Compression)
+	uncompressedResponse, err := common.Decompress(
+		relayedResponse.Compression, relayedResponse.Response)
+	if err != nil {
+		return false, errors.Trace(err)
 	}
 	}
 
 
 	err = cbor.Unmarshal(uncompressedResponse, response)
 	err = cbor.Unmarshal(uncompressedResponse, response)

+ 5 - 22
psiphon/common/dsl/relay.go

@@ -21,7 +21,6 @@ package dsl
 
 
 import (
 import (
 	"bytes"
 	"bytes"
-	"compress/zlib"
 	"context"
 	"context"
 	"crypto/tls"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
@@ -397,30 +396,14 @@ func (r *Relay) HandleRequest(
 	// CPU load on the DSL backend, and avoid relays having to always
 	// CPU load on the DSL backend, and avoid relays having to always
 	// decompress the backend response in cacheGetServerEntriesResponse.
 	// decompress the backend response in cacheGetServerEntriesResponse.
 
 
-	compression := int32(relayedResponseNoCompression)
+	compression := common.CompressionNone
 	if relayedRequest.RequestType == requestTypeGetServerEntries {
 	if relayedRequest.RequestType == requestTypeGetServerEntries {
-		compression = relayedResponseZlibCompression
+		compression = common.CompressionZlib
 	}
 	}
 
 
-	var compressedResponse []byte
-
-	switch compression {
-
-	case relayedResponseNoCompression:
-		compressedResponse = response
-
-	case relayedResponseZlibCompression:
-		var b bytes.Buffer
-		w := zlib.NewWriter(&b)
-		_, err := w.Write(response)
-		if err != nil {
-			return nil, errors.Trace(err)
-		}
-		err = w.Close()
-		if err != nil {
-			return nil, errors.Trace(err)
-		}
-		compressedResponse = b.Bytes()
+	compressedResponse, err := common.Compress(compression, response)
+	if err != nil {
+		return nil, errors.Trace(err)
 	}
 	}
 
 
 	cborRelayedResponse, err := protocol.CBOREncoding.Marshal(
 	cborRelayedResponse, err := protocol.CBOREncoding.Marshal(

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

@@ -521,6 +521,7 @@ const (
 	CheckServerEntryTagsMaxSendBytes                   = "CheckServerEntryTagsMaxSendBytes"
 	CheckServerEntryTagsMaxSendBytes                   = "CheckServerEntryTagsMaxSendBytes"
 	CheckServerEntryTagsMaxWorkTime                    = "CheckServerEntryTagsMaxWorkTime"
 	CheckServerEntryTagsMaxWorkTime                    = "CheckServerEntryTagsMaxWorkTime"
 	ServerEntryPruneDialPortNumberZero                 = "ServerEntryPruneDialPortNumberZero"
 	ServerEntryPruneDialPortNumberZero                 = "ServerEntryPruneDialPortNumberZero"
+	CompressTactics                                    = "CompressTactics"
 	DSLRelayMaxHttpConns                               = "DSLRelayMaxHttpConns"
 	DSLRelayMaxHttpConns                               = "DSLRelayMaxHttpConns"
 	DSLRelayMaxHttpIdleConns                           = "DSLRelayMaxHttpIdleConns"
 	DSLRelayMaxHttpIdleConns                           = "DSLRelayMaxHttpIdleConns"
 	DSLRelayHttpIdleConnTimeout                        = "DSLRelayHttpIdleConnTimeout"
 	DSLRelayHttpIdleConnTimeout                        = "DSLRelayHttpIdleConnTimeout"
@@ -1122,6 +1123,8 @@ var defaultParameters = map[string]struct {
 	CheckServerEntryTagsMaxWorkTime:    {value: 60 * time.Second, minimum: time.Duration(0)},
 	CheckServerEntryTagsMaxWorkTime:    {value: 60 * time.Second, minimum: time.Duration(0)},
 	ServerEntryPruneDialPortNumberZero: {value: true},
 	ServerEntryPruneDialPortNumberZero: {value: true},
 
 
+	CompressTactics: {value: true},
+
 	DSLRelayMaxHttpConns:        {value: 100, minimum: 1, flags: serverSideOnly},
 	DSLRelayMaxHttpConns:        {value: 100, minimum: 1, flags: serverSideOnly},
 	DSLRelayMaxHttpIdleConns:    {value: 10, minimum: 1, flags: serverSideOnly},
 	DSLRelayMaxHttpIdleConns:    {value: 10, minimum: 1, flags: serverSideOnly},
 	DSLRelayHttpIdleConnTimeout: {value: 120 * time.Second, minimum: time.Duration(0), flags: serverSideOnly},
 	DSLRelayHttpIdleConnTimeout: {value: 120 * time.Second, minimum: time.Duration(0), flags: serverSideOnly},

+ 5 - 1
psiphon/common/protocol/packed.go

@@ -831,7 +831,11 @@ func init() {
 
 
 		{167, "shadowsocks_prefix", nil},
 		{167, "shadowsocks_prefix", nil},
 
 
-		// Next key value = 168
+		// Specs: protocol.PSIPHON_API_RESPONSE_VERSION_FIELD_NAME
+
+		{168, "psiphon_api_response_version", intConverter},
+
+		// Next key value = 169
 	}
 	}
 
 
 	for _, spec := range packedAPIParameterSpecs {
 	for _, spec := range packedAPIParameterSpecs {

+ 62 - 33
psiphon/common/protocol/protocol.go

@@ -21,7 +21,6 @@ package protocol
 
 
 import (
 import (
 	"crypto/sha256"
 	"crypto/sha256"
-	"encoding/json"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
 
 
@@ -90,6 +89,9 @@ const (
 	PSIPHON_API_ENCODING_CBOR = "cbor"
 	PSIPHON_API_ENCODING_CBOR = "cbor"
 	PSIPHON_API_ENCODING_JSON = "json"
 	PSIPHON_API_ENCODING_JSON = "json"
 
 
+	PSIPHON_API_RESPONSE_VERSION_FIELD_NAME = "psiphon_api_response_version"
+	PSIPHON_API_RESPONSE_V1                 = "1"
+
 	PACKET_TUNNEL_CHANNEL_TYPE            = "tun@psiphon.ca"
 	PACKET_TUNNEL_CHANNEL_TYPE            = "tun@psiphon.ca"
 	RANDOM_STREAM_CHANNEL_TYPE            = "random@psiphon.ca"
 	RANDOM_STREAM_CHANNEL_TYPE            = "random@psiphon.ca"
 	TCP_PORT_FORWARD_NO_SPLIT_TUNNEL_TYPE = "direct-tcpip-no-split-tunnel@psiphon.ca"
 	TCP_PORT_FORWARD_NO_SPLIT_TUNNEL_TYPE = "direct-tcpip-no-split-tunnel@psiphon.ca"
@@ -791,59 +793,60 @@ func (transports ConjureTransports) PruneInvalid() ConjureTransports {
 }
 }
 
 
 type HandshakeResponse struct {
 type HandshakeResponse struct {
-	Homepages                []string            `json:"homepages"`
-	UpgradeClientVersion     string              `json:"upgrade_client_version"`
-	PageViewRegexes          []map[string]string `json:"page_view_regexes"`
-	HttpsRequestRegexes      []map[string]string `json:"https_request_regexes"`
-	EncodedServerList        []string            `json:"encoded_server_list"`
-	ClientRegion             string              `json:"client_region"`
-	ClientAddress            string              `json:"client_address"`
-	ServerTimestamp          string              `json:"server_timestamp"`
-	ActiveAuthorizationIDs   []string            `json:"active_authorization_ids"`
-	TacticsPayload           json.RawMessage     `json:"tactics_payload"`
-	UpstreamBytesPerSecond   int64               `json:"upstream_bytes_per_second"`
-	DownstreamBytesPerSecond int64               `json:"downstream_bytes_per_second"`
-	SteeringIP               string              `json:"steering_ip"`
-	Padding                  string              `json:"padding"`
+	Homepages                 []string            `json:"homepages,omitempty" cbor:"1,keyasint,omitempty"`
+	UpgradeClientVersion      string              `json:"upgrade_client_version,omitempty" cbor:"2,keyasint,omitempty"`
+	PageViewRegexes           []map[string]string `json:"page_view_regexes,omitempty" cbor:"3,keyasint,omitempty"`
+	HttpsRequestRegexes       []map[string]string `json:"https_request_regexes,omitempty" cbor:"4,keyasint,omitempty"`
+	EncodedServerList         []string            `json:"encoded_server_list,omitempty" cbor:"5,keyasint,omitempty"`
+	ClientRegion              string              `json:"client_region,omitempty" cbor:"6,keyasint,omitempty"`
+	ClientAddress             string              `json:"client_address,omitempty" cbor:"7,keyasint,omitempty"`
+	ServerTimestamp           string              `json:"server_timestamp,omitempty" cbor:"8,keyasint,omitempty"`
+	ActiveAuthorizationIDs    []string            `json:"active_authorization_ids,omitempty" cbor:"9,keyasint,omitempty"`
+	TacticsPayload            []byte              `json:"tactics_payload,omitempty" cbor:"10,keyasint,omitempty"`
+	TacticsPayloadCompression int32               `json:"tactics_payload_compression,omitempty" cbor:"11,keyasint,omitempty"`
+	UpstreamBytesPerSecond    int64               `json:"upstream_bytes_per_second,omitempty" cbor:"12,keyasint,omitempty"`
+	DownstreamBytesPerSecond  int64               `json:"downstream_bytes_per_second,omitempty" cbor:"13,keyasint,omitempty"`
+	SteeringIP                string              `json:"steering_ip,omitempty" cbor:"14,keyasint,omitempty"`
+	Padding                   string              `json:"padding,omitempty" cbor:"15,keyasint,omitempty"`
 }
 }
 
 
 type ConnectedResponse struct {
 type ConnectedResponse struct {
-	ConnectedTimestamp string `json:"connected_timestamp"`
-	Padding            string `json:"padding"`
+	ConnectedTimestamp string `json:"connected_timestamp,omitempty"`
+	Padding            string `json:"padding,omitempty"`
 }
 }
 
 
 type StatusResponse struct {
 type StatusResponse struct {
-	InvalidServerEntryTags []string `json:"invalid_server_entry_tags"`
-	Padding                string   `json:"padding"`
+	InvalidServerEntryTags []string `json:"invalid_server_entry_tags,omitempty"`
+	Padding                string   `json:"padding,omitempty"`
 }
 }
 
 
 type OSLRequest struct {
 type OSLRequest struct {
-	ClearLocalSLOKs bool             `json:"clear_local_sloks"`
-	SeedPayload     *osl.SeedPayload `json:"seed_payload"`
+	ClearLocalSLOKs bool             `json:"clear_local_sloks,omitempty"`
+	SeedPayload     *osl.SeedPayload `json:"seed_payload,omitempty"`
 }
 }
 
 
 type SSHPasswordPayload struct {
 type SSHPasswordPayload struct {
-	SessionId          string   `json:"SessionId"`
-	SshPassword        string   `json:"SshPassword"`
-	ClientCapabilities []string `json:"ClientCapabilities"`
-	SponsorID          string   `json:"SponsorId"`
+	SessionId          string   `json:"SessionId,omitempty"`
+	SshPassword        string   `json:"SshPassword,omitempty"`
+	ClientCapabilities []string `json:"ClientCapabilities,omitempty"`
+	SponsorID          string   `json:"SponsorId,omitempty"`
 }
 }
 
 
 type MeekCookieData struct {
 type MeekCookieData struct {
-	MeekProtocolVersion  int    `json:"v"`
-	ClientTunnelProtocol string `json:"t"`
-	EndPoint             string `json:"e"`
+	MeekProtocolVersion  int    `json:"v,omitempty"`
+	ClientTunnelProtocol string `json:"t,omitempty"`
+	EndPoint             string `json:"e,omitempty"`
 }
 }
 
 
 type RandomStreamRequest struct {
 type RandomStreamRequest struct {
-	UpstreamBytes   int `json:"u"`
-	DownstreamBytes int `json:"d"`
+	UpstreamBytes   int `json:"u,omitempty"`
+	DownstreamBytes int `json:"d,omitempty"`
 }
 }
 
 
 type AlertRequest struct {
 type AlertRequest struct {
-	Reason     string   `json:"reason"`
-	Subject    string   `json:"subject"`
-	ActionURLs []string `json:"action"`
+	Reason     string   `json:"reason,omitempty"`
+	Subject    string   `json:"subject,omitempty"`
+	ActionURLs []string `json:"action,omitempty"`
 }
 }
 
 
 // CBOREncoding defines the specific CBDR encoding used for all Psiphon CBOR
 // CBOREncoding defines the specific CBDR encoding used for all Psiphon CBOR
@@ -885,3 +888,29 @@ func DeriveBPFServerProgramPRNGSeed(obfuscatedKey string) (*prng.Seed, error) {
 	seed := prng.Seed(sha256.Sum256([]byte(obfuscatedKey)))
 	seed := prng.Seed(sha256.Sum256([]byte(obfuscatedKey)))
 	return prng.NewSaltedSeed(&seed, "bpf-server-program")
 	return prng.NewSaltedSeed(&seed, "bpf-server-program")
 }
 }
+
+// GetCompressTactics checks for the parameter field indicating that
+// compressed tactics are supported and requested.
+func GetCompressTactics(params common.APIParameters) bool {
+
+	// The generic, versioned psiphon_api_response_version field allows for
+	// future enhancements and upgrades to other responses and response values.
+	//
+	// Version 1 adds compressed tactics payloads, and CBOR binary encoding of
+	// responses containing compressed tactics payloads.
+
+	if params[PSIPHON_API_RESPONSE_VERSION_FIELD_NAME] == nil {
+		return false
+	}
+	value, ok := params[PSIPHON_API_RESPONSE_VERSION_FIELD_NAME].(string)
+	if !ok {
+		return false
+	}
+	return value == PSIPHON_API_RESPONSE_V1
+}
+
+// SetCompressTactics adds a field to params indicating that compressed
+// tactics are support and requested.
+func SetCompressTactics(params common.APIParameters) {
+	params[PSIPHON_API_RESPONSE_VERSION_FIELD_NAME] = PSIPHON_API_RESPONSE_V1
+}

+ 84 - 20
psiphon/common/tactics/tactics.go

@@ -166,7 +166,9 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/obfuscator"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	lrucache "github.com/cognusion/go-cache-lru"
 	lrucache "github.com/cognusion/go-cache-lru"
+	"github.com/fxamacker/cbor/v2"
 	"golang.org/x/crypto/nacl/box"
 	"golang.org/x/crypto/nacl/box"
 )
 )
 
 
@@ -327,10 +329,13 @@ type Payload struct {
 	// is the same as the stored tag the client specified in its
 	// is the same as the stored tag the client specified in its
 	// request, the Tactics will be empty as the client already has the
 	// request, the Tactics will be empty as the client already has the
 	// correct data.
 	// correct data.
-	Tag string
+	Tag string `cbor:"1,keyasint,omitempty"`
+
+	// Tactics is a JSON- or CBOR-encoded Tactics struct and may be nil.
+	Tactics []byte `cbor:"2,keyasint,omitempty"`
 
 
-	// Tactics is a JSON-encoded Tactics struct and may be nil.
-	Tactics json.RawMessage
+	// TacticsCompression specifies how Tactics is compressed.
+	TacticsCompression int32 `cbor:"3,keyasint,omitempty"`
 }
 }
 
 
 // Record is the tactics data persisted by the client. There is one
 // Record is the tactics data persisted by the client. There is one
@@ -797,7 +802,8 @@ func (server *Server) GetFilterGeoIPScope(geoIPData common.GeoIPData) int {
 // Callers must not mutate returned tactics data, which is cached.
 // Callers must not mutate returned tactics data, which is cached.
 func (server *Server) GetTacticsPayload(
 func (server *Server) GetTacticsPayload(
 	geoIPData common.GeoIPData,
 	geoIPData common.GeoIPData,
-	apiParams common.APIParameters) (*Payload, error) {
+	apiParams common.APIParameters,
+	compressPayload bool) (*Payload, error) {
 
 
 	// includeServerSideOnly is false: server-side only parameters are not
 	// includeServerSideOnly is false: server-side only parameters are not
 	// used by the client, so including them wastes space and unnecessarily
 	// used by the client, so including them wastes space and unnecessarily
@@ -837,7 +843,21 @@ func (server *Server) GetTacticsPayload(
 	}
 	}
 
 
 	if sendPayloadTactics {
 	if sendPayloadTactics {
-		payload.Tactics = tacticsData.payload
+
+		tacticsDataPayload := tacticsData.payload
+		compression := common.CompressionNone
+
+		if compressPayload {
+			compression = common.CompressionZlib
+			tacticsDataPayload, err = common.Compress(
+				compression, tacticsDataPayload)
+			if err != nil {
+				return nil, errors.Trace(err)
+			}
+		}
+
+		payload.Tactics = tacticsDataPayload
+		payload.TacticsCompression = compression
 	}
 	}
 
 
 	return payload, nil
 	return payload, nil
@@ -1342,7 +1362,8 @@ func (server *Server) handleTacticsRequest(
 		requestPrivateKey,
 		requestPrivateKey,
 		requestObfuscatedKey,
 		requestObfuscatedKey,
 		boxedRequest,
 		boxedRequest,
-		&apiParams)
+		&apiParams,
+		nil)
 	if err != nil {
 	if err != nil {
 		server.logger.WithTraceFields(
 		server.logger.WithTraceFields(
 			common.LogFields{"error": err}).Warning("failed to unbox request")
 			common.LogFields{"error": err}).Warning("failed to unbox request")
@@ -1358,7 +1379,16 @@ func (server *Server) handleTacticsRequest(
 		return
 		return
 	}
 	}
 
 
-	tacticsPayload, err := server.GetTacticsPayload(geoIPData, apiParams)
+	// When compressed tactics are requested, use CBOR binary encoding for the
+	// response.
+
+	compressTactics := protocol.GetCompressTactics(apiParams)
+	var responseMarshaler func(any) ([]byte, error)
+	if compressTactics {
+		responseMarshaler = protocol.CBOREncoding.Marshal
+	}
+
+	tacticsPayload, err := server.GetTacticsPayload(geoIPData, apiParams, compressTactics)
 	if err == nil && tacticsPayload == nil {
 	if err == nil && tacticsPayload == nil {
 		err = errors.TraceNew("unexpected missing tactics payload")
 		err = errors.TraceNew("unexpected missing tactics payload")
 	}
 	}
@@ -1377,7 +1407,8 @@ func (server *Server) handleTacticsRequest(
 		requestPrivateKey,
 		requestPrivateKey,
 		requestObfuscatedKey,
 		requestObfuscatedKey,
 		nil,
 		nil,
-		tacticsPayload)
+		tacticsPayload,
+		responseMarshaler)
 	if err != nil {
 	if err != nil {
 		server.logger.WithTraceFields(
 		server.logger.WithTraceFields(
 			common.LogFields{"error": err}).Warning("failed to box response")
 			common.LogFields{"error": err}).Warning("failed to box response")
@@ -1454,7 +1485,7 @@ func SetTacticsAPIParameters(
 // the payload has a new tag/tactics, this is stored and a new expiry time is
 // the payload has a new tag/tactics, this is stored and a new expiry time is
 // set. If the payload has the same tag, the existing tactics are retained,
 // set. If the payload has the same tag, the existing tactics are retained,
 // the expiry is extended using the previous TTL, and a nil record is
 // the expiry is extended using the previous TTL, and a nil record is
-// rerturned.
+// returned.
 //
 //
 // HandleTacticsPayload is called by the Psiphon client to handle the tactics
 // HandleTacticsPayload is called by the Psiphon client to handle the tactics
 // payload in the API handshake and inproxy broker responses. As the Psiphon
 // payload in the API handshake and inproxy broker responses. As the Psiphon
@@ -1566,6 +1597,12 @@ func FetchTactics(
 	encodedRequestObfuscatedKey string,
 	encodedRequestObfuscatedKey string,
 	obfuscatedRoundTripper ObfuscatedRoundTripper) (*Record, error) {
 	obfuscatedRoundTripper ObfuscatedRoundTripper) (*Record, error) {
 
 
+	p := params.Get()
+	speedTestPaddingMinBytes := p.Int(parameters.SpeedTestPaddingMinBytes)
+	speedTestPaddingMaxBytes := p.Int(parameters.SpeedTestPaddingMaxBytes)
+	compressTactics := p.Bool(parameters.CompressTactics)
+	p.Close()
+
 	networkID := getNetworkID()
 	networkID := getNetworkID()
 
 
 	record, err := getStoredTacticsRecord(storer, networkID)
 	record, err := getStoredTacticsRecord(storer, networkID)
@@ -1582,10 +1619,7 @@ func FetchTactics(
 
 
 	if len(speedTestSamples) == 0 {
 	if len(speedTestSamples) == 0 {
 
 
-		p := params.Get()
-		request := prng.Padding(
-			p.Int(parameters.SpeedTestPaddingMinBytes),
-			p.Int(parameters.SpeedTestPaddingMaxBytes))
+		request := prng.Padding(speedTestPaddingMinBytes, speedTestPaddingMaxBytes)
 
 
 		startTime := time.Now()
 		startTime := time.Now()
 
 
@@ -1625,6 +1659,15 @@ func FetchTactics(
 	apiParams[STORED_TACTICS_TAG_PARAMETER_NAME] = record.Tag
 	apiParams[STORED_TACTICS_TAG_PARAMETER_NAME] = record.Tag
 	apiParams[SPEED_TEST_SAMPLES_PARAMETER_NAME] = speedTestSamples
 	apiParams[SPEED_TEST_SAMPLES_PARAMETER_NAME] = speedTestSamples
 
 
+	// When requesting compressed tactics, the response will use CBOR binary
+	// encoding.
+
+	var responseUnmarshaler func([]byte, any) error
+	if compressTactics {
+		protocol.SetCompressTactics(apiParams)
+		responseUnmarshaler = cbor.Unmarshal
+	}
+
 	requestPublicKey, err := base64.StdEncoding.DecodeString(encodedRequestPublicKey)
 	requestPublicKey, err := base64.StdEncoding.DecodeString(encodedRequestPublicKey)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
@@ -1646,7 +1689,8 @@ func FetchTactics(
 		ephemeralPrivateKey[:],
 		ephemeralPrivateKey[:],
 		requestObfuscatedKey,
 		requestObfuscatedKey,
 		ephemeralPublicKey[:],
 		ephemeralPublicKey[:],
-		&apiParams)
+		&apiParams,
+		nil)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
@@ -1670,7 +1714,8 @@ func FetchTactics(
 		ephemeralPrivateKey[:],
 		ephemeralPrivateKey[:],
 		requestObfuscatedKey,
 		requestObfuscatedKey,
 		boxedResponse,
 		boxedResponse,
-		&payload)
+		&payload,
+		responseUnmarshaler)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
@@ -1869,9 +1914,15 @@ func applyTacticsPayload(
 			return newTactics, errors.TraceNew("missing tactics")
 			return newTactics, errors.TraceNew("missing tactics")
 		}
 		}
 
 
+		payloadTactics, err := common.Decompress(
+			payload.TacticsCompression, payload.Tactics)
+		if err != nil {
+			return newTactics, errors.Trace(err)
+		}
+
 		record.Tag = payload.Tag
 		record.Tag = payload.Tag
 		record.Tactics = Tactics{}
 		record.Tactics = Tactics{}
-		err := json.Unmarshal(payload.Tactics, &record.Tactics)
+		err = json.Unmarshal(payloadTactics, &record.Tactics)
 		if err != nil {
 		if err != nil {
 			return newTactics, errors.Trace(err)
 			return newTactics, errors.Trace(err)
 		}
 		}
@@ -1916,7 +1967,8 @@ func setStoredTacticsRecord(
 
 
 func boxPayload(
 func boxPayload(
 	nonce, peerPublicKey, privateKey, obfuscatedKey, bundlePublicKey []byte,
 	nonce, peerPublicKey, privateKey, obfuscatedKey, bundlePublicKey []byte,
-	payload interface{}) ([]byte, error) {
+	payload interface{},
+	marshaler func(any) ([]byte, error)) ([]byte, error) {
 
 
 	if len(nonce) > 24 ||
 	if len(nonce) > 24 ||
 		len(peerPublicKey) != 32 ||
 		len(peerPublicKey) != 32 ||
@@ -1924,7 +1976,14 @@ func boxPayload(
 		return nil, errors.TraceNew("unexpected box key length")
 		return nil, errors.TraceNew("unexpected box key length")
 	}
 	}
 
 
-	marshaledPayload, err := json.Marshal(payload)
+	var marshaledPayload []byte
+	var err error
+
+	if marshaler == nil {
+		marshaler = json.Marshal
+	}
+
+	marshaledPayload, err = marshaler(payload)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
@@ -1974,7 +2033,8 @@ func boxPayload(
 // unboxPayload mutates obfuscatedBoxedPayload by deobfuscating in-place.
 // unboxPayload mutates obfuscatedBoxedPayload by deobfuscating in-place.
 func unboxPayload(
 func unboxPayload(
 	nonce, peerPublicKey, privateKey, obfuscatedKey, obfuscatedBoxedPayload []byte,
 	nonce, peerPublicKey, privateKey, obfuscatedKey, obfuscatedBoxedPayload []byte,
-	payload interface{}) ([]byte, error) {
+	payload interface{},
+	unmarshaler func([]byte, any) error) ([]byte, error) {
 
 
 	if len(nonce) > 24 ||
 	if len(nonce) > 24 ||
 		(peerPublicKey != nil && len(peerPublicKey) != 32) ||
 		(peerPublicKey != nil && len(peerPublicKey) != 32) ||
@@ -2024,7 +2084,11 @@ func unboxPayload(
 		return nil, errors.TraceNew("invalid box")
 		return nil, errors.TraceNew("invalid box")
 	}
 	}
 
 
-	err = json.Unmarshal(marshaledPayload, payload)
+	if unmarshaler == nil {
+		unmarshaler = json.Unmarshal
+	}
+
+	err = unmarshaler(marshaledPayload, payload)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}

+ 5 - 2
psiphon/common/tactics/tactics_test.go

@@ -569,7 +569,10 @@ func TestTactics(t *testing.T) {
 		t.Fatalf("SetTacticsAPIParameters failed: %s", err)
 		t.Fatalf("SetTacticsAPIParameters failed: %s", err)
 	}
 	}
 
 
-	tacticsPayload, err := server.GetTacticsPayload(clientGeoIPData, handshakeParams)
+	// FetchTactics will exercise the compression case.
+	compressPayload := false
+
+	tacticsPayload, err := server.GetTacticsPayload(clientGeoIPData, handshakeParams, compressPayload)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("GetTacticsPayload failed: %s", err)
 		t.Fatalf("GetTacticsPayload failed: %s", err)
 	}
 	}
@@ -777,7 +780,7 @@ func TestTactics(t *testing.T) {
 		t.Fatalf("Server config failed to reload")
 		t.Fatalf("Server config failed to reload")
 	}
 	}
 
 
-	_, err = server.GetTacticsPayload(clientGeoIPData, handshakeParams)
+	_, err = server.GetTacticsPayload(clientGeoIPData, handshakeParams, compressPayload)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("GetTacticsPayload failed: %s", err)
 		t.Fatalf("GetTacticsPayload failed: %s", err)
 	}
 	}

+ 27 - 7
psiphon/common/utils.go

@@ -133,23 +133,43 @@ func TruncateTimestampToHour(timestamp string) string {
 	return t.Truncate(1 * time.Hour).Format(time.RFC3339)
 	return t.Truncate(1 * time.Hour).Format(time.RFC3339)
 }
 }
 
 
-// Compress returns zlib compressed data
-func Compress(data []byte) []byte {
+const (
+	CompressionNone = int32(0)
+	CompressionZlib = int32(1)
+)
+
+// Compress compresses data with the specified algorithm.
+func Compress(compression int32, data []byte) ([]byte, error) {
+	if compression == CompressionNone {
+		return data, nil
+	}
+	if compression != CompressionZlib {
+		return nil, errors.TraceNew("unknown compression algorithm")
+	}
 	var compressedData bytes.Buffer
 	var compressedData bytes.Buffer
 	writer := zlib.NewWriter(&compressedData)
 	writer := zlib.NewWriter(&compressedData)
-	_, _ = writer.Write(data)
+	_, err := writer.Write(data)
+	if err != nil {
+		return nil, errors.Trace(err)
+	}
 	_ = writer.Close()
 	_ = writer.Close()
-	return compressedData.Bytes()
+	return compressedData.Bytes(), nil
 }
 }
 
 
-// Decompress returns zlib decompressed data
-func Decompress(data []byte) ([]byte, error) {
+// Decompress decompresses data with the specified algorithm.
+func Decompress(compression int32, data []byte) ([]byte, error) {
+	if compression == CompressionNone {
+		return data, nil
+	}
+	if compression != CompressionZlib {
+		return nil, errors.TraceNew("unknown compression algorithm")
+	}
 	reader, err := zlib.NewReader(bytes.NewReader(data))
 	reader, err := zlib.NewReader(bytes.NewReader(data))
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
 	uncompressedData, err := ioutil.ReadAll(reader)
 	uncompressedData, err := ioutil.ReadAll(reader)
-	reader.Close()
+	_ = reader.Close()
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}

+ 14 - 8
psiphon/common/utils_test.go

@@ -60,17 +60,23 @@ func TestGetStringSlice(t *testing.T) {
 
 
 func TestCompress(t *testing.T) {
 func TestCompress(t *testing.T) {
 
 
-	originalData := []byte("test data")
+	for _, compression := range []int32{CompressionNone, CompressionZlib} {
 
 
-	compressedData := Compress(originalData)
+		originalData := []byte("test data")
 
 
-	decompressedData, err := Decompress(compressedData)
-	if err != nil {
-		t.Errorf("Uncompress failed: %s", err)
-	}
+		compressedData, err := Compress(compression, originalData)
+		if err != nil {
+			t.Errorf("Compress failed: %s", err)
+		}
+
+		decompressedData, err := Decompress(compression, compressedData)
+		if err != nil {
+			t.Errorf("Decompress failed: %s", err)
+		}
 
 
-	if !bytes.Equal(originalData, decompressedData) {
-		t.Error("decompressed data doesn't match original data")
+		if !bytes.Equal(originalData, decompressedData) {
+			t.Error("decompressed data doesn't match original data")
+		}
 	}
 	}
 }
 }
 
 

+ 6 - 0
psiphon/config.go

@@ -1101,6 +1101,8 @@ type Config struct {
 
 
 	NetworkIDCacheTTLMilliseconds *int `json:",omitempty"`
 	NetworkIDCacheTTLMilliseconds *int `json:",omitempty"`
 
 
+	CompressTactics *bool `json:",omitempty"`
+
 	// params is the active parameters.Parameters with defaults, config values,
 	// params is the active parameters.Parameters with defaults, config values,
 	// and, optionally, tactics applied.
 	// and, optionally, tactics applied.
 	//
 	//
@@ -2890,6 +2892,10 @@ func (config *Config) makeConfigParameters() map[string]interface{} {
 		applyParameters[parameters.NetworkIDCacheTTL] = fmt.Sprintf("%dms", *config.NetworkIDCacheTTLMilliseconds)
 		applyParameters[parameters.NetworkIDCacheTTL] = fmt.Sprintf("%dms", *config.NetworkIDCacheTTLMilliseconds)
 	}
 	}
 
 
+	if config.CompressTactics != nil {
+		applyParameters[parameters.CompressTactics] = *config.CompressTactics
+	}
+
 	// When adding new config dial parameters that may override tactics, also
 	// When adding new config dial parameters that may override tactics, also
 	// update setDialParametersHash.
 	// update setDialParametersHash.
 
 

+ 3 - 0
psiphon/controller_test.go

@@ -480,6 +480,9 @@ func controllerRun(t *testing.T, runConfig *controllerRunConfig) {
 
 
 	modifyConfig["LimitQUICVersions"] = runConfig.quicVersions
 	modifyConfig["LimitQUICVersions"] = runConfig.quicVersions
 
 
+	// TODO: vary this option
+	modifyConfig["CompressTactics"] = false
+
 	configJSON, _ = json.Marshal(modifyConfig)
 	configJSON, _ = json.Marshal(modifyConfig)
 
 
 	// Don't print initial config setup notices
 	// Don't print initial config setup notices

+ 18 - 6
psiphon/server/api.go

@@ -305,8 +305,19 @@ func handshakeAPIRequestHandler(
 
 
 	httpsRequestRegexes, domainBytesChecksum := db.GetHttpsRequestRegexes(sponsorID)
 	httpsRequestRegexes, domainBytesChecksum := db.GetHttpsRequestRegexes(sponsorID)
 
 
+	// When compressed tactics are requested, use CBOR binary encoding for the
+	// response.
+
+	var responseMarshaler func(any) ([]byte, error)
+	responseMarshaler = json.Marshal
+
+	compressTactics := protocol.GetCompressTactics(params)
+	if compressTactics {
+		responseMarshaler = protocol.CBOREncoding.Marshal
+	}
+
 	tacticsPayload, err := support.TacticsServer.GetTacticsPayload(
 	tacticsPayload, err := support.TacticsServer.GetTacticsPayload(
-		common.GeoIPData(clientGeoIPData), params)
+		common.GeoIPData(clientGeoIPData), params, compressTactics)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
@@ -364,6 +375,9 @@ func handshakeAPIRequestHandler(
 
 
 	pad_response, _ := getPaddingSizeRequestParam(params, "pad_response")
 	pad_response, _ := getPaddingSizeRequestParam(params, "pad_response")
 
 
+	// TODO: as a future enhancement, use the packed, CBOR encoding --
+	// protocol.EncodePackedServerEntryFields -- for server entries.
+
 	// Discover new servers
 	// Discover new servers
 
 
 	var encodedServerList []string
 	var encodedServerList []string
@@ -434,7 +448,7 @@ func handshakeAPIRequestHandler(
 
 
 	var marshaledTacticsPayload []byte
 	var marshaledTacticsPayload []byte
 	if tacticsPayload != nil {
 	if tacticsPayload != nil {
-		marshaledTacticsPayload, err = json.Marshal(tacticsPayload)
+		marshaledTacticsPayload, err = responseMarshaler(tacticsPayload)
 		if err != nil {
 		if err != nil {
 			return nil, errors.Trace(err)
 			return nil, errors.Trace(err)
 		}
 		}
@@ -457,10 +471,7 @@ func handshakeAPIRequestHandler(
 		Padding:                  strings.Repeat(" ", pad_response),
 		Padding:                  strings.Repeat(" ", pad_response),
 	}
 	}
 
 
-	// TODO: as a future enhancement, pack and CBOR encode this and other API
-	// responses
-
-	responsePayload, err := json.Marshal(handshakeResponse)
+	responsePayload, err := responseMarshaler(handshakeResponse)
 	if err != nil {
 	if err != nil {
 		return nil, errors.Trace(err)
 		return nil, errors.Trace(err)
 	}
 	}
@@ -1166,6 +1177,7 @@ var baseParams = []requestParamSpec{
 	{"device_location", isGeoHashString, requestParamOptional},
 	{"device_location", isGeoHashString, requestParamOptional},
 	{"network_type", isAnyString, requestParamOptional},
 	{"network_type", isAnyString, requestParamOptional},
 	{tactics.APPLIED_TACTICS_TAG_PARAMETER_NAME, isAnyString, requestParamOptional},
 	{tactics.APPLIED_TACTICS_TAG_PARAMETER_NAME, isAnyString, requestParamOptional},
+	{"compress_response", isIntString, requestParamOptional | requestParamNotLogged},
 }
 }
 
 
 // baseDialParams are the dial parameters, per-tunnel network protocol and
 // baseDialParams are the dial parameters, per-tunnel network protocol and

+ 13 - 2
psiphon/server/meek.go

@@ -2031,8 +2031,19 @@ func (server *MeekServer) inproxyBrokerGetTacticsPayload(
 	geoIPData common.GeoIPData,
 	geoIPData common.GeoIPData,
 	apiParameters common.APIParameters) ([]byte, string, error) {
 	apiParameters common.APIParameters) ([]byte, string, error) {
 
 
+	// When compressed tactics are requested, use CBOR binary encoding for the
+	// response.
+
+	var responseMarshaler func(any) ([]byte, error)
+	responseMarshaler = json.Marshal
+
+	compressTactics := protocol.GetCompressTactics(apiParameters)
+	if compressTactics {
+		responseMarshaler = protocol.CBOREncoding.Marshal
+	}
+
 	tacticsPayload, err := server.support.TacticsServer.GetTacticsPayload(
 	tacticsPayload, err := server.support.TacticsServer.GetTacticsPayload(
-		geoIPData, apiParameters)
+		geoIPData, apiParameters, compressTactics)
 	if err != nil {
 	if err != nil {
 		return nil, "", errors.Trace(err)
 		return nil, "", errors.Trace(err)
 	}
 	}
@@ -2042,7 +2053,7 @@ func (server *MeekServer) inproxyBrokerGetTacticsPayload(
 
 
 	if tacticsPayload != nil {
 	if tacticsPayload != nil {
 
 
-		marshaledTacticsPayload, err = json.Marshal(tacticsPayload)
+		marshaledTacticsPayload, err = responseMarshaler(tacticsPayload)
 		if err != nil {
 		if err != nil {
 			return nil, "", errors.Trace(err)
 			return nil, "", errors.Trace(err)
 		}
 		}

+ 21 - 13
psiphon/server/server_test.go

@@ -298,13 +298,14 @@ func TestUnfrontedMeekSessionTicket(t *testing.T) {
 func TestUnfrontedMeekSessionTicketTLS13(t *testing.T) {
 func TestUnfrontedMeekSessionTicketTLS13(t *testing.T) {
 	runServer(t,
 	runServer(t,
 		&runServerConfig{
 		&runServerConfig{
-			tunnelProtocol:       "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
-			tlsProfile:           protocol.TLS_PROFILE_CHROME_70,
-			requireAuthorization: true,
-			doTunneledWebRequest: true,
-			doTunneledNTPRequest: true,
-			doDanglingTCPConn:    true,
-			doLogHostProvider:    true,
+			tunnelProtocol:        "UNFRONTED-MEEK-SESSION-TICKET-OSSH",
+			tlsProfile:            protocol.TLS_PROFILE_CHROME_70,
+			requireAuthorization:  true,
+			doTunneledWebRequest:  true,
+			doTunneledNTPRequest:  true,
+			doDanglingTCPConn:     true,
+			doLogHostProvider:     true,
+			doUncompressedTactics: true,
 		})
 		})
 }
 }
 
 
@@ -430,12 +431,13 @@ func TestInproxyTLSOSSH(t *testing.T) {
 	}
 	}
 	runServer(t,
 	runServer(t,
 		&runServerConfig{
 		&runServerConfig{
-			tunnelProtocol:       "INPROXY-WEBRTC-TLS-OSSH",
-			requireAuthorization: true,
-			doTunneledWebRequest: true,
-			doTunneledNTPRequest: true,
-			doDanglingTCPConn:    true,
-			doLogHostProvider:    true,
+			tunnelProtocol:        "INPROXY-WEBRTC-TLS-OSSH",
+			requireAuthorization:  true,
+			doTunneledWebRequest:  true,
+			doTunneledNTPRequest:  true,
+			doDanglingTCPConn:     true,
+			doLogHostProvider:     true,
+			doUncompressedTactics: true,
 		})
 		})
 }
 }
 
 
@@ -774,6 +776,7 @@ type runServerConfig struct {
 	doPersonalPairing        bool
 	doPersonalPairing        bool
 	doRestrictInproxy        bool
 	doRestrictInproxy        bool
 	useInproxyMediaStreams   bool
 	useInproxyMediaStreams   bool
+	doUncompressedTactics    bool
 }
 }
 
 
 var (
 var (
@@ -1546,6 +1549,11 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
 		}
 		}
 	}
 	}
 
 
+	if runConfig.doUncompressedTactics {
+		compressTactics := false
+		clientConfig.CompressTactics = &compressTactics
+	}
+
 	err = clientConfig.Commit(false)
 	err = clientConfig.Commit(false)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("error committing configuration file: %s", err)
 		t.Fatalf("error committing configuration file: %s", err)

+ 20 - 2
psiphon/serverApi.go

@@ -47,6 +47,7 @@ import (
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/transferstats"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/transferstats"
 	lrucache "github.com/cognusion/go-cache-lru"
 	lrucache "github.com/cognusion/go-cache-lru"
+	"github.com/fxamacker/cbor/v2"
 )
 )
 
 
 // ServerContext is a utility struct which holds all of the data associated
 // ServerContext is a utility struct which holds all of the data associated
@@ -157,6 +158,8 @@ func (serverContext *ServerContext) doHandshakeRequest(ignoreStatsRegexps bool)
 
 
 	doTactics := !serverContext.tunnel.config.DisableTactics
 	doTactics := !serverContext.tunnel.config.DisableTactics
 
 
+	compressTactics := false
+
 	networkID := ""
 	networkID := ""
 	if doTactics {
 	if doTactics {
 
 
@@ -180,6 +183,10 @@ func (serverContext *ServerContext) doHandshakeRequest(ignoreStatsRegexps bool)
 		if err != nil {
 		if err != nil {
 			return errors.Trace(err)
 			return errors.Trace(err)
 		}
 		}
+
+		p := serverContext.tunnel.config.GetParameters().Get()
+		compressTactics = p.Bool(parameters.CompressTactics)
+		p.Close()
 	}
 	}
 
 
 	// When split tunnel mode is enabled, indicate this to the server. When
 	// When split tunnel mode is enabled, indicate this to the server. When
@@ -212,6 +219,17 @@ func (serverContext *ServerContext) doHandshakeRequest(ignoreStatsRegexps bool)
 		}
 		}
 	}
 	}
 
 
+	// When requesting compressed tactics, the response will use CBOR binary
+	// encoding.
+
+	var responseUnmarshaler func([]byte, any) error
+	responseUnmarshaler = json.Unmarshal
+
+	if compressTactics && serverContext.psiphonHttpsClient == nil {
+		protocol.SetCompressTactics(params)
+		responseUnmarshaler = cbor.Unmarshal
+	}
+
 	var response []byte
 	var response []byte
 	if serverContext.psiphonHttpsClient == nil {
 	if serverContext.psiphonHttpsClient == nil {
 
 
@@ -262,7 +280,7 @@ func (serverContext *ServerContext) doHandshakeRequest(ignoreStatsRegexps bool)
 	handshakeResponse.UpstreamBytesPerSecond = -1
 	handshakeResponse.UpstreamBytesPerSecond = -1
 	handshakeResponse.DownstreamBytesPerSecond = -1
 	handshakeResponse.DownstreamBytesPerSecond = -1
 
 
-	err := json.Unmarshal(response, &handshakeResponse)
+	err := responseUnmarshaler(response, &handshakeResponse)
 	if err != nil {
 	if err != nil {
 		return errors.Trace(err)
 		return errors.Trace(err)
 	}
 	}
@@ -384,7 +402,7 @@ func (serverContext *ServerContext) doHandshakeRequest(ignoreStatsRegexps bool)
 		networkID == serverContext.tunnel.config.GetNetworkID() {
 		networkID == serverContext.tunnel.config.GetNetworkID() {
 
 
 		var payload *tactics.Payload
 		var payload *tactics.Payload
-		err := json.Unmarshal(handshakeResponse.TacticsPayload, &payload)
+		err := responseUnmarshaler(handshakeResponse.TacticsPayload, &payload)
 		if err != nil {
 		if err != nil {
 			return errors.Trace(err)
 			return errors.Trace(err)
 		}
 		}