| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521 |
- /*
- * Copyright (c) 2018, Psiphon Inc.
- * All rights reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
- package clientlib
- import (
- "context"
- "encoding/json"
- "errors"
- "os"
- "strings"
- "testing"
- "time"
- "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
- )
- func setupConfig(t *testing.T, disableFetcher bool) []byte {
- configJSON, err := os.ReadFile("../../psiphon/controller_test.config")
- if err != nil {
- // What to do if config file is not present?
- t.Skipf("error loading configuration file: %s", err)
- }
- var config map[string]interface{}
- err = json.Unmarshal(configJSON, &config)
- if err != nil {
- t.Fatalf("json.Unmarshal failed: %v", err)
- }
- if disableFetcher {
- config["DisableRemoteServerListFetcher"] = true
- }
- configJSON, err = json.Marshal(config)
- if err != nil {
- t.Fatalf("json.Marshal failed: %v", err)
- }
- return configJSON
- }
- func TestStartTunnel(t *testing.T) {
- // TODO: More comprehensive tests. This is only a smoke test.
- configJSON := setupConfig(t, false)
- configJSONNoFetcher := setupConfig(t, true)
- clientPlatform := "clientlib_test.go"
- networkID := "UNKNOWN"
- timeout := 60
- quickTimeout := 1
- trueVal := true
- // Initialize a fresh datastore and create a modified config which cannot
- // connect without known servers, to be used in timeout cases.
- testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
- if err != nil {
- t.Fatalf("ioutil.TempDir failed: %v", err)
- }
- defer os.RemoveAll(testDataDirName)
- paramsDeltaErr := func(err error) bool {
- return strings.Contains(err.Error(), "SetParameters failed for delta")
- }
- timeoutErr := func(err error) bool {
- return errors.Is(err, ErrTimeout)
- }
- type args struct {
- ctxTimeout time.Duration
- configJSON []byte
- embeddedServerEntryList string
- params Parameters
- paramsDelta ParametersDelta
- noticeReceiver func(NoticeEvent)
- }
- tests := []struct {
- name string
- args args
- wantTunnel bool
- expectedErr func(error) bool
- }{
- {
- name: "Failure: context timeout",
- args: args{
- ctxTimeout: 10 * time.Millisecond,
- configJSON: configJSONNoFetcher,
- embeddedServerEntryList: "",
- params: Parameters{
- DataRootDirectory: &testDataDirName,
- ClientPlatform: &clientPlatform,
- NetworkID: &networkID,
- EstablishTunnelTimeoutSeconds: &timeout,
- },
- paramsDelta: nil,
- noticeReceiver: nil,
- },
- wantTunnel: false,
- expectedErr: timeoutErr,
- },
- {
- name: "Failure: config timeout",
- args: args{
- ctxTimeout: 0,
- configJSON: configJSONNoFetcher,
- embeddedServerEntryList: "",
- params: Parameters{
- DataRootDirectory: &testDataDirName,
- ClientPlatform: &clientPlatform,
- NetworkID: &networkID,
- EstablishTunnelTimeoutSeconds: &quickTimeout,
- },
- paramsDelta: nil,
- noticeReceiver: nil,
- },
- wantTunnel: false,
- expectedErr: timeoutErr,
- },
- {
- name: "Success: simple",
- args: args{
- ctxTimeout: 0,
- configJSON: configJSON,
- embeddedServerEntryList: "",
- params: Parameters{
- DataRootDirectory: &testDataDirName,
- ClientPlatform: &clientPlatform,
- NetworkID: &networkID,
- EstablishTunnelTimeoutSeconds: &timeout,
- },
- paramsDelta: nil,
- noticeReceiver: nil,
- },
- wantTunnel: true,
- expectedErr: nil,
- },
- {
- name: "Success: disable SOCKS proxy",
- args: args{
- ctxTimeout: 0,
- configJSON: configJSON,
- embeddedServerEntryList: "",
- params: Parameters{
- DataRootDirectory: &testDataDirName,
- ClientPlatform: &clientPlatform,
- NetworkID: &networkID,
- EstablishTunnelTimeoutSeconds: &timeout,
- DisableLocalSocksProxy: &trueVal,
- },
- paramsDelta: nil,
- noticeReceiver: nil,
- },
- wantTunnel: true,
- expectedErr: nil,
- },
- {
- name: "Success: disable HTTP proxy",
- args: args{
- ctxTimeout: 0,
- configJSON: configJSON,
- embeddedServerEntryList: "",
- params: Parameters{
- DataRootDirectory: &testDataDirName,
- ClientPlatform: &clientPlatform,
- NetworkID: &networkID,
- EstablishTunnelTimeoutSeconds: &timeout,
- DisableLocalHTTPProxy: &trueVal,
- },
- paramsDelta: nil,
- noticeReceiver: nil,
- },
- wantTunnel: true,
- expectedErr: nil,
- },
- {
- name: "Success: disable SOCKS and HTTP proxies",
- args: args{
- ctxTimeout: 0,
- configJSON: configJSON,
- embeddedServerEntryList: "",
- params: Parameters{
- DataRootDirectory: &testDataDirName,
- ClientPlatform: &clientPlatform,
- NetworkID: &networkID,
- EstablishTunnelTimeoutSeconds: &timeout,
- DisableLocalSocksProxy: &trueVal,
- DisableLocalHTTPProxy: &trueVal,
- },
- paramsDelta: nil,
- noticeReceiver: nil,
- },
- wantTunnel: true,
- expectedErr: nil,
- },
- {
- name: "Success: good ParametersDelta",
- args: args{
- ctxTimeout: 0,
- configJSON: configJSON,
- embeddedServerEntryList: "",
- params: Parameters{
- DataRootDirectory: &testDataDirName,
- ClientPlatform: &clientPlatform,
- NetworkID: &networkID,
- EstablishTunnelTimeoutSeconds: &timeout,
- },
- paramsDelta: ParametersDelta{"NetworkLatencyMultiplierMin": 1},
- noticeReceiver: nil,
- },
- wantTunnel: true,
- expectedErr: nil,
- },
- {
- name: "Failure: bad ParametersDelta",
- args: args{
- ctxTimeout: 0,
- configJSON: configJSON,
- embeddedServerEntryList: "",
- params: Parameters{
- DataRootDirectory: &testDataDirName,
- ClientPlatform: &clientPlatform,
- NetworkID: &networkID,
- EstablishTunnelTimeoutSeconds: &timeout,
- },
- paramsDelta: ParametersDelta{"invalidParam": 1},
- noticeReceiver: nil,
- },
- wantTunnel: false,
- expectedErr: paramsDeltaErr,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- ctx := context.Background()
- var cancelFunc context.CancelFunc
- if tt.args.ctxTimeout > 0 {
- ctx, cancelFunc = context.WithTimeout(ctx, tt.args.ctxTimeout)
- }
- tunnel, err := StartTunnel(
- ctx,
- tt.args.configJSON,
- tt.args.embeddedServerEntryList,
- tt.args.params,
- tt.args.paramsDelta,
- tt.args.noticeReceiver)
- gotTunnel := (tunnel != nil)
- if cancelFunc != nil {
- cancelFunc()
- }
- if tunnel != nil {
- tunnel.Stop()
- }
- if gotTunnel != tt.wantTunnel {
- t.Errorf("StartTunnel() gotTunnel = %v, wantTunnel %v", err, tt.wantTunnel)
- }
- if tt.expectedErr == nil {
- if err != nil {
- t.Fatalf("StartTunnel() returned unexpected error: %v", err)
- }
- } else if !tt.expectedErr(err) {
- t.Fatalf("StartTunnel() error: %v", err)
- return
- }
- if err != nil {
- return
- }
- if tunnel == nil {
- return
- }
- if tt.args.params.DisableLocalSocksProxy != nil && *tt.args.params.DisableLocalSocksProxy {
- if tunnel.SOCKSProxyPort != 0 {
- t.Fatalf("should not have started SOCKS proxy")
- }
- } else {
- if tunnel.SOCKSProxyPort == 0 {
- t.Fatalf("failed to start SOCKS proxy")
- }
- }
- if tt.args.params.DisableLocalHTTPProxy != nil && *tt.args.params.DisableLocalHTTPProxy {
- if tunnel.HTTPProxyPort != 0 {
- t.Fatalf("should not have started HTTP proxy")
- }
- } else {
- if tunnel.HTTPProxyPort == 0 {
- t.Fatalf("failed to start HTTP proxy")
- }
- }
- })
- }
- }
- func TestMultipleStartTunnel(t *testing.T) {
- configJSON := setupConfig(t, false)
- testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
- if err != nil {
- t.Fatalf("ioutil.TempDir failed: %v", err)
- }
- defer os.RemoveAll(testDataDirName)
- ctx := context.Background()
- tunnel1, err := StartTunnel(
- ctx,
- configJSON,
- "",
- Parameters{DataRootDirectory: &testDataDirName},
- nil,
- nil)
- if err != nil {
- t.Fatalf("first StartTunnel() error = %v", err)
- }
- // We have not stopped the tunnel, so a second StartTunnel() should fail
- _, err = StartTunnel(
- ctx,
- configJSON,
- "",
- Parameters{DataRootDirectory: &testDataDirName},
- nil,
- nil)
- if err != errMultipleStart {
- t.Fatalf("second StartTunnel() should have failed with errMultipleStart; got %v", err)
- }
- // Stop the tunnel and try again
- tunnel1.Stop()
- tunnel3, err := StartTunnel(
- ctx,
- configJSON,
- "",
- Parameters{DataRootDirectory: &testDataDirName},
- nil,
- nil)
- if err != nil {
- t.Fatalf("third StartTunnel() error = %v", err)
- }
- // Stop the tunnel so it doesn't interfere with other tests
- tunnel3.Stop()
- }
- func TestPsiphonTunnel_Dial(t *testing.T) {
- configJSON := setupConfig(t, false)
- trueVal := true
- testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
- if err != nil {
- t.Fatalf("ioutil.TempDir failed: %v", err)
- }
- defer os.RemoveAll(testDataDirName)
- type args struct {
- remoteAddr string
- }
- tests := []struct {
- name string
- args args
- wantErr bool
- tunnelStopped bool
- }{
- {
- name: "Success: example.com",
- args: args{remoteAddr: "example.com:443"},
- wantErr: false,
- },
- {
- name: "Failure: invalid address",
- args: args{remoteAddr: "example.com:99999"},
- wantErr: true,
- },
- {
- name: "Failure: tunnel not started",
- args: args{remoteAddr: "example.com:443"},
- wantErr: true,
- tunnelStopped: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- tunnel, err := StartTunnel(
- context.Background(),
- configJSON,
- "",
- Parameters{
- DataRootDirectory: &testDataDirName,
- // Don't need local proxies for dial tests
- // (and this is likely the configuration that will be used by consumers of the library who utilitize Dial).
- DisableLocalSocksProxy: &trueVal,
- DisableLocalHTTPProxy: &trueVal,
- },
- nil,
- nil)
- if err != nil {
- t.Fatalf("StartTunnel() error = %v", err)
- }
- defer tunnel.Stop()
- if tt.tunnelStopped {
- tunnel.Stop()
- }
- conn, err := tunnel.Dial(tt.args.remoteAddr)
- if (err != nil) != tt.wantErr {
- t.Fatalf("PsiphonTunnel.Dial() error = %v, wantErr %v", err, tt.wantErr)
- return
- }
- if tt.wantErr != (conn == nil) {
- t.Fatalf("PsiphonTunnel.Dial() conn = %v, wantConn %v", conn, !tt.wantErr)
- }
- })
- }
- }
- // We had a problem where config-related notices were being printed to stderr before we
- // set the NoticeWriter. We want to make sure that no longer happens.
- func TestStartTunnelNoOutput(t *testing.T) {
- // Before starting the tunnel, set up a notice receiver. If it receives anything at
- // all, that means that it would have been printed to stderr.
- psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver(
- func(notice []byte) {
- t.Fatalf("Received notice: %v", string(notice))
- }))
- configJSON := setupConfig(t, false)
- testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
- if err != nil {
- t.Fatalf("ioutil.TempDir failed: %v", err)
- }
- defer os.RemoveAll(testDataDirName)
- ctx := context.Background()
- tunnel, err := StartTunnel(
- ctx,
- configJSON,
- "",
- Parameters{DataRootDirectory: &testDataDirName},
- nil,
- nil)
- if err != nil {
- t.Fatalf("StartTunnel() error = %v", err)
- }
- tunnel.Stop()
- }
- // We had a problem where a very early error could result in `started` being set to true
- // and not be set back to false, preventing StartTunnel from being re-callable.
- func TestStartTunnelReentry(t *testing.T) {
- testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
- if err != nil {
- t.Fatalf("ioutil.TempDir failed: %v", err)
- }
- defer os.RemoveAll(testDataDirName)
- configJSON := []byte("BAD CONFIG JSON")
- ctx := context.Background()
- _, err = StartTunnel(
- ctx,
- configJSON,
- "",
- Parameters{DataRootDirectory: &testDataDirName},
- nil,
- nil)
- if err == nil {
- t.Fatalf("expected config error")
- }
- // Call again with a good config. Should work.
- configJSON = setupConfig(t, false)
- tunnel, err := StartTunnel(
- ctx,
- configJSON,
- "",
- Parameters{DataRootDirectory: &testDataDirName},
- nil,
- nil)
- if err != nil {
- t.Fatalf("StartTunnel() error = %v", err)
- }
- tunnel.Stop()
- }
|