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

Merge pull request #678 from mirokuratczyk/osl-asn-bytes

OSL enhancements
Rod Hynes 1 год назад
Родитель
Сommit
4cb9826b5d

+ 53 - 15
psiphon/common/osl/osl.go

@@ -103,7 +103,7 @@ type Scheme struct {
 	// SeedSpecs is the set of different client network activity patterns
 	// that will result in issuing SLOKs. For a given time period, a distinct
 	// SLOK is issued for each SeedSpec.
-	// Duplicate subnets may appear in multiple SeedSpecs.
+	// Duplicate subnets and ASNs may appear in multiple SeedSpecs.
 	SeedSpecs []*SeedSpec
 
 	// SeedSpecThreshold is the threshold scheme for combining SLOKs to
@@ -135,7 +135,7 @@ type Scheme struct {
 	//   SeedPeriodNanoseconds = 100,000,000 = 100 milliseconds
 	//   SeedPeriodKeySplits = [{10, 7}, {60, 5}]
 	//
-	//   In these scheme, up to 3 distinct SLOKs, one per spec, are issued
+	//   In this scheme, up to 3 distinct SLOKs, one per spec, are issued
 	//   every 100 milliseconds.
 	//
 	//   Distinct OSLs are paved for every minute (60 seconds). Each OSL
@@ -156,15 +156,16 @@ type Scheme struct {
 // SeedSpec defines a client traffic pattern that results in a seeded SLOK.
 // For each time period, a unique SLOK is issued to a client that meets the
 // traffic levels specified in Targets. All upstream port forward traffic to
-// UpstreamSubnets is counted towards the targets.
+// UpstreamSubnets and UpstreamASNs are counted towards the targets.
 //
 // ID is a SLOK key derivation component and must be 32 random bytes, base64
-// encoded. UpstreamSubnets is a list of CIDRs. Description is not used; it's
-// for JSON config file comments.
+// encoded. UpstreamSubnets is a list of CIDRs. UpstreamASNs is a list of
+// ASNs. Description is not used; it's for JSON config file comments.
 type SeedSpec struct {
 	Description     string
 	ID              []byte
 	UpstreamSubnets []string
+	UpstreamASNs    []string
 	Targets         TrafficValues
 }
 
@@ -213,7 +214,7 @@ type ClientSeedProgress struct {
 
 // ClientSeedPortForward map a client port forward, which is relaying
 // traffic to a specific upstream address, to all seed state progress
-// counters for SeedSpecs with subnets containing the upstream address.
+// counters for SeedSpecs with subnets and ASNs containing the upstream address.
 // As traffic is relayed through the port forwards, the bytes transferred
 // and duration count towards the progress of these SeedSpecs and
 // associated SLOKs.
@@ -342,6 +343,16 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 			}
 
 			scheme.subnetLookups[index] = subnetLookup
+
+			// Ensure there are no duplicates.
+			ASNs := make(map[string]struct{}, len(seedSpec.UpstreamASNs))
+			for _, ASN := range seedSpec.UpstreamASNs {
+				if _, ok := ASNs[ASN]; ok {
+					return nil, errors.Tracef("invalid upstream ASNs, duplicate ASN: %s", ASN)
+				} else {
+					ASNs[ASN] = struct{}{}
+				}
+			}
 		}
 
 		if !isValidShamirSplit(len(scheme.SeedSpecs), scheme.SeedSpecThreshold) {
@@ -450,13 +461,14 @@ func (state *ClientSeedState) Resume(
 // NewClientSeedPortForward creates a new client port forward
 // traffic progress tracker. Port forward progress reported to the
 // ClientSeedPortForward is added to seed state progress for all
-// seed specs containing upstreamIPAddress in their subnets.
+// seed specs containing upstreamIPAddress in their subnets or ASNs.
 // The return value will be nil when activity for upstreamIPAddress
 // does not count towards any progress.
 // NewClientSeedPortForward may be invoked concurrently by many
 // psiphond port forward establishment goroutines.
 func (state *ClientSeedState) NewClientSeedPortForward(
-	upstreamIPAddress net.IP) *ClientSeedPortForward {
+	upstreamIPAddress net.IP,
+	lookupASN func(net.IP) string) *ClientSeedPortForward {
 
 	// Concurrency: access to ClientSeedState is unsynchronized
 	// but references only read-only fields.
@@ -467,18 +479,46 @@ func (state *ClientSeedState) NewClientSeedPortForward(
 
 	var progressReferences []progressReference
 
-	// Determine which seed spec subnets contain upstreamIPAddress
+	// Determine which seed spec subnets and ASNs contain upstreamIPAddress
 	// and point to the progress for each. When progress is reported,
 	// it is added directly to all of these TrafficValues instances.
-	// Assumes state.progress entries correspond 1-to-1 with
+	// Assumes state.seedProgress entries correspond 1-to-1 with
 	// state.scheme.subnetLookups.
 	// Note: this implementation assumes a small number of schemes and
 	// seed specs. For larger numbers, instead of N SubnetLookups, create
 	// a single SubnetLookup which returns, for a given IP address, all
 	// matching subnets and associated seed specs.
 	for seedProgressIndex, seedProgress := range state.seedProgress {
-		for trafficProgressIndex, subnetLookup := range seedProgress.scheme.subnetLookups {
-			if subnetLookup.ContainsIPAddress(upstreamIPAddress) {
+
+		var upstreamASN string
+		var upstreamASNSet bool
+
+		for trafficProgressIndex, seedSpec := range seedProgress.scheme.SeedSpecs {
+
+			matchesSeedSpec := false
+
+			// First check for subnet match before performing more expensive
+			// check for ASN match.
+			subnetLookup := seedProgress.scheme.subnetLookups[trafficProgressIndex]
+			matchesSeedSpec = subnetLookup.ContainsIPAddress(upstreamIPAddress)
+
+			if !matchesSeedSpec && lookupASN != nil {
+				// No subnet match. Check for ASN match.
+				if len(seedSpec.UpstreamASNs) > 0 {
+					// Lookup ASN on demand and only once.
+					if !upstreamASNSet {
+						upstreamASN = lookupASN(upstreamIPAddress)
+						upstreamASNSet = true
+					}
+					// TODO: use a map for faster lookups when the number of
+					// string values to compare against exceeds a threshold
+					// where benchmarks show maps are faster than looping
+					// through a string slice.
+					matchesSeedSpec = common.Contains(seedSpec.UpstreamASNs, upstreamASN)
+				}
+			}
+
+			if matchesSeedSpec {
 				progressReferences = append(
 					progressReferences,
 					progressReference{
@@ -671,9 +711,7 @@ func (state *ClientSeedState) GetSeedPayload() *SeedPayload {
 	state.issueSLOKs()
 
 	sloks := make([]*SLOK, len(state.payloadSLOKs))
-	for index, slok := range state.payloadSLOKs {
-		sloks[index] = slok
-	}
+	copy(sloks, state.payloadSLOKs)
 
 	return &SeedPayload{
 		SLOKs: sloks,

+ 76 - 13
psiphon/common/osl/osl_test.go

@@ -62,6 +62,7 @@ func TestOSL(t *testing.T) {
           "Description": "spec2",
           "ID" : "qvpIcORLE2Pi5TZmqRtVkEp+OKov0MhfsYPLNV7FYtI=",
           "UpstreamSubnets" : ["192.168.0.0/16", "10.0.0.0/8"],
+          "UpstreamASNs" : ["0000"],
           "Targets" :
           {
               "BytesRead" : 10,
@@ -171,11 +172,16 @@ func TestOSL(t *testing.T) {
 		t.Fatalf("LoadConfig failed: %s", err)
 	}
 
+	portForwardASN := new(string)
+	lookupASN := func(net.IP) string {
+		return *portForwardASN
+	}
+
 	t.Run("ineligible client, sufficient transfer", func(t *testing.T) {
 
 		clientSeedState := config.NewClientSeedState("US", "C5E8D2EDFD093B50D8D65CF59D0263CA", nil)
 
-		seedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1"))
+		seedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1"), lookupASN)
 
 		if seedPortForward != nil {
 			t.Fatalf("expected nil client seed port forward")
@@ -195,7 +201,7 @@ func TestOSL(t *testing.T) {
 
 	t.Run("eligible client, insufficient transfer", func(t *testing.T) {
 
-		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5)
+		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1"), lookupASN).UpdateProgress(5, 5, 5)
 
 		if len(clientSeedState.GetSeedPayload().SLOKs) != 0 {
 			t.Fatalf("expected 0 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs))
@@ -212,18 +218,18 @@ func TestOSL(t *testing.T) {
 
 		rolloverToNextSLOKTime()
 
-		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5)
+		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1"), lookupASN).UpdateProgress(5, 5, 5)
 
 		if len(clientSeedState.GetSeedPayload().SLOKs) != 0 {
 			t.Fatalf("expected 0 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs))
 		}
 	})
 
-	t.Run("eligible client, sufficient transfer, one port forward", func(t *testing.T) {
+	t.Run("eligible client, sufficient transfer, one port forward, match by ip", func(t *testing.T) {
 
 		rolloverToNextSLOKTime()
 
-		clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1"))
+		clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1"), lookupASN)
 
 		clientSeedPortForward.UpdateProgress(5, 5, 5)
 
@@ -240,13 +246,19 @@ func TestOSL(t *testing.T) {
 		}
 	})
 
-	t.Run("eligible client, sufficient transfer, multiple port forwards", func(t *testing.T) {
+	t.Run("eligible client, sufficient transfer, one port forward, match by asn", func(t *testing.T) {
 
 		rolloverToNextSLOKTime()
 
-		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5)
+		*portForwardASN = "0000"
+
+		clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("11.0.0.1"), lookupASN)
+
+		clientSeedPortForward.UpdateProgress(5, 5, 5)
+
+		clientSeedPortForward.UpdateProgress(5, 5, 5)
 
-		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5)
+		*portForwardASN = ""
 
 		select {
 		case <-signalIssueSLOKs:
@@ -260,13 +272,44 @@ func TestOSL(t *testing.T) {
 		}
 	})
 
-	t.Run("eligible client, sufficient transfer multiple SLOKs", func(t *testing.T) {
+	t.Run("eligible client, sufficient transfer, one port forward, match by ip and asn", func(t *testing.T) {
+
+		rolloverToNextSLOKTime()
+
+		*portForwardASN = "0000"
+
+		clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1"), lookupASN)
+
+		clientSeedPortForward.UpdateProgress(5, 5, 5)
+
+		// Check that progress is not double counted.
+		if len(clientSeedState.GetSeedPayload().SLOKs) != 2 {
+			t.Fatalf("expected 2 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs))
+		}
+
+		clientSeedPortForward.UpdateProgress(5, 5, 5)
+
+		*portForwardASN = ""
+
+		select {
+		case <-signalIssueSLOKs:
+		default:
+			t.Fatalf("expected issue SLOKs signal")
+		}
+
+		// Expect 3 SLOKS: 1 new, and 2 remaining in payload.
+		if len(clientSeedState.GetSeedPayload().SLOKs) != 3 {
+			t.Fatalf("expected 3 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs))
+		}
+	})
+
+	t.Run("eligible client, sufficient transfer, multiple port forwards", func(t *testing.T) {
 
 		rolloverToNextSLOKTime()
 
-		clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1")).UpdateProgress(5, 5, 5)
+		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1"), lookupASN).UpdateProgress(5, 5, 5)
 
-		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5)
+		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1"), lookupASN).UpdateProgress(5, 5, 5)
 
 		select {
 		case <-signalIssueSLOKs:
@@ -274,12 +317,32 @@ func TestOSL(t *testing.T) {
 			t.Fatalf("expected issue SLOKs signal")
 		}
 
-		// Expect 4 SLOKS: 2 new, and 2 remaining in payload.
+		// Expect 4 SLOKS: 1 new, and 3 remaining in payload.
 		if len(clientSeedState.GetSeedPayload().SLOKs) != 4 {
 			t.Fatalf("expected 4 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs))
 		}
 	})
 
+	t.Run("eligible client, sufficient transfer multiple SLOKs", func(t *testing.T) {
+
+		rolloverToNextSLOKTime()
+
+		clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1"), lookupASN).UpdateProgress(5, 5, 5)
+
+		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1"), lookupASN).UpdateProgress(5, 5, 5)
+
+		select {
+		case <-signalIssueSLOKs:
+		default:
+			t.Fatalf("expected issue SLOKs signal")
+		}
+
+		// Expect 6 SLOKS: 2 new, and 4 remaining in payload.
+		if len(clientSeedState.GetSeedPayload().SLOKs) != 6 {
+			t.Fatalf("expected 6 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs))
+		}
+	})
+
 	t.Run("clear payload", func(t *testing.T) {
 		clientSeedState.ClearSeedPayload()
 
@@ -305,7 +368,7 @@ func TestOSL(t *testing.T) {
 
 		clientSeedState := config.NewClientSeedState("US", "B4A780E67695595FA486E9B900EA7335", nil)
 
-		clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1"))
+		clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1"), lookupASN)
 
 		clientSeedPortForward.UpdateProgress(10, 10, 10)
 

+ 1 - 1
psiphon/remoteServerList_test.go

@@ -229,7 +229,7 @@ func testObfuscatedRemoteServerLists(t *testing.T, omitMD5Sums bool) {
 		t.Fatalf("unexpected server entries")
 	}
 
-	seedState := oslConfig.NewClientSeedState("", propagationChannelID, nil)
+	seedState := oslConfig.NewClientSeedState("", propagationChannelID, nil, nil)
 	seedPortForward := seedState.NewClientSeedPortForward(net.ParseIP("0.0.0.0"))
 	seedPortForward.UpdateProgress(1, 1, 1)
 	payload := seedState.GetSeedPayload()

+ 2 - 1
psiphon/server/geoip.go

@@ -209,7 +209,8 @@ func (geoIP *GeoIPService) LookupIP(IP net.IP) GeoIPData {
 
 // LookupISPForIP determines a GeoIPData for a given client IP address. Only
 // ISP, ASN, and ASO fields will be populated. This lookup is faster than a
-// full lookup.
+// full lookup. Benchmarks show this lookup is <= ~1 microsecond against the
+// production geo IP database.
 func (geoIP *GeoIPService) LookupISPForIP(IP net.IP) GeoIPData {
 	return geoIP.lookupIP(IP, true)
 }

+ 7 - 1
psiphon/server/tunnelServer.go

@@ -3770,7 +3770,13 @@ func (sshClient *sshClient) newClientSeedPortForward(IPAddress net.IP) *osl.Clie
 		return nil
 	}
 
-	return sshClient.oslClientSeedState.NewClientSeedPortForward(IPAddress)
+	lookupASN := func(IP net.IP) string {
+		// TODO: there are potentially multiple identical geo IP lookups per new
+		// port forward and flow, cache and use result of first lookup.
+		return sshClient.sshServer.support.GeoIPService.LookupISPForIP(IP).ASN
+	}
+
+	return sshClient.oslClientSeedState.NewClientSeedPortForward(IPAddress, lookupASN)
 }
 
 // getOSLSeedPayload returns a payload containing all seeded SLOKs for