Просмотр исходного кода

Add GeoIP city filtering to tactics and traffic rules

Rod Hynes 6 лет назад
Родитель
Сommit
08d8a3174d

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

@@ -269,6 +269,9 @@ type Filter struct {
 	// ISPs specifies a list of GeoIP ISPs the client must match.
 	ISPs []string
 
+	// Cities specifies a list of GeoIP Cities the client must match.
+	Cities []string
+
 	// APIParameters specifies API, e.g. handshake, parameter names and
 	// a list of values, one of which must be specified to match this
 	// filter. Only scalar string API parameters may be filtered.
@@ -281,6 +284,7 @@ type Filter struct {
 
 	regionLookup map[string]bool
 	ispLookup    map[string]bool
+	cityLookup   map[string]bool
 }
 
 // Range is a filter field which specifies that the aggregation of
@@ -594,6 +598,13 @@ func (server *Server) initLookups() {
 			}
 		}
 
+		if len(filteredTactics.Filter.Cities) >= stringLookupThreshold {
+			filteredTactics.Filter.cityLookup = make(map[string]bool)
+			for _, city := range filteredTactics.Filter.Cities {
+				filteredTactics.Filter.cityLookup[city] = true
+			}
+		}
+
 		// TODO: add lookups for APIParameters?
 		// Not expected to be long lists of values.
 	}
@@ -715,6 +726,18 @@ func (server *Server) GetTactics(
 			}
 		}
 
+		if len(filteredTactics.Filter.Cities) > 0 {
+			if filteredTactics.Filter.cityLookup != nil {
+				if !filteredTactics.Filter.cityLookup[geoIPData.City] {
+					continue
+				}
+			} else {
+				if !common.Contains(filteredTactics.Filter.Cities, geoIPData.City) {
+					continue
+				}
+			}
+		}
+
 		if filteredTactics.Filter.APIParameters != nil {
 			mismatch := false
 			for name, values := range filteredTactics.Filter.APIParameters {

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

@@ -101,7 +101,9 @@ func TestTactics(t *testing.T) {
         },
         {
           "Filter" : {
-            "Regions": ["R7"]
+            "Regions": ["R7"],
+            "ISPs": ["I1"],
+            "Cities": ["C1"]
           },
           "Tactics" : {
             "Parameters" : {
@@ -135,8 +137,18 @@ func TestTactics(t *testing.T) {
 	expectedApplyCount := 3
 
 	listenerProtocol := "OSSH"
-	listenerFragmentedGeoIP := func(string) common.GeoIPData { return common.GeoIPData{Country: "R7"} }
-	listenerUnfragmentedGeoIP := func(string) common.GeoIPData { return common.GeoIPData{Country: "R8"} }
+	listenerFragmentedGeoIP := func(string) common.GeoIPData {
+		return common.GeoIPData{Country: "R7", ISP: "I1", City: "C1"}
+	}
+	listenerUnfragmentedGeoIPWrongRegion := func(string) common.GeoIPData {
+		return common.GeoIPData{Country: "R8", ISP: "I1", City: "C1"}
+	}
+	listenerUnfragmentedGeoIPWrongISP := func(string) common.GeoIPData {
+		return common.GeoIPData{Country: "R7", ISP: "I2", City: "C1"}
+	}
+	listenerUnfragmentedGeoIPWrongCity := func(string) common.GeoIPData {
+		return common.GeoIPData{Country: "R7", ISP: "I1", City: "C2"}
+	}
 
 	tacticsConfig := fmt.Sprintf(
 		tacticsConfigTemplate,
@@ -754,8 +766,18 @@ func TestTactics(t *testing.T) {
 			true,
 		},
 		{
-			"unfragmented",
-			listenerUnfragmentedGeoIP,
+			"unfragmented-region",
+			listenerUnfragmentedGeoIPWrongRegion,
+			false,
+		},
+		{
+			"unfragmented-ISP",
+			listenerUnfragmentedGeoIPWrongISP,
+			false,
+		},
+		{
+			"unfragmented-city",
+			listenerUnfragmentedGeoIPWrongCity,
 			false,
 		},
 	}

+ 10 - 4
psiphon/server/meek.go

@@ -664,14 +664,14 @@ func (server *MeekServer) getSessionOrEndpoint(
 
 func (server *MeekServer) rateLimit(clientIP string) bool {
 
-	historySize, thresholdSeconds, regions, ISPs, GCTriggerCount, _ :=
+	historySize, thresholdSeconds, regions, ISPs, cities, GCTriggerCount, _ :=
 		server.support.TrafficRulesSet.GetMeekRateLimiterConfig()
 
 	if historySize == 0 {
 		return false
 	}
 
-	if len(regions) > 0 || len(ISPs) > 0 {
+	if len(regions) > 0 || len(ISPs) > 0 || len(cities) > 0 {
 
 		// TODO: avoid redundant GeoIP lookups?
 		geoIPData := server.support.GeoIPService.Lookup(clientIP)
@@ -687,6 +687,12 @@ func (server *MeekServer) rateLimit(clientIP string) bool {
 				return false
 			}
 		}
+
+		if len(cities) > 0 {
+			if !common.Contains(cities, geoIPData.City) {
+				return false
+			}
+		}
 	}
 
 	limit := true
@@ -738,7 +744,7 @@ func (server *MeekServer) rateLimit(clientIP string) bool {
 
 func (server *MeekServer) rateLimitWorker() {
 
-	_, _, _, _, _, reapFrequencySeconds :=
+	_, _, _, _, _, _, reapFrequencySeconds :=
 		server.support.TrafficRulesSet.GetMeekRateLimiterConfig()
 
 	timer := time.NewTimer(time.Duration(reapFrequencySeconds) * time.Second)
@@ -748,7 +754,7 @@ func (server *MeekServer) rateLimitWorker() {
 		select {
 		case <-timer.C:
 
-			_, thresholdSeconds, _, _, _, reapFrequencySeconds :=
+			_, thresholdSeconds, _, _, _, _, reapFrequencySeconds :=
 				server.support.TrafficRulesSet.GetMeekRateLimiterConfig()
 
 			server.rateLimitLock.Lock()

+ 34 - 2
psiphon/server/trafficRules.go

@@ -71,7 +71,7 @@ type TrafficRulesSet struct {
 	// not any meek request for an existing session, if the
 	// MeekRateLimiterHistorySize requests occur in
 	// MeekRateLimiterThresholdSeconds. The scope of rate limiting may be
-	// limited using LimitMeekRateLimiterRegions and LimitMeekRateLimiterISPs.
+	// limited using LimitMeekRateLimiterRegions/ISPs/Cities.
 	//
 	// Hot reloading a new history size will result in existing history being
 	// truncated.
@@ -93,6 +93,12 @@ type TrafficRulesSet struct {
 	// is applied to all client ISPs.
 	MeekRateLimiterISPs []string
 
+	// MeekRateLimiterCities, if set, limits application of the meek
+	// late-stage rate limiter to clients in the specified list of GeoIP
+	// cities. When omitted or empty, meek rate limiting, if configured,
+	// is applied to all client cities.
+	MeekRateLimiterCities []string
+
 	// MeekRateLimiterGarbageCollectionTriggerCount specifies the number of
 	// rate limit events after which garbage collection is manually triggered
 	// in order to reclaim memory used by rate limited and other rejected
@@ -125,6 +131,10 @@ type TrafficRulesFilter struct {
 	// match this filter. When omitted or empty, any client ISP matches.
 	ISPs []string
 
+	// Cities is a list of cities that the client must geolocate to in order to
+	// match this filter. When omitted or empty, any client city matches.
+	Cities []string
+
 	// APIProtocol specifies whether the client must use the SSH
 	// API protocol (when "ssh") or the web API protocol (when "web").
 	// When omitted or blank, any API protocol matches.
@@ -149,6 +159,7 @@ type TrafficRulesFilter struct {
 
 	regionLookup map[string]bool
 	ispLookup    map[string]bool
+	cityLookup   map[string]bool
 }
 
 // TrafficRules specify the limits placed on client traffic.
@@ -290,6 +301,7 @@ func NewTrafficRulesSet(filename string) (*TrafficRulesSet, error) {
 			set.MeekRateLimiterThresholdSeconds = newSet.MeekRateLimiterThresholdSeconds
 			set.MeekRateLimiterRegions = newSet.MeekRateLimiterRegions
 			set.MeekRateLimiterISPs = newSet.MeekRateLimiterISPs
+			set.MeekRateLimiterCities = newSet.MeekRateLimiterCities
 			set.MeekRateLimiterGarbageCollectionTriggerCount = newSet.MeekRateLimiterGarbageCollectionTriggerCount
 			set.MeekRateLimiterReapHistoryFrequencySeconds = newSet.MeekRateLimiterReapHistoryFrequencySeconds
 			set.DefaultRules = newSet.DefaultRules
@@ -432,6 +444,13 @@ func (set *TrafficRulesSet) initLookups() {
 				filter.ispLookup[ISP] = true
 			}
 		}
+
+		if len(filter.Cities) >= stringLookupThreshold {
+			filter.cityLookup = make(map[string]bool)
+			for _, city := range filter.Cities {
+				filter.cityLookup[city] = true
+			}
+		}
 	}
 
 	initTrafficRulesLookups(&set.DefaultRules)
@@ -579,6 +598,18 @@ func (set *TrafficRulesSet) GetTrafficRules(
 			}
 		}
 
+		if len(filteredRules.Filter.Cities) > 0 {
+			if filteredRules.Filter.cityLookup != nil {
+				if !filteredRules.Filter.cityLookup[geoIPData.City] {
+					continue
+				}
+			} else {
+				if !common.Contains(filteredRules.Filter.Cities, geoIPData.City) {
+					continue
+				}
+			}
+		}
+
 		if filteredRules.Filter.APIProtocol != "" {
 			if !state.completed {
 				continue
@@ -803,7 +834,7 @@ func (rules *TrafficRules) allowSubnet(remoteIP net.IP) bool {
 
 // GetMeekRateLimiterConfig gets a snapshot of the meek rate limiter
 // configuration values.
-func (set *TrafficRulesSet) GetMeekRateLimiterConfig() (int, int, []string, []string, int, int) {
+func (set *TrafficRulesSet) GetMeekRateLimiterConfig() (int, int, []string, []string, []string, int, int) {
 
 	set.ReloadableFile.RLock()
 	defer set.ReloadableFile.RUnlock()
@@ -823,6 +854,7 @@ func (set *TrafficRulesSet) GetMeekRateLimiterConfig() (int, int, []string, []st
 		set.MeekRateLimiterThresholdSeconds,
 		set.MeekRateLimiterRegions,
 		set.MeekRateLimiterISPs,
+		set.MeekRateLimiterCities,
 		GCTriggerCount,
 		reapFrequencySeconds
 }