github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/server/tactics_test.go (about)

     1  /*
     2   * Copyright (c) 2020, Psiphon Inc.
     3   * All rights reserved.
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License as published by
     7   * the Free Software Foundation, either version 3 of the License, or
     8   * (at your option) any later version.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package server
    21  
    22  import (
    23  	"fmt"
    24  	"io/ioutil"
    25  	"path/filepath"
    26  	"testing"
    27  
    28  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters"
    29  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
    30  )
    31  
    32  func TestServerTacticsParametersCache(t *testing.T) {
    33  
    34  	tacticsConfigJSONFormat := `
    35      {
    36        "RequestPublicKey" : "%s",
    37        "RequestPrivateKey" : "%s",
    38        "RequestObfuscatedKey" : "%s",
    39        "DefaultTactics" : {
    40          "TTL" : "60s",
    41          "Probability" : 1.0,
    42          "Parameters" : {
    43            "ConnectionWorkerPoolSize" : 1
    44          }
    45        },
    46        "FilteredTactics" : [
    47          {
    48            "Filter" : {
    49              "Regions": ["R1"]
    50            },
    51            "Tactics" : {
    52              "Parameters" : {
    53                "ConnectionWorkerPoolSize" : 2
    54              }
    55            }
    56          },
    57          {
    58            "Filter" : {
    59              "Regions": ["R2"],
    60              "ISPs": ["I2a"]
    61            },
    62            "Tactics" : {
    63              "Parameters" : {
    64                "ConnectionWorkerPoolSize" : 3
    65              }
    66            }
    67          },
    68          {
    69            "Filter" : {
    70              "Regions": ["R2"],
    71              "ISPs": ["I2b"]
    72            },
    73            "Tactics" : {
    74              "Parameters" : {
    75                "ConnectionWorkerPoolSize" : 4
    76              }
    77            }
    78          },
    79          {
    80            "Filter" : {
    81              "Regions": ["R2"],
    82              "ISPs": ["I2c"]
    83            },
    84            "Tactics" : {
    85              "Parameters" : {
    86                "ConnectionWorkerPoolSize" : 4
    87              }
    88            }
    89          },
    90          {
    91            "Filter" : {
    92              "Regions": ["R3"],
    93              "ASNs": ["31"]
    94            },
    95            "Tactics" : {
    96              "Parameters" : {
    97                "ConnectionWorkerPoolSize" : 5
    98              }
    99            }
   100          },
   101          {
   102            "Filter" : {
   103              "Regions": ["R3"],
   104              "ASNs": ["32"]
   105            },
   106            "Tactics" : {
   107              "Parameters" : {
   108                "ConnectionWorkerPoolSize" : 6
   109              }
   110            }
   111          },
   112          {
   113            "Filter" : {
   114              "Regions": ["R3"],
   115              "ASNs": ["33"]
   116            },
   117            "Tactics" : {
   118              "Parameters" : {
   119                "ConnectionWorkerPoolSize" : 6
   120              }
   121            }
   122          }
   123        ]
   124      }
   125      `
   126  
   127  	tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey, err :=
   128  		tactics.GenerateKeys()
   129  	if err != nil {
   130  		t.Fatalf("error generating tactics keys: %s", err)
   131  	}
   132  
   133  	tacticsConfigJSON := fmt.Sprintf(
   134  		tacticsConfigJSONFormat,
   135  		tacticsRequestPublicKey, tacticsRequestPrivateKey, tacticsRequestObfuscatedKey)
   136  
   137  	tacticsConfigFilename := filepath.Join(testDataDirName, "tactics_config.json")
   138  
   139  	err = ioutil.WriteFile(tacticsConfigFilename, []byte(tacticsConfigJSON), 0600)
   140  	if err != nil {
   141  		t.Fatalf("error paving tactics config file: %s", err)
   142  	}
   143  
   144  	tacticsServer, err := tactics.NewServer(
   145  		nil,
   146  		nil,
   147  		nil,
   148  		tacticsConfigFilename)
   149  	if err != nil {
   150  		t.Fatalf("NewServer failed: %s", err)
   151  	}
   152  
   153  	support := &SupportServices{
   154  		TacticsServer: tacticsServer,
   155  	}
   156  	support.ReplayCache = NewReplayCache(support)
   157  	support.ServerTacticsParametersCache =
   158  		NewServerTacticsParametersCache(support)
   159  
   160  	keySplitTestCases := []struct {
   161  		description                          string
   162  		geoIPData                            GeoIPData
   163  		expectedConnectionWorkerPoolSize     int
   164  		expectedCacheSizeBefore              int
   165  		expectedCacheSizeAfter               int
   166  		expectedParameterReferencesSizeAfter int
   167  	}{
   168  		{
   169  			"add new cache entry, default parameter",
   170  			GeoIPData{Country: "R0", ISP: "I0", City: "C0"},
   171  			1,
   172  			0, 1, 1,
   173  		},
   174  		{
   175  			"region already cached, region-only key",
   176  			GeoIPData{Country: "R0", ISP: "I1", City: "C1"},
   177  			1,
   178  			1, 1, 1,
   179  		},
   180  		{
   181  			"add new cache entry, filtered parameter",
   182  			GeoIPData{Country: "R1", ISP: "I1a", City: "C1a"},
   183  			2,
   184  			1, 2, 2,
   185  		},
   186  		{
   187  			"region already cached, region-only key",
   188  			GeoIPData{Country: "R1", ISP: "I1a", City: "C1a"},
   189  			2,
   190  			2, 2, 2,
   191  		},
   192  		{
   193  			"region already cached, region-only key",
   194  			GeoIPData{Country: "R1", ISP: "I1b", City: "C1b"},
   195  			2,
   196  			2, 2, 2,
   197  		},
   198  		{
   199  			"region already cached, region-only key",
   200  			GeoIPData{Country: "R1", ISP: "I1b", City: "C1c"},
   201  			2,
   202  			2, 2, 2,
   203  		},
   204  		{
   205  			"add new cache entry, filtered parameter, region/ISP key",
   206  			GeoIPData{Country: "R2", ISP: "I2a", City: "C2a"},
   207  			3,
   208  			2, 3, 3,
   209  		},
   210  		{
   211  			"region/ISP already cached",
   212  			GeoIPData{Country: "R2", ISP: "I2a", City: "C2a"},
   213  			3,
   214  			3, 3, 3,
   215  		},
   216  		{
   217  			"region/ISP already cached, city is ignored",
   218  			GeoIPData{Country: "R2", ISP: "I2a", City: "C2b"},
   219  			3,
   220  			3, 3, 3,
   221  		},
   222  		{
   223  			"add new cache entry, filtered parameter, region/ISP key",
   224  			GeoIPData{Country: "R2", ISP: "I2b", City: "C2a"},
   225  			4,
   226  			3, 4, 4,
   227  		},
   228  		{
   229  			"region/ISP already cached, city is ignored",
   230  			GeoIPData{Country: "R2", ISP: "I2b", City: "C2b"},
   231  			4,
   232  			4, 4, 4,
   233  		},
   234  		{
   235  			"add new cache entry, filtered parameter, region/ISP key, duplicate parameters",
   236  			GeoIPData{Country: "R2", ISP: "I2c", City: "C2a"},
   237  			4,
   238  			4, 5, 4,
   239  		},
   240  		{
   241  			"region already cached, region-only key",
   242  			GeoIPData{Country: "R0", ASN: "0", City: "C1"},
   243  			1,
   244  			5, 5, 4,
   245  		},
   246  		{
   247  			"region already cached, region-only key",
   248  			GeoIPData{Country: "R1", ASN: "1", City: "C1a"},
   249  			2,
   250  			5, 5, 4,
   251  		},
   252  		{
   253  			"add new cache entry, filtered parameter, region/ASN key",
   254  			GeoIPData{Country: "R3", ASN: "31", City: "C2a"},
   255  			5,
   256  			5, 6, 5,
   257  		},
   258  		{
   259  			"region/ASN already cached",
   260  			GeoIPData{Country: "R3", ASN: "31", City: "C2a"},
   261  			5,
   262  			6, 6, 5,
   263  		},
   264  		{
   265  			"region/ASN already cached, city is ignored",
   266  			GeoIPData{Country: "R3", ASN: "31", City: "C2b"},
   267  			5,
   268  			6, 6, 5,
   269  		},
   270  		{
   271  			"add new cache entry, filtered parameter, region/ASN key",
   272  			GeoIPData{Country: "R3", ASN: "32", City: "C2a"},
   273  			6,
   274  			6, 7, 6,
   275  		},
   276  		{
   277  			"region/ASN already cached, city is ignored",
   278  			GeoIPData{Country: "R3", ASN: "32", City: "C2b"},
   279  			6,
   280  			7, 7, 6,
   281  		},
   282  		{
   283  			"add new cache entry, filtered parameter, region/ASN key, duplicate parameters",
   284  			GeoIPData{Country: "R3", ASN: "33", City: "C2a"},
   285  			6,
   286  			7, 8, 6,
   287  		},
   288  	}
   289  
   290  	for _, testCase := range keySplitTestCases {
   291  		t.Run(testCase.description, func(t *testing.T) {
   292  
   293  			support.ServerTacticsParametersCache.mutex.Lock()
   294  			cacheSize := support.ServerTacticsParametersCache.tacticsCache.Len()
   295  			support.ServerTacticsParametersCache.mutex.Unlock()
   296  			if cacheSize != testCase.expectedCacheSizeBefore {
   297  				t.Fatalf("unexpected tacticsCache size before lookup: %d", cacheSize)
   298  			}
   299  
   300  			p, err := support.ServerTacticsParametersCache.Get(testCase.geoIPData)
   301  			if err != nil {
   302  				t.Fatalf("ServerTacticsParametersCache.Get failed: %d", err)
   303  			}
   304  
   305  			connectionWorkerPoolSize := p.Int(parameters.ConnectionWorkerPoolSize)
   306  			if connectionWorkerPoolSize != testCase.expectedConnectionWorkerPoolSize {
   307  				t.Fatalf("unexpected ConnectionWorkerPoolSize value: %d", connectionWorkerPoolSize)
   308  			}
   309  
   310  			support.ServerTacticsParametersCache.mutex.Lock()
   311  			cacheSize = support.ServerTacticsParametersCache.tacticsCache.Len()
   312  			support.ServerTacticsParametersCache.mutex.Unlock()
   313  			if cacheSize != testCase.expectedCacheSizeAfter {
   314  				t.Fatalf("unexpected cache size after lookup: %d", cacheSize)
   315  			}
   316  
   317  			support.ServerTacticsParametersCache.mutex.Lock()
   318  			paramRefsSize := len(support.ServerTacticsParametersCache.parameterReferences)
   319  			support.ServerTacticsParametersCache.mutex.Unlock()
   320  			if paramRefsSize != testCase.expectedParameterReferencesSizeAfter {
   321  				t.Fatalf("unexpected parameterReferences size after lookup: %d", paramRefsSize)
   322  			}
   323  
   324  		})
   325  	}
   326  
   327  	metrics := support.ServerTacticsParametersCache.GetMetrics()
   328  	if metrics["server_tactics_max_cache_entries"].(int64) != 8 ||
   329  		metrics["server_tactics_max_parameter_references"].(int64) != 6 ||
   330  		metrics["server_tactics_cache_hit_count"].(int64) != 12 ||
   331  		metrics["server_tactics_cache_miss_count"].(int64) != 8 {
   332  
   333  		t.Fatalf("unexpected metrics: %v", metrics)
   334  	}
   335  
   336  	// Test: force eviction and check parameterReferences cleanup.
   337  
   338  	for i := 0; i < TACTICS_CACHE_MAX_ENTRIES*2; i++ {
   339  		_, err := support.ServerTacticsParametersCache.Get(
   340  			GeoIPData{Country: "R2", ISP: fmt.Sprintf("I-%d", i), City: "C2a"})
   341  		if err != nil {
   342  			t.Fatalf("ServerTacticsParametersCache.Get failed: %d", err)
   343  		}
   344  	}
   345  
   346  	support.ServerTacticsParametersCache.mutex.Lock()
   347  	cacheSize := support.ServerTacticsParametersCache.tacticsCache.Len()
   348  	paramRefsSize := len(support.ServerTacticsParametersCache.parameterReferences)
   349  	support.ServerTacticsParametersCache.mutex.Unlock()
   350  
   351  	if cacheSize != TACTICS_CACHE_MAX_ENTRIES {
   352  		t.Fatalf("unexpected tacticsCache size before lookup: %d", cacheSize)
   353  
   354  	}
   355  
   356  	if paramRefsSize != 1 {
   357  		t.Fatalf("unexpected parameterReferences size after lookup: %d", paramRefsSize)
   358  	}
   359  }