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

OSL enhancements

-  Add ASN target configurability
mirokuratczyk 1 год назад
Родитель
Сommit
8511cbd5af
4 измененных файлов с 137 добавлено и 29 удалено
  1. 56 16
      psiphon/common/osl/osl.go
  2. 74 11
      psiphon/common/osl/osl_test.go
  3. 1 1
      psiphon/remoteServerList_test.go
  4. 6 1
      psiphon/server/tunnelServer.go

+ 56 - 16
psiphon/common/osl/osl.go

@@ -103,7 +103,7 @@ type Scheme struct {
 	// SeedSpecs is the set of different client network activity patterns
 	// SeedSpecs is the set of different client network activity patterns
 	// that will result in issuing SLOKs. For a given time period, a distinct
 	// that will result in issuing SLOKs. For a given time period, a distinct
 	// SLOK is issued for each SeedSpec.
 	// SLOK is issued for each SeedSpec.
-	// Duplicate subnets may appear in multiple SeedSpecs.
+	// Duplicate subnets and asns may appear in multiple SeedSpecs.
 	SeedSpecs []*SeedSpec
 	SeedSpecs []*SeedSpec
 
 
 	// SeedSpecThreshold is the threshold scheme for combining SLOKs to
 	// SeedSpecThreshold is the threshold scheme for combining SLOKs to
@@ -135,7 +135,7 @@ type Scheme struct {
 	//   SeedPeriodNanoseconds = 100,000,000 = 100 milliseconds
 	//   SeedPeriodNanoseconds = 100,000,000 = 100 milliseconds
 	//   SeedPeriodKeySplits = [{10, 7}, {60, 5}]
 	//   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.
 	//   every 100 milliseconds.
 	//
 	//
 	//   Distinct OSLs are paved for every minute (60 seconds). Each OSL
 	//   Distinct OSLs are paved for every minute (60 seconds). Each OSL
@@ -149,6 +149,7 @@ type Scheme struct {
 
 
 	epoch                 time.Time
 	epoch                 time.Time
 	subnetLookups         []common.SubnetLookup
 	subnetLookups         []common.SubnetLookup
+	asnLookups            [][]string
 	derivedSLOKCacheMutex sync.RWMutex
 	derivedSLOKCacheMutex sync.RWMutex
 	derivedSLOKCache      map[slokReference]*SLOK
 	derivedSLOKCache      map[slokReference]*SLOK
 }
 }
@@ -156,15 +157,16 @@ type Scheme struct {
 // SeedSpec defines a client traffic pattern that results in a seeded SLOK.
 // 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
 // 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
 // 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
 // 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 {
 type SeedSpec struct {
 	Description     string
 	Description     string
 	ID              []byte
 	ID              []byte
 	UpstreamSubnets []string
 	UpstreamSubnets []string
+	UpstreamASNs    []string
 	Targets         TrafficValues
 	Targets         TrafficValues
 }
 }
 
 
@@ -198,6 +200,7 @@ type ClientSeedState struct {
 	signalIssueSLOKs     chan struct{}
 	signalIssueSLOKs     chan struct{}
 	issuedSLOKs          map[string]*SLOK
 	issuedSLOKs          map[string]*SLOK
 	payloadSLOKs         []*SLOK
 	payloadSLOKs         []*SLOK
+	lookupASN            func(net.IP) string
 }
 }
 
 
 // ClientSeedProgress tracks client progress towards seeding SLOKs for
 // ClientSeedProgress tracks client progress towards seeding SLOKs for
@@ -213,7 +216,7 @@ type ClientSeedProgress struct {
 
 
 // ClientSeedPortForward map a client port forward, which is relaying
 // ClientSeedPortForward map a client port forward, which is relaying
 // traffic to a specific upstream address, to all seed state progress
 // 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
 // As traffic is relayed through the port forwards, the bytes transferred
 // and duration count towards the progress of these SeedSpecs and
 // and duration count towards the progress of these SeedSpecs and
 // associated SLOKs.
 // associated SLOKs.
@@ -319,6 +322,7 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 
 
 		scheme.epoch = epoch
 		scheme.epoch = epoch
 		scheme.subnetLookups = make([]common.SubnetLookup, len(scheme.SeedSpecs))
 		scheme.subnetLookups = make([]common.SubnetLookup, len(scheme.SeedSpecs))
+		scheme.asnLookups = make([][]string, len(scheme.SeedSpecs))
 		scheme.derivedSLOKCache = make(map[slokReference]*SLOK)
 		scheme.derivedSLOKCache = make(map[slokReference]*SLOK)
 
 
 		if len(scheme.MasterKey) != KEY_LENGTH_BYTES {
 		if len(scheme.MasterKey) != KEY_LENGTH_BYTES {
@@ -342,6 +346,18 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 			}
 			}
 
 
 			scheme.subnetLookups[index] = subnetLookup
 			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{}{}
+				}
+			}
+
+			scheme.asnLookups[index] = seedSpec.UpstreamASNs
 		}
 		}
 
 
 		if !isValidShamirSplit(len(scheme.SeedSpecs), scheme.SeedSpecThreshold) {
 		if !isValidShamirSplit(len(scheme.SeedSpecs), scheme.SeedSpecThreshold) {
@@ -374,7 +390,8 @@ func LoadConfig(configJSON []byte) (*Config, error) {
 // should be appropriately buffered.
 // should be appropriately buffered.
 func (config *Config) NewClientSeedState(
 func (config *Config) NewClientSeedState(
 	clientRegion, propagationChannelID string,
 	clientRegion, propagationChannelID string,
-	signalIssueSLOKs chan struct{}) *ClientSeedState {
+	signalIssueSLOKs chan struct{},
+	lookupASN func(net.IP) string) *ClientSeedState {
 
 
 	config.ReloadableFile.RLock()
 	config.ReloadableFile.RLock()
 	defer config.ReloadableFile.RUnlock()
 	defer config.ReloadableFile.RUnlock()
@@ -384,6 +401,7 @@ func (config *Config) NewClientSeedState(
 		signalIssueSLOKs:     signalIssueSLOKs,
 		signalIssueSLOKs:     signalIssueSLOKs,
 		issuedSLOKs:          make(map[string]*SLOK),
 		issuedSLOKs:          make(map[string]*SLOK),
 		payloadSLOKs:         nil,
 		payloadSLOKs:         nil,
+		lookupASN:            lookupASN,
 	}
 	}
 
 
 	for _, scheme := range config.Schemes {
 	for _, scheme := range config.Schemes {
@@ -450,7 +468,7 @@ func (state *ClientSeedState) Resume(
 // NewClientSeedPortForward creates a new client port forward
 // NewClientSeedPortForward creates a new client port forward
 // traffic progress tracker. Port forward progress reported to the
 // traffic progress tracker. Port forward progress reported to the
 // ClientSeedPortForward is added to seed state progress for all
 // 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
 // The return value will be nil when activity for upstreamIPAddress
 // does not count towards any progress.
 // does not count towards any progress.
 // NewClientSeedPortForward may be invoked concurrently by many
 // NewClientSeedPortForward may be invoked concurrently by many
@@ -467,18 +485,42 @@ func (state *ClientSeedState) NewClientSeedPortForward(
 
 
 	var progressReferences []progressReference
 	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,
 	// and point to the progress for each. When progress is reported,
 	// it is added directly to all of these TrafficValues instances.
 	// it is added directly to all of these TrafficValues instances.
-	// Assumes state.progress entries correspond 1-to-1 with
-	// state.scheme.subnetLookups.
+	// Assumes state.seedProgress entries correspond 1-to-1 with
+	// state.scheme.subnetLookups and state.scheme.asnLookups.
 	// Note: this implementation assumes a small number of schemes and
 	// Note: this implementation assumes a small number of schemes and
 	// seed specs. For larger numbers, instead of N SubnetLookups, create
 	// seed specs. For larger numbers, instead of N SubnetLookups, create
 	// a single SubnetLookup which returns, for a given IP address, all
 	// a single SubnetLookup which returns, for a given IP address, all
 	// matching subnets and associated seed specs.
 	// matching subnets and associated seed specs.
 	for seedProgressIndex, seedProgress := range state.seedProgress {
 	for seedProgressIndex, seedProgress := range state.seedProgress {
-		for trafficProgressIndex, subnetLookup := range seedProgress.scheme.subnetLookups {
-			if subnetLookup.ContainsIPAddress(upstreamIPAddress) {
+
+		var upstreamASN *string
+
+		for trafficProgressIndex := 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 && state.lookupASN != nil {
+				// No subnet match. Check for ASN match.
+				asnLookup := seedProgress.scheme.asnLookups[trafficProgressIndex]
+				if len(asnLookup) > 0 {
+					// Lookup ASN on demand and only once.
+					if upstreamASN == nil {
+						upstreamASN = new(string)
+						*upstreamASN = state.lookupASN(upstreamIPAddress)
+					}
+					matchesSeedSpec = common.Contains(asnLookup, *upstreamASN)
+				}
+			}
+
+			if matchesSeedSpec {
 				progressReferences = append(
 				progressReferences = append(
 					progressReferences,
 					progressReferences,
 					progressReference{
 					progressReference{
@@ -671,9 +713,7 @@ func (state *ClientSeedState) GetSeedPayload() *SeedPayload {
 	state.issueSLOKs()
 	state.issueSLOKs()
 
 
 	sloks := make([]*SLOK, len(state.payloadSLOKs))
 	sloks := make([]*SLOK, len(state.payloadSLOKs))
-	for index, slok := range state.payloadSLOKs {
-		sloks[index] = slok
-	}
+	copy(sloks, state.payloadSLOKs)
 
 
 	return &SeedPayload{
 	return &SeedPayload{
 		SLOKs: sloks,
 		SLOKs: sloks,

+ 74 - 11
psiphon/common/osl/osl_test.go

@@ -62,6 +62,7 @@ func TestOSL(t *testing.T) {
           "Description": "spec2",
           "Description": "spec2",
           "ID" : "qvpIcORLE2Pi5TZmqRtVkEp+OKov0MhfsYPLNV7FYtI=",
           "ID" : "qvpIcORLE2Pi5TZmqRtVkEp+OKov0MhfsYPLNV7FYtI=",
           "UpstreamSubnets" : ["192.168.0.0/16", "10.0.0.0/8"],
           "UpstreamSubnets" : ["192.168.0.0/16", "10.0.0.0/8"],
+          "UpstreamASNs" : ["0000"],
           "Targets" :
           "Targets" :
           {
           {
               "BytesRead" : 10,
               "BytesRead" : 10,
@@ -171,9 +172,14 @@ func TestOSL(t *testing.T) {
 		t.Fatalf("LoadConfig failed: %s", err)
 		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) {
 	t.Run("ineligible client, sufficient transfer", func(t *testing.T) {
 
 
-		clientSeedState := config.NewClientSeedState("US", "C5E8D2EDFD093B50D8D65CF59D0263CA", nil)
+		clientSeedState := config.NewClientSeedState("US", "C5E8D2EDFD093B50D8D65CF59D0263CA", nil, lookupASN)
 
 
 		seedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1"))
 		seedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1"))
 
 
@@ -184,7 +190,7 @@ func TestOSL(t *testing.T) {
 
 
 	// This clientSeedState is used across multiple tests.
 	// This clientSeedState is used across multiple tests.
 	signalIssueSLOKs := make(chan struct{}, 1)
 	signalIssueSLOKs := make(chan struct{}, 1)
-	clientSeedState := config.NewClientSeedState("US", "2995DB0C968C59C4F23E87988D9C0D41", signalIssueSLOKs)
+	clientSeedState := config.NewClientSeedState("US", "2995DB0C968C59C4F23E87988D9C0D41", signalIssueSLOKs, lookupASN)
 
 
 	t.Run("eligible client, no transfer", func(t *testing.T) {
 	t.Run("eligible client, no transfer", func(t *testing.T) {
 
 
@@ -219,7 +225,7 @@ func TestOSL(t *testing.T) {
 		}
 		}
 	})
 	})
 
 
-	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()
 		rolloverToNextSLOKTime()
 
 
@@ -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()
 		rolloverToNextSLOKTime()
 
 
-		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5)
+		*portForwardASN = "0000"
 
 
-		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5)
+		clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("11.0.0.1"))
+
+		clientSeedPortForward.UpdateProgress(5, 5, 5)
+
+		clientSeedPortForward.UpdateProgress(5, 5, 5)
+
+		*portForwardASN = ""
 
 
 		select {
 		select {
 		case <-signalIssueSLOKs:
 		case <-signalIssueSLOKs:
@@ -260,11 +272,42 @@ 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()
 		rolloverToNextSLOKTime()
 
 
-		clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1")).UpdateProgress(5, 5, 5)
+		*portForwardASN = "0000"
+
+		clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1"))
+
+		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("10.0.0.1")).UpdateProgress(5, 5, 5)
 
 
 		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5)
 		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).UpdateProgress(5, 5, 5)
 
 
@@ -274,12 +317,32 @@ func TestOSL(t *testing.T) {
 			t.Fatalf("expected issue SLOKs signal")
 			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 {
 		if len(clientSeedState.GetSeedPayload().SLOKs) != 4 {
 			t.Fatalf("expected 4 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs))
 			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")).UpdateProgress(5, 5, 5)
+
+		clientSeedState.NewClientSeedPortForward(net.ParseIP("10.0.0.1")).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) {
 	t.Run("clear payload", func(t *testing.T) {
 		clientSeedState.ClearSeedPayload()
 		clientSeedState.ClearSeedPayload()
 
 
@@ -292,7 +355,7 @@ func TestOSL(t *testing.T) {
 
 
 		rolloverToNextSLOKTime()
 		rolloverToNextSLOKTime()
 
 
-		clientSeedState := config.NewClientSeedState("US", "36F1CF2DF1250BF0C7BA0629CE3DC657", nil)
+		clientSeedState := config.NewClientSeedState("US", "36F1CF2DF1250BF0C7BA0629CE3DC657", nil, lookupASN)
 
 
 		if len(clientSeedState.GetSeedPayload().SLOKs) != 1 {
 		if len(clientSeedState.GetSeedPayload().SLOKs) != 1 {
 			t.Fatalf("expected 1 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs))
 			t.Fatalf("expected 1 SLOKs, got %d", len(clientSeedState.GetSeedPayload().SLOKs))
@@ -303,7 +366,7 @@ func TestOSL(t *testing.T) {
 
 
 		rolloverToNextSLOKTime()
 		rolloverToNextSLOKTime()
 
 
-		clientSeedState := config.NewClientSeedState("US", "B4A780E67695595FA486E9B900EA7335", nil)
+		clientSeedState := config.NewClientSeedState("US", "B4A780E67695595FA486E9B900EA7335", nil, lookupASN)
 
 
 		clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1"))
 		clientSeedPortForward := clientSeedState.NewClientSeedPortForward(net.ParseIP("192.168.0.1"))
 
 

+ 1 - 1
psiphon/remoteServerList_test.go

@@ -229,7 +229,7 @@ func testObfuscatedRemoteServerLists(t *testing.T, omitMD5Sums bool) {
 		t.Fatalf("unexpected server entries")
 		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 := seedState.NewClientSeedPortForward(net.ParseIP("0.0.0.0"))
 	seedPortForward.UpdateProgress(1, 1, 1)
 	seedPortForward.UpdateProgress(1, 1, 1)
 	payload := seedState.GetSeedPayload()
 	payload := seedState.GetSeedPayload()

+ 6 - 1
psiphon/server/tunnelServer.go

@@ -3753,10 +3753,15 @@ func (sshClient *sshClient) setOSLConfig() {
 	//    port forwards will not send progress to the new client
 	//    port forwards will not send progress to the new client
 	//    seed state.
 	//    seed state.
 
 
+	lookupASN := func(IPAddress net.IP) string {
+		return sshClient.sshServer.support.GeoIPService.LookupISPForIP(IPAddress).ASN
+	}
+
 	sshClient.oslClientSeedState = sshClient.sshServer.support.OSLConfig.NewClientSeedState(
 	sshClient.oslClientSeedState = sshClient.sshServer.support.OSLConfig.NewClientSeedState(
 		sshClient.geoIPData.Country,
 		sshClient.geoIPData.Country,
 		propagationChannelID,
 		propagationChannelID,
-		sshClient.signalIssueSLOKs)
+		sshClient.signalIssueSLOKs,
+		lookupASN)
 }
 }
 
 
 // newClientSeedPortForward will return nil when no seeding is
 // newClientSeedPortForward will return nil when no seeding is