github.com/Psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/exchange_test.go (about)

     1  /*
     2   * Copyright (c) 2019, 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 psiphon
    21  
    22  import (
    23  	"encoding/base64"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"testing"
    28  
    29  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    30  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng"
    31  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
    32  )
    33  
    34  func TestServerEntryExchange(t *testing.T) {
    35  
    36  	// Prepare an empty database
    37  
    38  	testDataDirName, err := ioutil.TempDir("", "psiphon-exchange-test")
    39  	if err != nil {
    40  		t.Fatalf("TempDir failed: %s", err)
    41  	}
    42  	defer os.RemoveAll(testDataDirName)
    43  
    44  	SetNoticeWriter(ioutil.Discard)
    45  
    46  	// Generate signing and exchange key material
    47  
    48  	obfuscationKeyBytes, err := common.MakeSecureRandomBytes(32)
    49  	if err != nil {
    50  		t.Fatalf("MakeRandomBytes failed: %s", err)
    51  	}
    52  
    53  	obfuscationKey := base64.StdEncoding.EncodeToString(obfuscationKeyBytes)
    54  
    55  	publicKey, privateKey, err := protocol.NewServerEntrySignatureKeyPair()
    56  	if err != nil {
    57  		t.Fatalf("NewServerEntrySignatureKeyPair failed: %s", err)
    58  	}
    59  
    60  	// Initialize config required for datastore operation
    61  
    62  	networkID := prng.HexString(8)
    63  
    64  	configJSONTemplate := `
    65  		    {
    66                  "SponsorId" : "0",
    67                  "PropagationChannelId" : "0",
    68  		        "ServerEntrySignaturePublicKey" : "%s",
    69  		        "ExchangeObfuscationKey" : "%s",
    70  		        "NetworkID" : "%s"
    71  		    }`
    72  
    73  	configJSON := fmt.Sprintf(
    74  		configJSONTemplate,
    75  		publicKey,
    76  		obfuscationKey,
    77  		networkID)
    78  
    79  	config, err := LoadConfig([]byte(configJSON))
    80  	if err != nil {
    81  		t.Fatalf("LoadConfig failed: %s", err)
    82  	}
    83  
    84  	config.DataRootDirectory = testDataDirName
    85  
    86  	err = config.Commit(false)
    87  	if err != nil {
    88  		t.Fatalf("Commit failed: %s", err)
    89  	}
    90  
    91  	resolver := NewResolver(config, true)
    92  	defer resolver.Stop()
    93  	config.SetResolver(resolver)
    94  
    95  	err = OpenDataStore(config)
    96  	if err != nil {
    97  		t.Fatalf("OpenDataStore failed: %s", err)
    98  	}
    99  	defer CloseDataStore()
   100  
   101  	// Generate server entries to test different cases
   102  	//
   103  	// Note: invalid signature cases are exercised in
   104  	// protocol.TestServerEntryListSignatures
   105  
   106  	makeServerEntryFields := func(IPAddress string) protocol.ServerEntryFields {
   107  		n := 16
   108  		fields := make(protocol.ServerEntryFields)
   109  		fields["ipAddress"] = IPAddress
   110  		fields["sshPort"] = 22
   111  		fields["sshUsername"] = prng.HexString(n)
   112  		fields["sshPassword"] = prng.HexString(n)
   113  		fields["sshHostKey"] = prng.HexString(n)
   114  		fields["sshObfuscatedPort"] = 23
   115  		fields["sshObfuscatedQUICPort"] = 24
   116  		fields["sshObfuscatedKey"] = prng.HexString(n)
   117  		fields["capabilities"] = []string{"SSH", "OSSH", "QUIC", "ssh-api-requests"}
   118  		fields["region"] = "US"
   119  		fields["configurationVersion"] = 1
   120  		return fields
   121  	}
   122  
   123  	serverEntry0 := makeServerEntryFields("192.168.1.1")
   124  	tunnelProtocol0 := "SSH"
   125  
   126  	serverEntry1 := makeServerEntryFields("192.168.1.2")
   127  	err = serverEntry1.AddSignature(publicKey, privateKey)
   128  	if err != nil {
   129  		t.Fatalf("AddSignature failed: %s", err)
   130  	}
   131  	tunnelProtocol1 := "OSSH"
   132  
   133  	serverEntry2 := makeServerEntryFields("192.168.1.3")
   134  	err = serverEntry2.AddSignature(publicKey, privateKey)
   135  	if err != nil {
   136  		t.Fatalf("AddSignature failed: %s", err)
   137  	}
   138  	tunnelProtocol2 := "QUIC-OSSH"
   139  
   140  	serverEntry3 := makeServerEntryFields("192.168.1.4")
   141  	err = serverEntry3.AddSignature(publicKey, privateKey)
   142  	if err != nil {
   143  		t.Fatalf("AddSignature failed: %s", err)
   144  	}
   145  	tunnelProtocol3 := ""
   146  
   147  	// paveServerEntry stores a server entry in the datastore with source
   148  	// EMBEDDED, promotes the server entry to the affinity/export candidate
   149  	// position, and generates and stores associated dial parameters when
   150  	// specified. This creates potential candidates for export.
   151  	//
   152  	// When tunnelProtocol is "", no dial parameters are created.
   153  
   154  	paveServerEntry := func(
   155  		fields protocol.ServerEntryFields, tunnelProtocol string) {
   156  
   157  		fields.SetLocalSource(protocol.SERVER_ENTRY_SOURCE_EMBEDDED)
   158  		fields.SetLocalTimestamp(
   159  			common.TruncateTimestampToHour(common.GetCurrentTimestamp()))
   160  
   161  		err = StoreServerEntry(fields, true)
   162  		if err != nil {
   163  			t.Fatalf("StoreServerEntry failed: %s", err)
   164  		}
   165  
   166  		err = PromoteServerEntry(config, fields["ipAddress"].(string))
   167  		if err != nil {
   168  			t.Fatalf("PromoteServerEntry failed: %s", err)
   169  		}
   170  
   171  		if tunnelProtocol != "" {
   172  
   173  			serverEntry, err := fields.GetServerEntry()
   174  			if err != nil {
   175  				t.Fatalf("ServerEntryFields.GetServerEntry failed: %s", err)
   176  			}
   177  
   178  			canReplay := func(serverEntry *protocol.ServerEntry, replayProtocol string) bool {
   179  				return true
   180  			}
   181  
   182  			selectProtocol := func(serverEntry *protocol.ServerEntry) (string, bool) {
   183  				return tunnelProtocol, true
   184  			}
   185  
   186  			dialParams, err := MakeDialParameters(
   187  				config,
   188  				nil,
   189  				canReplay,
   190  				selectProtocol,
   191  				serverEntry,
   192  				false,
   193  				0,
   194  				0)
   195  			if err != nil {
   196  				t.Fatalf("MakeDialParameters failed: %s", err)
   197  			}
   198  
   199  			err = SetDialParameters(serverEntry.IpAddress, networkID, dialParams)
   200  			if err != nil {
   201  				t.Fatalf("SetDialParameters failed: %s", err)
   202  			}
   203  		}
   204  	}
   205  
   206  	// checkFirstServerEntry checks that the current affinity server entry has
   207  	// the expected ID (IP address), and that any associated, stored dial
   208  	// parameters are in the expected exchanged state. This is used to verify
   209  	// that an import has succeed and set the datastore correctly.
   210  
   211  	checkFirstServerEntry := func(
   212  		fields protocol.ServerEntryFields, tunnelProtocol string, isExchanged bool) {
   213  
   214  		_, iterator, err := NewServerEntryIterator(config)
   215  		if err != nil {
   216  			t.Fatalf("NewServerEntryIterator failed: %s", err)
   217  		}
   218  		defer iterator.Close()
   219  
   220  		serverEntry, err := iterator.Next()
   221  		if err != nil {
   222  			t.Fatalf("ServerEntryIterator.Next failed: %s", err)
   223  		}
   224  		if serverEntry == nil {
   225  			t.Fatalf("unexpected nil server entry")
   226  		}
   227  
   228  		if serverEntry.IpAddress != fields["ipAddress"] {
   229  			t.Fatalf("unexpected server entry IP address")
   230  		}
   231  
   232  		if isExchanged {
   233  			if serverEntry.LocalSource != protocol.SERVER_ENTRY_SOURCE_EXCHANGED {
   234  				t.Fatalf("unexpected non-exchanged server entry source")
   235  			}
   236  		} else {
   237  			if serverEntry.LocalSource == protocol.SERVER_ENTRY_SOURCE_EXCHANGED {
   238  				t.Fatalf("unexpected exchanged server entry source")
   239  			}
   240  		}
   241  
   242  		dialParams, err := GetDialParameters(config, serverEntry.IpAddress, networkID)
   243  		if err != nil {
   244  			t.Fatalf("GetDialParameters failed: %s", err)
   245  		}
   246  
   247  		if tunnelProtocol == "" {
   248  			if dialParams != nil {
   249  				t.Fatalf("unexpected non-nil dial parameters")
   250  			}
   251  		} else if isExchanged {
   252  			if !dialParams.IsExchanged {
   253  				t.Fatalf("unexpected non-exchanged dial parameters")
   254  			}
   255  			if dialParams.TunnelProtocol != tunnelProtocol {
   256  				t.Fatalf("unexpected exchanged dial parameters tunnel protocol")
   257  			}
   258  		} else {
   259  			if dialParams.IsExchanged {
   260  				t.Fatalf("unexpected exchanged dial parameters")
   261  			}
   262  			if dialParams.TunnelProtocol != tunnelProtocol {
   263  				t.Fatalf("unexpected dial parameters tunnel protocol")
   264  			}
   265  		}
   266  	}
   267  
   268  	// Test: pave only an unsigned server entry; export should fail
   269  
   270  	paveServerEntry(serverEntry0, tunnelProtocol0)
   271  
   272  	payload := ExportExchangePayload(config)
   273  	if payload != "" {
   274  		t.Fatalf("ExportExchangePayload unexpectedly succeeded")
   275  	}
   276  
   277  	// Test: pave two signed server entries; serverEntry2 is the affinity server
   278  	// entry and should be the exported server entry
   279  
   280  	paveServerEntry(serverEntry1, tunnelProtocol1)
   281  	paveServerEntry(serverEntry2, tunnelProtocol2)
   282  
   283  	payload = ExportExchangePayload(config)
   284  	if payload == "" {
   285  		t.Fatalf("ExportExchangePayload failed")
   286  	}
   287  
   288  	// Test: import; serverEntry2 should be imported
   289  
   290  	// Before importing the exported payload, move serverEntry1 to the affinity
   291  	// position. After the import, we expect serverEntry2 to be at the affinity
   292  	// position and its dial parameters to be IsExchanged and and have the
   293  	// exchanged tunnel protocol.
   294  
   295  	err = PromoteServerEntry(config, serverEntry1["ipAddress"].(string))
   296  	if err != nil {
   297  		t.Fatalf("PromoteServerEntry failed: %s", err)
   298  	}
   299  
   300  	checkFirstServerEntry(serverEntry1, tunnelProtocol1, false)
   301  
   302  	if !ImportExchangePayload(config, payload) {
   303  		t.Fatalf("ImportExchangePayload failed")
   304  	}
   305  
   306  	checkFirstServerEntry(serverEntry2, tunnelProtocol2, true)
   307  
   308  	// Test: nil exchanged dial parameters case
   309  
   310  	paveServerEntry(serverEntry3, tunnelProtocol3)
   311  
   312  	payload = ExportExchangePayload(config)
   313  	if payload == "" {
   314  		t.Fatalf("ExportExchangePayload failed")
   315  	}
   316  
   317  	err = PromoteServerEntry(config, serverEntry1["ipAddress"].(string))
   318  	if err != nil {
   319  		t.Fatalf("PromoteServerEntry failed: %s", err)
   320  	}
   321  
   322  	checkFirstServerEntry(serverEntry1, tunnelProtocol1, false)
   323  
   324  	if !ImportExchangePayload(config, payload) {
   325  		t.Fatalf("ImportExchangePayload failed")
   326  	}
   327  
   328  	checkFirstServerEntry(serverEntry3, tunnelProtocol3, true)
   329  }