|
|
@@ -58,6 +58,7 @@ import (
|
|
|
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tactics"
|
|
|
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/transforms"
|
|
|
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/values"
|
|
|
+ "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/server/psinet"
|
|
|
lrucache "github.com/cognusion/go-cache-lru"
|
|
|
"github.com/miekg/dns"
|
|
|
"golang.org/x/net/proxy"
|
|
|
@@ -396,6 +397,19 @@ func TestHotReload(t *testing.T) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+func TestHotReloadWithTactics(t *testing.T) {
|
|
|
+ runServer(t,
|
|
|
+ &runServerConfig{
|
|
|
+ tunnelProtocol: "UNFRONTED-MEEK-OSSH",
|
|
|
+ enableSSHAPIRequests: true,
|
|
|
+ doHotReload: true,
|
|
|
+ requireAuthorization: true,
|
|
|
+ doTunneledWebRequest: true,
|
|
|
+ doTunneledNTPRequest: true,
|
|
|
+ doLogHostProvider: true,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
func TestDefaultSponsorID(t *testing.T) {
|
|
|
runServer(t,
|
|
|
&runServerConfig{
|
|
|
@@ -743,6 +757,11 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
|
|
|
// customize server config
|
|
|
|
|
|
+ discoveryServers, err := newDiscoveryServers([]string{"1.1.1.1", "2.2.2.2"})
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("newDiscoveryServers failed: %s\n", err)
|
|
|
+ }
|
|
|
+
|
|
|
// Initialize prune server entry test cases and associated data to pave into psinet.
|
|
|
pruneServerEntryTestCases, psinetValidServerEntryTags, expectedNumPruneNotices :=
|
|
|
initializePruneServerEntriesTest(t, runConfig)
|
|
|
@@ -750,7 +769,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
// Pave psinet with random values to test handshake homepages.
|
|
|
psinetFilename := filepath.Join(testDataDirName, "psinet.json")
|
|
|
sponsorID, expectedHomepageURL := pavePsinetDatabaseFile(
|
|
|
- t, psinetFilename, "", runConfig.doDefaultSponsorID, true, psinetValidServerEntryTags)
|
|
|
+ t, psinetFilename, "", runConfig.doDefaultSponsorID, true, psinetValidServerEntryTags, discoveryServers)
|
|
|
|
|
|
// Pave OSL config for SLOK testing
|
|
|
oslConfigFilename := filepath.Join(testDataDirName, "osl_config.json")
|
|
|
@@ -771,15 +790,17 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
livenessTestSize)
|
|
|
|
|
|
var tacticsConfigFilename string
|
|
|
+ var tacticsTunnelProtocol string
|
|
|
|
|
|
// Only pave the tactics config when tactics are required. This exercises the
|
|
|
// case where the tactics config is omitted.
|
|
|
if doServerTactics {
|
|
|
tacticsConfigFilename = filepath.Join(testDataDirName, "tactics_config.json")
|
|
|
|
|
|
- tacticsTunnelProtocol := runConfig.tunnelProtocol
|
|
|
if runConfig.clientTunnelProtocol != "" {
|
|
|
tacticsTunnelProtocol = runConfig.clientTunnelProtocol
|
|
|
+ } else {
|
|
|
+ tacticsTunnelProtocol = runConfig.tunnelProtocol
|
|
|
}
|
|
|
|
|
|
paveTacticsConfigFile(
|
|
|
@@ -795,6 +816,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
runConfig.doDestinationBytes,
|
|
|
runConfig.applyPrefix,
|
|
|
runConfig.forceFragmenting,
|
|
|
+ "classic",
|
|
|
)
|
|
|
}
|
|
|
|
|
|
@@ -864,6 +886,11 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
uniqueUserLog := make(chan map[string]interface{}, 1)
|
|
|
domainBytesLog := make(chan map[string]interface{}, 1)
|
|
|
serverTunnelLog := make(chan map[string]interface{}, 1)
|
|
|
+ // Max 3 discovery logs:
|
|
|
+ // 1. server startup
|
|
|
+ // 2. hot reload of psinet db (runConfig.doHotReload)
|
|
|
+ // 3. hot reload of server tactics (runConfig.doHotReload && doServerTactics)
|
|
|
+ discoveryLog := make(chan map[string]interface{}, 3)
|
|
|
|
|
|
setLogCallback(func(log []byte) {
|
|
|
|
|
|
@@ -875,6 +902,12 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
}
|
|
|
|
|
|
if logFields["event_name"] == nil {
|
|
|
+ if logFields["discovery_strategy"] != nil {
|
|
|
+ select {
|
|
|
+ case discoveryLog <- logFields:
|
|
|
+ default:
|
|
|
+ }
|
|
|
+ }
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -969,9 +1002,16 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
|
|
|
if runConfig.doHotReload {
|
|
|
|
|
|
+ // Change discovery servers. Tests that discovery switches over to
|
|
|
+ // these new servers.
|
|
|
+ discoveryServers, err = newDiscoveryServers([]string{"3.3.3.3"})
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("newDiscoveryServers failed: %s\n", err)
|
|
|
+ }
|
|
|
+
|
|
|
// Pave new config files with different random values.
|
|
|
sponsorID, expectedHomepageURL = pavePsinetDatabaseFile(
|
|
|
- t, psinetFilename, "", runConfig.doDefaultSponsorID, true, psinetValidServerEntryTags)
|
|
|
+ t, psinetFilename, "", runConfig.doDefaultSponsorID, true, psinetValidServerEntryTags, discoveryServers)
|
|
|
|
|
|
propagationChannelID = paveOSLConfigFile(t, oslConfigFilename)
|
|
|
|
|
|
@@ -985,6 +1025,26 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
runConfig.denyTrafficRules,
|
|
|
livenessTestSize)
|
|
|
|
|
|
+ if doServerTactics {
|
|
|
+ // Pave new tactics file with different discovery strategy. Tests
|
|
|
+ // that discovery switches over to the new strategy.
|
|
|
+ paveTacticsConfigFile(
|
|
|
+ t,
|
|
|
+ tacticsConfigFilename,
|
|
|
+ tacticsRequestPublicKey,
|
|
|
+ tacticsRequestPrivateKey,
|
|
|
+ tacticsRequestObfuscatedKey,
|
|
|
+ tacticsTunnelProtocol,
|
|
|
+ propagationChannelID,
|
|
|
+ livenessTestSize,
|
|
|
+ runConfig.doBurstMonitor,
|
|
|
+ runConfig.doDestinationBytes,
|
|
|
+ runConfig.applyPrefix,
|
|
|
+ runConfig.forceFragmenting,
|
|
|
+ "consistent",
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
p, _ := os.FindProcess(os.Getpid())
|
|
|
p.Signal(syscall.SIGUSR1)
|
|
|
|
|
|
@@ -1356,12 +1416,7 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
// random homepage URLs will change, but this has no effect on the
|
|
|
// already connected client.
|
|
|
_, _ = pavePsinetDatabaseFile(
|
|
|
- t, psinetFilename, sponsorID, runConfig.doDefaultSponsorID, false, psinetValidServerEntryTags)
|
|
|
-
|
|
|
- tacticsTunnelProtocol := runConfig.tunnelProtocol
|
|
|
- if runConfig.clientTunnelProtocol != "" {
|
|
|
- tacticsTunnelProtocol = runConfig.clientTunnelProtocol
|
|
|
- }
|
|
|
+ t, psinetFilename, sponsorID, runConfig.doDefaultSponsorID, false, psinetValidServerEntryTags, discoveryServers)
|
|
|
|
|
|
// Pave tactics without destination bytes.
|
|
|
paveTacticsConfigFile(
|
|
|
@@ -1375,7 +1430,8 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
livenessTestSize,
|
|
|
runConfig.doBurstMonitor,
|
|
|
false,
|
|
|
- false, false)
|
|
|
+ false, false,
|
|
|
+ "consistent")
|
|
|
|
|
|
p, _ := os.FindProcess(os.Getpid())
|
|
|
p.Signal(syscall.SIGUSR1)
|
|
|
@@ -1584,6 +1640,41 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Check logs emitted by discovery.
|
|
|
+
|
|
|
+ var expectedDiscoveryStrategy []string
|
|
|
+
|
|
|
+ // Discovery emits 1 log on startup.
|
|
|
+ if doServerTactics {
|
|
|
+ expectedDiscoveryStrategy = append(expectedDiscoveryStrategy, "classic")
|
|
|
+ } else {
|
|
|
+ expectedDiscoveryStrategy = append(expectedDiscoveryStrategy, "consistent")
|
|
|
+ }
|
|
|
+ if runConfig.doHotReload {
|
|
|
+ if doServerTactics {
|
|
|
+ // Discovery emits 1 log when tactics are reloaded, which happens
|
|
|
+ // before the psinet database is reloaded.
|
|
|
+ expectedDiscoveryStrategy = append(expectedDiscoveryStrategy, "classic")
|
|
|
+ }
|
|
|
+ // Discovery emits 1 when the psinet database is reloaded.
|
|
|
+ expectedDiscoveryStrategy = append(expectedDiscoveryStrategy, "consistent")
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, expectedStrategy := range expectedDiscoveryStrategy {
|
|
|
+ select {
|
|
|
+ case logFields := <-discoveryLog:
|
|
|
+ if strategy, ok := logFields["discovery_strategy"].(string); ok {
|
|
|
+ if strategy != expectedStrategy {
|
|
|
+ t.Fatalf("expected discovery strategy \"%s\"", expectedStrategy)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ t.Fatalf("missing discovery_strategy field")
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ t.Fatalf("missing discovery log")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// Check that datastore had retained/pruned server entries as expected.
|
|
|
checkPruneServerEntriesTest(t, runConfig, testDataDirName, pruneServerEntryTestCases)
|
|
|
|
|
|
@@ -1670,6 +1761,49 @@ func runServer(t *testing.T, runConfig *runServerConfig) {
|
|
|
t.Fatalf("unexpected cached steering IP: %v", entry)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // Check that the client discovered one of the discovery servers.
|
|
|
+
|
|
|
+ discoveredServers := make(map[string]*protocol.ServerEntry)
|
|
|
+
|
|
|
+ // Otherwise NewServerEntryIterator only returns TargetServerEntry.
|
|
|
+ clientConfig.TargetServerEntry = ""
|
|
|
+
|
|
|
+ _, iterator, err := psiphon.NewServerEntryIterator(clientConfig)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("NewServerEntryIterator failed: %s", err)
|
|
|
+ }
|
|
|
+ defer iterator.Close()
|
|
|
+
|
|
|
+ for {
|
|
|
+ serverEntry, err := iterator.Next()
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("ServerIterator.Next failed: %s", err)
|
|
|
+ }
|
|
|
+ if serverEntry == nil {
|
|
|
+ break
|
|
|
+ }
|
|
|
+ discoveredServers[serverEntry.IpAddress] = serverEntry
|
|
|
+ }
|
|
|
+
|
|
|
+ foundOne := false
|
|
|
+ for _, server := range discoveryServers {
|
|
|
+
|
|
|
+ serverEntry, err := protocol.DecodeServerEntry(server.EncodedServerEntry, "", "")
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("protocol.DecodeServerEntry failed: %s", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if v, ok := discoveredServers[serverEntry.IpAddress]; ok {
|
|
|
+ if v.Tag == serverEntry.Tag {
|
|
|
+ foundOne = true
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if !foundOne {
|
|
|
+ t.Fatalf("expected client to discover at least one server")
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
func sendNotificationReceived(c chan<- struct{}) {
|
|
|
@@ -2549,7 +2683,8 @@ func pavePsinetDatabaseFile(
|
|
|
sponsorID string,
|
|
|
useDefaultSponsorID bool,
|
|
|
doDomainBytes bool,
|
|
|
- validServerEntryTags []string) (string, string) {
|
|
|
+ validServerEntryTags []string,
|
|
|
+ discoveryServers []*psinet.DiscoveryServer) (string, string) {
|
|
|
|
|
|
if sponsorID == "" {
|
|
|
sponsorID = prng.HexString(8)
|
|
|
@@ -2564,6 +2699,11 @@ func pavePsinetDatabaseFile(
|
|
|
fakePath := prng.HexString(4)
|
|
|
expectedHomepageURL := fmt.Sprintf("https://%s.com/%s", fakeDomain, fakePath)
|
|
|
|
|
|
+ discoverServersJSON, err := json.Marshal(discoveryServers)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatalf("json.Marshal failed: %s\n", err)
|
|
|
+ }
|
|
|
+
|
|
|
psinetJSONFormat := `
|
|
|
{
|
|
|
"default_sponsor_id" : "%s",
|
|
|
@@ -2585,7 +2725,8 @@ func pavePsinetDatabaseFile(
|
|
|
},
|
|
|
"valid_server_entry_tags" : {
|
|
|
%s
|
|
|
- }
|
|
|
+ },
|
|
|
+ "discovery_servers" : %s
|
|
|
}
|
|
|
`
|
|
|
|
|
|
@@ -2619,9 +2760,10 @@ func pavePsinetDatabaseFile(
|
|
|
expectedHomepageURL,
|
|
|
protocol.PSIPHON_API_ALERT_DISALLOWED_TRAFFIC,
|
|
|
actionURLsJSON,
|
|
|
- validServerEntryTagsJSON)
|
|
|
+ validServerEntryTagsJSON,
|
|
|
+ discoverServersJSON)
|
|
|
|
|
|
- err := ioutil.WriteFile(psinetFilename, []byte(psinetJSON), 0600)
|
|
|
+ err = ioutil.WriteFile(psinetFilename, []byte(psinetJSON), 0600)
|
|
|
if err != nil {
|
|
|
t.Fatalf("error paving psinet database file: %s", err)
|
|
|
}
|
|
|
@@ -2827,7 +2969,8 @@ func paveTacticsConfigFile(
|
|
|
doBurstMonitor bool,
|
|
|
doDestinationBytes bool,
|
|
|
applyOsshPrefix bool,
|
|
|
- enableOsshPrefixFragmenting bool) {
|
|
|
+ enableOsshPrefixFragmenting bool,
|
|
|
+ discoveryStategy string) {
|
|
|
|
|
|
// Setting LimitTunnelProtocols passively exercises the
|
|
|
// server-side LimitTunnelProtocols enforcement.
|
|
|
@@ -2877,7 +3020,8 @@ func paveTacticsConfigFile(
|
|
|
"BPFClientTCPProbability" : 1.0,
|
|
|
"ServerPacketManipulationSpecs" : [{"Name": "test-packetman-spec", "PacketSpecs": [["TCP-flags S"]]}],
|
|
|
"ServerPacketManipulationProbability" : 1.0,
|
|
|
- "ServerProtocolPacketManipulations": {"All" : ["test-packetman-spec"]}
|
|
|
+ "ServerProtocolPacketManipulations": {"All" : ["test-packetman-spec"]},
|
|
|
+ "ServerDiscoveryStrategy": "%s"
|
|
|
}
|
|
|
},
|
|
|
"FilteredTactics" : [
|
|
|
@@ -2953,6 +3097,7 @@ func paveTacticsConfigFile(
|
|
|
tunnelProtocol,
|
|
|
tunnelProtocol,
|
|
|
livenessTestSize, livenessTestSize, livenessTestSize, livenessTestSize,
|
|
|
+ discoveryStategy,
|
|
|
propagationChannelID,
|
|
|
strings.ReplaceAll(testCustomHostNameRegex, `\`, `\\`),
|
|
|
tunnelProtocol)
|
|
|
@@ -3481,3 +3626,30 @@ func (f *flows) Write(p []byte) (n int, err error) {
|
|
|
|
|
|
return n, err
|
|
|
}
|
|
|
+
|
|
|
+// newDiscoveryServers returns len(ipAddresses) discovery servers with the
|
|
|
+// given IP addresses and randomly generated tags.
|
|
|
+func newDiscoveryServers(ipAddresses []string) ([]*psinet.DiscoveryServer, error) {
|
|
|
+
|
|
|
+ servers := make([]*psinet.DiscoveryServer, len(ipAddresses))
|
|
|
+
|
|
|
+ for i, ipAddress := range ipAddresses {
|
|
|
+
|
|
|
+ encodedServer, err := protocol.EncodeServerEntry(&protocol.ServerEntry{
|
|
|
+ IpAddress: ipAddress,
|
|
|
+ Tag: prng.HexString(16),
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ return nil, errors.Trace(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ servers[i] = &psinet.DiscoveryServer{
|
|
|
+ DiscoveryDateRange: []time.Time{
|
|
|
+ time.Now().Add(-time.Hour).UTC(),
|
|
|
+ time.Now().Add(time.Hour).UTC(),
|
|
|
+ },
|
|
|
+ EncodedServerEntry: encodedServer,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return servers, nil
|
|
|
+}
|