Browse Source

Add Min/MaxClientVersion tactics/traffic rules constraints

Rod Hynes 5 tháng trước cách đây
mục cha
commit
4f6157d02a

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

@@ -100,6 +100,7 @@ const (
 	CHANNEL_REJECT_REASON_SPLIT_TUNNEL = 0xFE000000
 
 	PSIPHON_API_HANDSHAKE_AUTHORIZATIONS = "authorizations"
+	PSIPHON_API_HANDSHAKE_CLIENT_VERSION = "client_version"
 )
 
 var SupportedServerEntrySources = []string{

+ 42 - 0
psiphon/common/tactics/tactics.go

@@ -157,6 +157,7 @@ import (
 	"io/ioutil"
 	"net/http"
 	"sort"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -166,6 +167,7 @@ import (
 	"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/prng"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 	lrucache "github.com/cognusion/go-cache-lru"
 	"golang.org/x/crypto/nacl/box"
 )
@@ -289,6 +291,10 @@ type Filter struct {
 	// Values may be patterns containing the '*' wildcard.
 	APIParameters map[string][]string
 
+	// Min/MaxClientVersion specify version constraints the client must match.
+	MinClientVersion *int
+	MaxClientVersion *int
+
 	// SpeedTestRTTMilliseconds specifies a Range filter field that the
 	// client speed test samples must satisfy.
 	SpeedTestRTTMilliseconds *Range
@@ -990,6 +996,26 @@ func (server *Server) getTactics(
 			}
 		}
 
+		if filteredTactics.Filter.MinClientVersion != nil ||
+			filteredTactics.Filter.MaxClientVersion != nil {
+
+			clientVersion, err := getIntStringRequestParam(
+				apiParams, protocol.PSIPHON_API_HANDSHAKE_CLIENT_VERSION)
+			if err != nil {
+				continue
+			}
+
+			if filteredTactics.Filter.MinClientVersion != nil &&
+				clientVersion < *filteredTactics.Filter.MinClientVersion {
+				continue
+			}
+
+			if filteredTactics.Filter.MaxClientVersion != nil &&
+				clientVersion > *filteredTactics.Filter.MaxClientVersion {
+				continue
+			}
+		}
+
 		if filteredTactics.Filter.SpeedTestRTTMilliseconds != nil {
 
 			var speedTestSamples []SpeedTestSample
@@ -1113,6 +1139,22 @@ func getStringRequestParam(apiParams common.APIParameters, name string) (string,
 	return value, nil
 }
 
+// TODO: refactor this copy of psiphon/server.getIntStringRequestParam into common?
+func getIntStringRequestParam(params common.APIParameters, name string) (int, error) {
+	if params[name] == nil {
+		return 0, errors.Tracef("missing param: %s", name)
+	}
+	valueStr, ok := params[name].(string)
+	if !ok {
+		return 0, errors.Tracef("invalid param: %s", name)
+	}
+	value, err := strconv.Atoi(valueStr)
+	if !ok {
+		return 0, errors.Trace(err)
+	}
+	return value, nil
+}
+
 func getJSONRequestParam(apiParams common.APIParameters, name string, value interface{}) error {
 	if apiParams[name] == nil {
 		return errors.Tracef("missing param: %s", name)

+ 25 - 11
psiphon/common/tactics/tactics_test.go

@@ -81,7 +81,9 @@ func TestTactics(t *testing.T) {
           "Filter" : {
             "Regions": ["R1"],
             "ASNs": ["1"],
-            "APIParameters" : {"client_platform" : ["P1"], "client_version": ["V1"]},
+            "APIParameters" : {"client_platform" : ["P1"]},
+            "MinClientVersion" : 4,
+            "MaxClientVersion" : 10,
             "SpeedTestRTTMilliseconds" : {
               "Aggregation" : "Median",
               "AtLeast" : 1
@@ -95,7 +97,19 @@ func TestTactics(t *testing.T) {
         },
         {
           "Filter" : {
-            "APIParameters" : {"client_platform" : ["P2"], "client_version": ["V2"]}
+            "APIParameters" : {"client_platform" : ["P1"]},
+            "MinClientVersion" : 6,
+            "MaxClientVersion" : 10
+          },
+          "Tactics" : {
+            "Parameters" : {
+              "ConnectionWorkerPoolSize" : 1
+            }
+          }
+        },
+        {
+          "Filter" : {
+            "APIParameters" : {"client_platform" : ["P2"], "client_version": ["2"]}
           },
           "Tactics" : {
             "Parameters" : {
@@ -252,7 +266,7 @@ func TestTactics(t *testing.T) {
 
 	apiParams := common.APIParameters{
 		"client_platform": "P1",
-		"client_version":  "V1"}
+		"client_version":  "5"}
 
 	storer := newTestStorer()
 
@@ -388,7 +402,7 @@ func TestTactics(t *testing.T) {
 
 	// Server should be caching tactics data for tactics matching first two
 	// filters.
-	checkServerCache([]bool{true, true, false, false, false})
+	checkServerCache([]bool{true, true, false, false, false, false})
 
 	// There should now be cached local tactics
 
@@ -473,7 +487,7 @@ func TestTactics(t *testing.T) {
 	checkParameters(fetchTacticsRecord)
 
 	// Server cache should be the same
-	checkServerCache([]bool{true, true, false, false, false})
+	checkServerCache([]bool{true, true, false, false, false, false})
 
 	// Modify tactics configuration to change payload
 
@@ -553,7 +567,7 @@ func TestTactics(t *testing.T) {
 
 	checkParameters(fetchTacticsRecord)
 
-	checkServerCache([]bool{true, true, false, false, false})
+	checkServerCache([]bool{true, true, false, false, false, false})
 
 	// Exercise handshake transport of tactics
 
@@ -562,7 +576,7 @@ func TestTactics(t *testing.T) {
 
 	handshakeParams := common.APIParameters{
 		"client_platform": "P1",
-		"client_version":  "V1"}
+		"client_version":  "5"}
 
 	err = SetTacticsAPIParameters(storer, networkID, handshakeParams)
 	if err != nil {
@@ -609,7 +623,7 @@ func TestTactics(t *testing.T) {
 
 	checkParameters(handshakeTacticsRecord)
 
-	checkServerCache([]bool{true, true, false, false, false})
+	checkServerCache([]bool{true, true, false, false, false, false})
 
 	// Now there should be stored tactics
 
@@ -648,7 +662,7 @@ func TestTactics(t *testing.T) {
 
 	apiParams2 := common.APIParameters{
 		"client_platform": "P2",
-		"client_version":  "V2"}
+		"client_version":  "2"}
 
 	fetchTacticsRecord, err = FetchTactics(
 		context.Background(),
@@ -670,8 +684,8 @@ func TestTactics(t *testing.T) {
 	}
 
 	checkServerCache(
-		[]bool{true, true, false, false, false},
-		[]bool{false, false, true, false, false})
+		[]bool{true, true, false, false, false, false},
+		[]bool{false, false, false, true, false, false})
 
 	// Exercise speed test sample truncation
 

+ 25 - 0
psiphon/server/trafficRules.go

@@ -27,6 +27,7 @@ import (
 
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
 	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
 )
 
 const (
@@ -209,6 +210,10 @@ type TrafficRulesFilter struct {
 	// ProviderIDs.
 	ProviderIDs []string
 
+	// Min/MaxClientVersion specify version constraints the client must match.
+	MinClientVersion *int
+	MaxClientVersion *int
+
 	regionLookup                map[string]bool
 	ispLookup                   map[string]bool
 	asnLookup                   map[string]bool
@@ -845,6 +850,26 @@ func (set *TrafficRulesSet) GetTrafficRules(
 			}
 		}
 
+		if filter.MinClientVersion != nil ||
+			filter.MaxClientVersion != nil {
+
+			clientVersion, err := getIntStringRequestParam(
+				state.apiParams, protocol.PSIPHON_API_HANDSHAKE_CLIENT_VERSION)
+			if err != nil {
+				return false
+			}
+
+			if filter.MinClientVersion != nil &&
+				clientVersion < *filter.MinClientVersion {
+				return false
+			}
+
+			if filter.MaxClientVersion != nil &&
+				clientVersion > *filter.MaxClientVersion {
+				return false
+			}
+		}
+
 		return true
 	}
 

+ 53 - 0
psiphon/server/trafficRules_test.go

@@ -122,7 +122,40 @@ func TestTrafficRulesFilters(t *testing.T) {
             "AllowTCPPorts" : [5,17],
             "AllowUDPPorts" : [6,18]
           }
+        },
+
+        {
+          "Filter" : {
+            "Regions" : ["R5"],
+            "MinClientVersion" : 30,
+            "MaxClientVersion" : 40
+          },
+          "Rules" : {
+            "RateLimits" : {
+              "WriteBytesPerSecond": 17,
+              "ReadBytesPerSecond": 18
+            },
+            "AllowTCPPorts" : [5,9],
+            "AllowUDPPorts" : [6,10]
+          }
+        },
+
+        {
+          "Filter" : {
+            "Regions" : ["R5"],
+            "MinClientVersion" : 10,
+            "MaxClientVersion" : 20
+          },
+          "Rules" : {
+            "RateLimits" : {
+              "WriteBytesPerSecond": 19,
+              "ReadBytesPerSecond": 20
+            },
+            "AllowTCPPorts" : [7,11],
+            "AllowUDPPorts" : [8,12]
+          }
         }
+
       ]
     }
 	`
@@ -261,6 +294,26 @@ func TestTrafficRulesFilters(t *testing.T) {
 			handshakeState{apiParams: map[string]interface{}{"client_version": "1"}, completed: true},
 			1, 2, 3, 4, makePortList("[5]"), makePortList("[6]"),
 		},
+
+		{
+			"don't get 4th filtered rule due to Min/MaxClientVersion",
+			providerID,
+			true,
+			"P1",
+			GeoIPData{Country: "R3", ISP: "I2"},
+			handshakeState{apiParams: map[string]interface{}{"client_version": "1"}, completed: true},
+			1, 2, 3, 4, makePortList("[5]"), makePortList("[6]"),
+		},
+
+		{
+			"match 2nd Min/MaxClientVersion filtered rule",
+			providerID,
+			true,
+			"P1",
+			GeoIPData{Country: "R5", ISP: "I1"},
+			handshakeState{apiParams: map[string]interface{}{"client_version": "15"}, completed: true},
+			1, 19, 3, 20, makePortList("[7,11]"), makePortList("[8,12]"),
+		},
 	}
 	for _, testCase := range testCases {
 		t.Run(testCase.description, func(t *testing.T) {