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

Add support for filtering by AS number in tactics and traffic rules

Rod Hynes 3 лет назад
Родитель
Сommit
ab9e25cfff

+ 38 - 6
psiphon/common/tactics/tactics.go

@@ -261,7 +261,8 @@ type Server struct {
 const (
 	GeoIPScopeRegion = 1
 	GeoIPScopeISP    = 2
-	GeoIPScopeCity   = 4
+	GeoIPScopeASN    = 4
+	GeoIPScopeCity   = 8
 )
 
 // Filter defines a filter to match against client attributes.
@@ -275,6 +276,9 @@ type Filter struct {
 	// ISPs specifies a list of GeoIP ISPs the client must match.
 	ISPs []string
 
+	// ASNs specifies a list of GeoIP ASNs the client must match.
+	ASNs []string
+
 	// Cities specifies a list of GeoIP Cities the client must match.
 	Cities []string
 
@@ -290,6 +294,7 @@ type Filter struct {
 
 	regionLookup map[string]bool
 	ispLookup    map[string]bool
+	asnLookup    map[string]bool
 	cityLookup   map[string]bool
 }
 
@@ -632,6 +637,13 @@ func (server *Server) initLookups() {
 			}
 		}
 
+		if len(filteredTactics.Filter.ASNs) >= stringLookupThreshold {
+			filteredTactics.Filter.asnLookup = make(map[string]bool)
+			for _, ASN := range filteredTactics.Filter.ASNs {
+				filteredTactics.Filter.asnLookup[ASN] = true
+			}
+		}
+
 		if len(filteredTactics.Filter.Cities) >= stringLookupThreshold {
 			filteredTactics.Filter.cityLookup = make(map[string]bool)
 			for _, city := range filteredTactics.Filter.Cities {
@@ -649,8 +661,8 @@ func (server *Server) initLookups() {
 		// there is a filter with region and ISP, while other regions will set only
 		// GeoIPScopeRegion.
 		//
-		// When any ISP or City appears in a filter without a Region, the regional
-		// map optimization is disabled.
+		// When any ISP, ASN, or City appears in a filter without a Region,
+		// the regional map optimization is disabled.
 
 		if len(filteredTactics.Filter.Regions) == 0 {
 			disableRegionScope := false
@@ -658,6 +670,10 @@ func (server *Server) initLookups() {
 				server.filterGeoIPScope |= GeoIPScopeISP
 				disableRegionScope = true
 			}
+			if len(filteredTactics.Filter.ASNs) > 0 {
+				server.filterGeoIPScope |= GeoIPScopeASN
+				disableRegionScope = true
+			}
 			if len(filteredTactics.Filter.Cities) > 0 {
 				server.filterGeoIPScope |= GeoIPScopeCity
 				disableRegionScope = true
@@ -675,6 +691,9 @@ func (server *Server) initLookups() {
 				if len(filteredTactics.Filter.ISPs) > 0 {
 					regionScope |= GeoIPScopeISP
 				}
+				if len(filteredTactics.Filter.ASNs) > 0 {
+					regionScope |= GeoIPScopeASN
+				}
 				if len(filteredTactics.Filter.Cities) > 0 {
 					regionScope |= GeoIPScopeCity
 				}
@@ -690,9 +709,10 @@ func (server *Server) initLookups() {
 }
 
 // GetFilterGeoIPScope returns which GeoIP fields are relevent to tactics
-// filters. The return value is a bit array containing some combination of the
-// GeoIPScopeRegion, GeoIPScopeISP, and GeoIPScopeCity flags. For the given
-// geoIPData, all tactics filters reference only the flagged fields.
+// filters. The return value is a bit array containing some combination of
+// the GeoIPScopeRegion, GeoIPScopeISP, GeoIPScopeASN, and GeoIPScopeCity
+// flags. For the given geoIPData, all tactics filters reference only the
+// flagged fields.
 func (server *Server) GetFilterGeoIPScope(geoIPData common.GeoIPData) int {
 
 	scope := server.filterGeoIPScope
@@ -857,6 +877,18 @@ func (server *Server) GetTactics(
 			}
 		}
 
+		if len(filteredTactics.Filter.ASNs) > 0 {
+			if filteredTactics.Filter.asnLookup != nil {
+				if !filteredTactics.Filter.asnLookup[geoIPData.ASN] {
+					continue
+				}
+			} else {
+				if !common.Contains(filteredTactics.Filter.ASNs, geoIPData.ASN) {
+					continue
+				}
+			}
+		}
+
 		if len(filteredTactics.Filter.Cities) > 0 {
 			if filteredTactics.Filter.cityLookup != nil {
 				if !filteredTactics.Filter.cityLookup[geoIPData.City] {

+ 87 - 17
psiphon/common/tactics/tactics_test.go

@@ -81,6 +81,7 @@ func TestTactics(t *testing.T) {
         {
           "Filter" : {
             "Regions": ["R1"],
+            "ASNs": ["1"],
             "APIParameters" : {"client_platform" : ["P1"], "client_version": ["V1"]},
             "SpeedTestRTTMilliseconds" : {
               "Aggregation" : "Median",
@@ -161,7 +162,7 @@ func TestTactics(t *testing.T) {
 
 	// Mock server uses an insecure HTTP transport that exposes endpoint names
 
-	clientGeoIPData := common.GeoIPData{Country: "R1"}
+	clientGeoIPData := common.GeoIPData{Country: "R1", ASN: "1"}
 
 	logger := newTestLogger()
 
@@ -777,9 +778,28 @@ func TestTacticsFilterGeoIPScope(t *testing.T) {
 		t.Fatalf("NewServer failed: %s", err)
 	}
 
+	reload := func() {
+		tacticsConfig = fmt.Sprintf(tacticsConfigTemplate, filteredTactics)
+
+		err = ioutil.WriteFile(configFileName, []byte(tacticsConfig), 0600)
+		if err != nil {
+			t.Fatalf("WriteFile failed: %s", err)
+		}
+
+		reloaded, err := server.Reload()
+		if err != nil {
+			t.Fatalf("Reload failed: %s", err)
+		}
+
+		if !reloaded {
+			t.Fatalf("Server config failed to reload")
+		}
+	}
+
 	geoIPData := common.GeoIPData{
 		Country: "R0",
 		ISP:     "I0",
+		ASN:     "0",
 		City:    "C0",
 	}
 
@@ -806,29 +826,36 @@ func TestTacticsFilterGeoIPScope(t *testing.T) {
       ]
 	`
 
-	reload := func() {
-		tacticsConfig = fmt.Sprintf(tacticsConfigTemplate, filteredTactics)
-
-		err = ioutil.WriteFile(configFileName, []byte(tacticsConfig), 0600)
-		if err != nil {
-			t.Fatalf("WriteFile failed: %s", err)
-		}
+	reload()
 
-		reloaded, err := server.Reload()
-		if err != nil {
-			t.Fatalf("Reload failed: %s", err)
-		}
+	scope = server.GetFilterGeoIPScope(geoIPData)
 
-		if !reloaded {
-			t.Fatalf("Server config failed to reload")
-		}
+	if scope != GeoIPScopeISP {
+		t.Fatalf("unexpected scope: %b", scope)
 	}
 
+	// Test: ASN-only scope
+
+	filteredTactics = `
+      "FilteredTactics" : [
+        {
+          "Filter" : {
+            "ASNs": ["1", "2", "3"]
+          }
+        },
+        {
+          "Filter" : {
+            "ASNs": ["4", "5", "6"]
+          }
+        }
+      ]
+	`
+
 	reload()
 
 	scope = server.GetFilterGeoIPScope(geoIPData)
 
-	if scope != GeoIPScopeISP {
+	if scope != GeoIPScopeASN {
 		t.Fatalf("unexpected scope: %b", scope)
 	}
 
@@ -871,6 +898,11 @@ func TestTacticsFilterGeoIPScope(t *testing.T) {
             "ISPs": ["I1", "I2", "I3"]
           }
         },
+        {
+          "Filter" : {
+            "ASNs": ["1", "2", "3"]
+          }
+        },
         {
           "Filter" : {
             "Cities": ["C4", "C5", "C6"]
@@ -883,7 +915,7 @@ func TestTacticsFilterGeoIPScope(t *testing.T) {
 
 	scope = server.GetFilterGeoIPScope(geoIPData)
 
-	if scope != GeoIPScopeRegion|GeoIPScopeISP|GeoIPScopeCity {
+	if scope != GeoIPScopeRegion|GeoIPScopeISP|GeoIPScopeASN|GeoIPScopeCity {
 		t.Fatalf("unexpected scope: %b", scope)
 	}
 
@@ -921,6 +953,32 @@ func TestTacticsFilterGeoIPScope(t *testing.T) {
             "ISPs": ["I3b"],
             "Cities": ["C3b"]
           }
+        },
+        {
+          "Filter" : {
+            "Regions": ["R4"],
+            "ASNs": ["4"]
+          }
+        },
+        {
+          "Filter" : {
+            "Regions": ["R4"],
+            "ASNs": ["4"]
+          }
+        },
+        {
+          "Filter" : {
+            "Regions": ["R5"],
+            "ASNs": ["5"],
+            "Cities": ["C3a"]
+          }
+        },
+        {
+          "Filter" : {
+            "Regions": ["R5"],
+            "ASNs": ["5"],
+            "Cities": ["C3b"]
+          }
         }
       ]
 	`
@@ -951,6 +1009,18 @@ func TestTacticsFilterGeoIPScope(t *testing.T) {
 		t.Fatalf("unexpected scope: %b", scope)
 	}
 
+	scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R4"})
+
+	if scope != GeoIPScopeRegion|GeoIPScopeASN {
+		t.Fatalf("unexpected scope: %b", scope)
+	}
+
+	scope = server.GetFilterGeoIPScope(common.GeoIPData{Country: "R5"})
+
+	if scope != GeoIPScopeRegion|GeoIPScopeASN|GeoIPScopeCity {
+		t.Fatalf("unexpected scope: %b", scope)
+	}
+
 	// Test: reset regional map optimization
 
 	filteredTactics = `

+ 9 - 2
psiphon/server/meek.go

@@ -180,7 +180,7 @@ func NewMeekServer(
 		bufferCount = support.Config.MeekCachedResponsePoolBufferCount
 	}
 
-	_, thresholdSeconds, _, _, _, _, _, reapFrequencySeconds, maxEntries :=
+	_, thresholdSeconds, _, _, _, _, _, _, reapFrequencySeconds, maxEntries :=
 		support.TrafficRulesSet.GetMeekRateLimiterConfig()
 
 	rateLimitHistory := lrucache.NewWithLRU(
@@ -848,6 +848,7 @@ func (server *MeekServer) rateLimit(
 		tunnelProtocols,
 		regions,
 		ISPs,
+		ASNs,
 		cities,
 		GCTriggerCount, _, _ :=
 		server.support.TrafficRulesSet.GetMeekRateLimiterConfig()
@@ -862,7 +863,7 @@ func (server *MeekServer) rateLimit(
 		}
 	}
 
-	if len(regions) > 0 || len(ISPs) > 0 || len(cities) > 0 {
+	if len(regions) > 0 || len(ISPs) > 0 || len(ASNs) > 0 || len(cities) > 0 {
 
 		if len(regions) > 0 {
 			if !common.Contains(regions, geoIPData.Country) {
@@ -876,6 +877,12 @@ func (server *MeekServer) rateLimit(
 			}
 		}
 
+		if len(ASNs) > 0 {
+			if !common.Contains(ASNs, geoIPData.ASN) {
+				return false
+			}
+		}
+
 		if len(cities) > 0 {
 			if !common.Contains(cities, geoIPData.City) {
 				return false

+ 7 - 4
psiphon/server/tactics.go

@@ -162,8 +162,8 @@ func (c *ServerTacticsParametersCache) Get(
 	// Two optimizations are used to limit the memory size of the cache:
 	//
 	// 1. The scope of the GeoIP data cache key is limited to the fields --
-	// Country/ISP/City -- that are present in tactics filters. E.g., if only
-	// Country appears in filters, then the key will omit ISP and City.
+	// Country/ISP/ASN/City -- that are present in tactics filters. E.g., if only
+	// Country appears in filters, then the key will omit IS, ASN, and City.
 	//
 	// 2. Two maps are maintained: GeoIP-key -> tactics-tag; and tactics-tag ->
 	// parameters. For N keys with the same filtered parameters, the mapped value
@@ -232,7 +232,7 @@ func (c *ServerTacticsParametersCache) makeKey(geoIPData GeoIPData) string {
 	scope := c.support.TacticsServer.GetFilterGeoIPScope(
 		common.GeoIPData(geoIPData))
 
-	var region, ISP, city string
+	var region, ISP, ASN, city string
 
 	if scope&tactics.GeoIPScopeRegion != 0 {
 		region = geoIPData.Country
@@ -240,9 +240,12 @@ func (c *ServerTacticsParametersCache) makeKey(geoIPData GeoIPData) string {
 	if scope&tactics.GeoIPScopeISP != 0 {
 		ISP = geoIPData.ISP
 	}
+	if scope&tactics.GeoIPScopeASN != 0 {
+		ASN = geoIPData.ASN
+	}
 	if scope&tactics.GeoIPScopeCity != 0 {
 		city = geoIPData.City
 	}
 
-	return fmt.Sprintf("%s-%s-%s", region, ISP, city)
+	return fmt.Sprintf("%s-%s-%s-%s", region, ISP, ASN, city)
 }

+ 85 - 4
psiphon/server/tactics_test.go

@@ -86,6 +86,39 @@ func TestServerTacticsParametersCache(t *testing.T) {
               "ConnectionWorkerPoolSize" : 4
             }
           }
+        },
+        {
+          "Filter" : {
+            "Regions": ["R3"],
+            "ASNs": ["31"]
+          },
+          "Tactics" : {
+            "Parameters" : {
+              "ConnectionWorkerPoolSize" : 5
+            }
+          }
+        },
+        {
+          "Filter" : {
+            "Regions": ["R3"],
+            "ASNs": ["32"]
+          },
+          "Tactics" : {
+            "Parameters" : {
+              "ConnectionWorkerPoolSize" : 6
+            }
+          }
+        },
+        {
+          "Filter" : {
+            "Regions": ["R3"],
+            "ASNs": ["33"]
+          },
+          "Tactics" : {
+            "Parameters" : {
+              "ConnectionWorkerPoolSize" : 6
+            }
+          }
         }
       ]
     }
@@ -204,6 +237,54 @@ func TestServerTacticsParametersCache(t *testing.T) {
 			4,
 			4, 5, 4,
 		},
+		{
+			"region already cached, region-only key",
+			GeoIPData{Country: "R0", ASN: "0", City: "C1"},
+			1,
+			5, 5, 4,
+		},
+		{
+			"region already cached, region-only key",
+			GeoIPData{Country: "R1", ASN: "1", City: "C1a"},
+			2,
+			5, 5, 4,
+		},
+		{
+			"add new cache entry, filtered parameter, region/ASN key",
+			GeoIPData{Country: "R3", ASN: "31", City: "C2a"},
+			5,
+			5, 6, 5,
+		},
+		{
+			"region/ASN already cached",
+			GeoIPData{Country: "R3", ASN: "31", City: "C2a"},
+			5,
+			6, 6, 5,
+		},
+		{
+			"region/ASN already cached, city is ignored",
+			GeoIPData{Country: "R3", ASN: "31", City: "C2b"},
+			5,
+			6, 6, 5,
+		},
+		{
+			"add new cache entry, filtered parameter, region/ASN key",
+			GeoIPData{Country: "R3", ASN: "32", City: "C2a"},
+			6,
+			6, 7, 6,
+		},
+		{
+			"region/ASN already cached, city is ignored",
+			GeoIPData{Country: "R3", ASN: "32", City: "C2b"},
+			6,
+			7, 7, 6,
+		},
+		{
+			"add new cache entry, filtered parameter, region/ASN key, duplicate parameters",
+			GeoIPData{Country: "R3", ASN: "33", City: "C2a"},
+			6,
+			7, 8, 6,
+		},
 	}
 
 	for _, testCase := range keySplitTestCases {
@@ -244,10 +325,10 @@ func TestServerTacticsParametersCache(t *testing.T) {
 	}
 
 	metrics := support.ServerTacticsParametersCache.GetMetrics()
-	if metrics["server_tactics_max_cache_entries"].(int64) != 5 ||
-		metrics["server_tactics_max_parameter_references"].(int64) != 4 ||
-		metrics["server_tactics_cache_hit_count"].(int64) != 7 ||
-		metrics["server_tactics_cache_miss_count"].(int64) != 5 {
+	if metrics["server_tactics_max_cache_entries"].(int64) != 8 ||
+		metrics["server_tactics_max_parameter_references"].(int64) != 6 ||
+		metrics["server_tactics_cache_hit_count"].(int64) != 12 ||
+		metrics["server_tactics_cache_miss_count"].(int64) != 8 {
 
 		t.Fatalf("unexpected metrics: %v", metrics)
 	}

+ 37 - 5
psiphon/server/trafficRules.go

@@ -80,7 +80,7 @@ type TrafficRulesSet struct {
 	// itself may hold open the interrupted connection.
 	//
 	// The scope of rate limiting may be
-	// limited using LimitMeekRateLimiterTunnelProtocols/Regions/ISPs/Cities.
+	// limited using LimitMeekRateLimiterTunnelProtocols/Regions/ISPs/ASNs/Cities.
 	//
 	// Upon hot reload,
 	// MeekRateLimiterHistorySize/MeekRateLimiterThresholdSeconds are not
@@ -100,19 +100,25 @@ type TrafficRulesSet struct {
 	// MeekRateLimiterRegions, if set, limits application of the meek
 	// late-stage rate limiter to clients in the specified list of GeoIP
 	// countries. When omitted or empty, meek rate limiting, if configured,
-	// is applied to all client countries.
+	// is applied to any client country.
 	MeekRateLimiterRegions []string
 
 	// MeekRateLimiterISPs, if set, limits application of the meek
 	// late-stage rate limiter to clients in the specified list of GeoIP
 	// ISPs. When omitted or empty, meek rate limiting, if configured,
-	// is applied to all client ISPs.
+	// is applied to any client ISP.
 	MeekRateLimiterISPs []string
 
+	// MeekRateLimiterASNs, if set, limits application of the meek
+	// late-stage rate limiter to clients in the specified list of GeoIP
+	// ASNs. When omitted or empty, meek rate limiting, if configured,
+	// is applied to any client ASN.
+	MeekRateLimiterASNs []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.
+	// is applied to any client city.
 	MeekRateLimiterCities []string
 
 	// MeekRateLimiterGarbageCollectionTriggerCount specifies the number of
@@ -156,6 +162,10 @@ type TrafficRulesFilter struct {
 	// match this filter. When omitted or empty, any client ISP matches.
 	ISPs []string
 
+	// ASNs is a list of ASNs that the client must geolocate to in order to
+	// match this filter. When omitted or empty, any client ASN matches.
+	ASNs []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
@@ -190,6 +200,7 @@ type TrafficRulesFilter struct {
 
 	regionLookup                map[string]bool
 	ispLookup                   map[string]bool
+	asnLookup                   map[string]bool
 	cityLookup                  map[string]bool
 	activeAuthorizationIDLookup map[string]bool
 }
@@ -344,6 +355,7 @@ func NewTrafficRulesSet(filename string) (*TrafficRulesSet, error) {
 			set.MeekRateLimiterTunnelProtocols = newSet.MeekRateLimiterTunnelProtocols
 			set.MeekRateLimiterRegions = newSet.MeekRateLimiterRegions
 			set.MeekRateLimiterISPs = newSet.MeekRateLimiterISPs
+			set.MeekRateLimiterASNs = newSet.MeekRateLimiterASNs
 			set.MeekRateLimiterCities = newSet.MeekRateLimiterCities
 			set.MeekRateLimiterGarbageCollectionTriggerCount = newSet.MeekRateLimiterGarbageCollectionTriggerCount
 			set.MeekRateLimiterReapHistoryFrequencySeconds = newSet.MeekRateLimiterReapHistoryFrequencySeconds
@@ -468,6 +480,13 @@ func (set *TrafficRulesSet) initLookups() {
 			}
 		}
 
+		if len(filter.ASNs) >= stringLookupThreshold {
+			filter.asnLookup = make(map[string]bool)
+			for _, ASN := range filter.ASNs {
+				filter.asnLookup[ASN] = true
+			}
+		}
+
 		if len(filter.Cities) >= stringLookupThreshold {
 			filter.cityLookup = make(map[string]bool)
 			for _, city := range filter.Cities {
@@ -632,6 +651,18 @@ func (set *TrafficRulesSet) GetTrafficRules(
 			}
 		}
 
+		if len(filteredRules.Filter.ASNs) > 0 {
+			if filteredRules.Filter.asnLookup != nil {
+				if !filteredRules.Filter.asnLookup[geoIPData.ASN] {
+					continue
+				}
+			} else {
+				if !common.Contains(filteredRules.Filter.ASNs, geoIPData.ASN) {
+					continue
+				}
+			}
+		}
+
 		if len(filteredRules.Filter.Cities) > 0 {
 			if filteredRules.Filter.cityLookup != nil {
 				if !filteredRules.Filter.cityLookup[geoIPData.City] {
@@ -870,7 +901,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, []string, []string, int, int, int) {
+	int, int, []string, []string, []string, []string, []string, int, int, int) {
 
 	set.ReloadableFile.RLock()
 	defer set.ReloadableFile.RUnlock()
@@ -897,6 +928,7 @@ func (set *TrafficRulesSet) GetMeekRateLimiterConfig() (
 		set.MeekRateLimiterTunnelProtocols,
 		set.MeekRateLimiterRegions,
 		set.MeekRateLimiterISPs,
+		set.MeekRateLimiterASNs,
 		set.MeekRateLimiterCities,
 		GCTriggerCount,
 		reapFrequencySeconds,