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

     1  /*
     2   * Copyright (c) 2018, 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 clientlib
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	std_errors "errors"
    26  	"fmt"
    27  	"path/filepath"
    28  	"sync"
    29  
    30  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
    31  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
    32  )
    33  
    34  // Parameters provide an easier way to modify the tunnel config at runtime.
    35  type Parameters struct {
    36  	// Used as the directory for the datastore, remote server list, and obfuscasted
    37  	// server list.
    38  	// Empty string means the default will be used (current working directory).
    39  	// nil means the values in the config file will be used.
    40  	// Optional, but strongly recommended.
    41  	DataRootDirectory *string
    42  
    43  	// Overrides config.ClientPlatform. See config.go for details.
    44  	// nil means the value in the config file will be used.
    45  	// Optional, but strongly recommended.
    46  	ClientPlatform *string
    47  
    48  	// Overrides config.NetworkID. For details see:
    49  	// https://godoc.org/github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon#NetworkIDGetter
    50  	// nil means the value in the config file will be used. (If not set in the config,
    51  	// an error will result.)
    52  	// Empty string will produce an error.
    53  	// Optional, but strongly recommended.
    54  	NetworkID *string
    55  
    56  	// Overrides config.EstablishTunnelTimeoutSeconds. See config.go for details.
    57  	// nil means the EstablishTunnelTimeoutSeconds value in the config file will be used.
    58  	// If there's no such value in the config file, the default will be used.
    59  	// Zero means there will be no timeout.
    60  	// Optional.
    61  	EstablishTunnelTimeoutSeconds *int
    62  
    63  	// EmitDiagnosticNoticesToFile indicates whether to use the rotating log file
    64  	// facility to record diagnostic notices instead of sending diagnostic
    65  	// notices to noticeReceiver. Has no effect unless the tunnel
    66  	// config.EmitDiagnosticNotices flag is set.
    67  	EmitDiagnosticNoticesToFiles bool
    68  }
    69  
    70  // PsiphonTunnel is the tunnel object. It can be used for stopping the tunnel and
    71  // retrieving proxy ports.
    72  type PsiphonTunnel struct {
    73  	embeddedServerListWaitGroup sync.WaitGroup
    74  	controllerWaitGroup         sync.WaitGroup
    75  	stopController              context.CancelFunc
    76  
    77  	// The port on which the HTTP proxy is running
    78  	HTTPProxyPort int
    79  	// The port on which the SOCKS proxy is running
    80  	SOCKSProxyPort int
    81  }
    82  
    83  // ParametersDelta allows for fine-grained modification of parameters.Parameters.
    84  // NOTE: Ordinary users of this library should never need this.
    85  type ParametersDelta map[string]interface{}
    86  
    87  // NoticeEvent represents the notices emitted by tunnel core. It will be passed to
    88  // noticeReceiver, if supplied.
    89  // NOTE: Ordinary users of this library should never need this.
    90  type NoticeEvent struct {
    91  	Data      map[string]interface{} `json:"data"`
    92  	Type      string                 `json:"noticeType"`
    93  	Timestamp string                 `json:"timestamp"`
    94  }
    95  
    96  // ErrTimeout is returned when the tunnel establishment attempt fails due to timeout
    97  var ErrTimeout = std_errors.New("clientlib: tunnel establishment timeout")
    98  
    99  // StartTunnel establishes a Psiphon tunnel. It returns an error if the establishment
   100  // was not successful. If the returned error is nil, the returned tunnel can be used
   101  // to find out the proxy ports and subsequently stop the tunnel.
   102  //
   103  // ctx may be cancelable, if the caller wants to be able to interrupt the establishment
   104  // attempt, or context.Background().
   105  //
   106  // configJSON will be passed to psiphon.LoadConfig to configure the tunnel. Required.
   107  //
   108  // embeddedServerEntryList is the encoded embedded server entry list. It is optional.
   109  //
   110  // params are config values that typically need to be overridden at runtime.
   111  //
   112  // paramsDelta contains changes that will be applied to the Parameters.
   113  // NOTE: Ordinary users of this library should never need this and should pass nil.
   114  //
   115  // noticeReceiver, if non-nil, will be called for each notice emitted by tunnel core.
   116  // NOTE: Ordinary users of this library should never need this and should pass nil.
   117  func StartTunnel(
   118  	ctx context.Context,
   119  	configJSON []byte,
   120  	embeddedServerEntryList string,
   121  	params Parameters,
   122  	paramsDelta ParametersDelta,
   123  	noticeReceiver func(NoticeEvent)) (retTunnel *PsiphonTunnel, retErr error) {
   124  
   125  	config, err := psiphon.LoadConfig(configJSON)
   126  	if err != nil {
   127  		return nil, errors.TraceMsg(err, "failed to load config file")
   128  	}
   129  
   130  	// Use params.DataRootDirectory to set related config values.
   131  	if params.DataRootDirectory != nil {
   132  		config.DataRootDirectory = *params.DataRootDirectory
   133  
   134  		// Migrate old fields
   135  		config.MigrateDataStoreDirectory = *params.DataRootDirectory
   136  		config.MigrateObfuscatedServerListDownloadDirectory = *params.DataRootDirectory
   137  		config.MigrateRemoteServerListDownloadFilename = filepath.Join(*params.DataRootDirectory, "server_list_compressed")
   138  	}
   139  
   140  	if params.NetworkID != nil {
   141  		config.NetworkID = *params.NetworkID
   142  	}
   143  
   144  	if params.ClientPlatform != nil {
   145  		config.ClientPlatform = *params.ClientPlatform
   146  	} // else use the value in config
   147  
   148  	if params.EstablishTunnelTimeoutSeconds != nil {
   149  		config.EstablishTunnelTimeoutSeconds = params.EstablishTunnelTimeoutSeconds
   150  	} // else use the value in config
   151  
   152  	if config.UseNoticeFiles == nil && config.EmitDiagnosticNotices && params.EmitDiagnosticNoticesToFiles {
   153  		config.UseNoticeFiles = &psiphon.UseNoticeFiles{
   154  			RotatingFileSize:      0,
   155  			RotatingSyncFrequency: 0,
   156  		}
   157  	} // else use the value in the config
   158  
   159  	// config.Commit must be called before calling config.SetParameters
   160  	// or attempting to connect.
   161  	err = config.Commit(true)
   162  	if err != nil {
   163  		return nil, errors.TraceMsg(err, "config.Commit failed")
   164  	}
   165  
   166  	// If supplied, apply the parameters delta
   167  	if len(paramsDelta) > 0 {
   168  		err = config.SetParameters("", false, paramsDelta)
   169  		if err != nil {
   170  			return nil, errors.TraceMsg(
   171  				err, fmt.Sprintf("SetParameters failed for delta: %v", paramsDelta))
   172  		}
   173  	}
   174  
   175  	// Will receive a value when the tunnel has successfully connected.
   176  	connected := make(chan struct{}, 1)
   177  	// Will receive a value if an error occurs during the connection sequence.
   178  	errored := make(chan error, 1)
   179  
   180  	// Create the tunnel object
   181  	tunnel := new(PsiphonTunnel)
   182  
   183  	// Set up notice handling
   184  	psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver(
   185  		func(notice []byte) {
   186  			var event NoticeEvent
   187  			err := json.Unmarshal(notice, &event)
   188  			if err != nil {
   189  				// This is unexpected and probably indicates something fatal has occurred.
   190  				// We'll interpret it as a connection error and abort.
   191  				err = errors.TraceMsg(err, "failed to unmarshal notice JSON")
   192  				select {
   193  				case errored <- err:
   194  				default:
   195  				}
   196  				return
   197  			}
   198  
   199  			if event.Type == "ListeningHttpProxyPort" {
   200  				port := event.Data["port"].(float64)
   201  				tunnel.HTTPProxyPort = int(port)
   202  			} else if event.Type == "ListeningSocksProxyPort" {
   203  				port := event.Data["port"].(float64)
   204  				tunnel.SOCKSProxyPort = int(port)
   205  			} else if event.Type == "EstablishTunnelTimeout" {
   206  				select {
   207  				case errored <- ErrTimeout:
   208  				default:
   209  				}
   210  			} else if event.Type == "Tunnels" {
   211  				count := event.Data["count"].(float64)
   212  				if count > 0 {
   213  					select {
   214  					case connected <- struct{}{}:
   215  					default:
   216  					}
   217  				}
   218  			}
   219  
   220  			// Some users of this package may need to add special processing of notices.
   221  			// If the caller has requested it, we'll pass on the notices.
   222  			if noticeReceiver != nil {
   223  				noticeReceiver(event)
   224  			}
   225  		}))
   226  
   227  	err = psiphon.OpenDataStore(config)
   228  	if err != nil {
   229  		return nil, errors.TraceMsg(err, "failed to open data store")
   230  	}
   231  	// Make sure we close the datastore in case of error
   232  	defer func() {
   233  		if retErr != nil {
   234  			tunnel.controllerWaitGroup.Wait()
   235  			tunnel.embeddedServerListWaitGroup.Wait()
   236  			psiphon.CloseDataStore()
   237  		}
   238  	}()
   239  
   240  	// Create a cancelable context that will be used for stopping the tunnel
   241  	var controllerCtx context.Context
   242  	controllerCtx, tunnel.stopController = context.WithCancel(ctx)
   243  
   244  	// If specified, the embedded server list is loaded and stored. When there
   245  	// are no server candidates at all, we wait for this import to complete
   246  	// before starting the Psiphon controller. Otherwise, we import while
   247  	// concurrently starting the controller to minimize delay before attempting
   248  	// to connect to existing candidate servers.
   249  	//
   250  	// If the import fails, an error notice is emitted, but the controller is
   251  	// still started: either existing candidate servers may suffice, or the
   252  	// remote server list fetch may obtain candidate servers.
   253  	//
   254  	// The import will be interrupted if it's still running when the controller
   255  	// is stopped.
   256  	tunnel.embeddedServerListWaitGroup.Add(1)
   257  	go func() {
   258  		defer tunnel.embeddedServerListWaitGroup.Done()
   259  
   260  		err := psiphon.ImportEmbeddedServerEntries(
   261  			controllerCtx,
   262  			config,
   263  			"",
   264  			embeddedServerEntryList)
   265  		if err != nil {
   266  			psiphon.NoticeError("error importing embedded server entry list: %s", err)
   267  			return
   268  		}
   269  	}()
   270  	if !psiphon.HasServerEntries() {
   271  		psiphon.NoticeInfo("awaiting embedded server entry list import")
   272  		tunnel.embeddedServerListWaitGroup.Wait()
   273  	}
   274  
   275  	// Create the Psiphon controller
   276  	controller, err := psiphon.NewController(config)
   277  	if err != nil {
   278  		tunnel.stopController()
   279  		tunnel.embeddedServerListWaitGroup.Wait()
   280  		return nil, errors.TraceMsg(err, "psiphon.NewController failed")
   281  	}
   282  
   283  	// Begin tunnel connection
   284  	tunnel.controllerWaitGroup.Add(1)
   285  	go func() {
   286  		defer tunnel.controllerWaitGroup.Done()
   287  
   288  		// Start the tunnel. Only returns on error (or internal timeout).
   289  		controller.Run(controllerCtx)
   290  
   291  		// controller.Run does not exit until the goroutine that posts
   292  		// EstablishTunnelTimeout has terminated; so, if there was a
   293  		// EstablishTunnelTimeout event, ErrTimeout is guaranteed to be sent to
   294  		// errord before this next error and will be the StartTunnel return value.
   295  
   296  		var err error
   297  		switch ctx.Err() {
   298  		case context.DeadlineExceeded:
   299  			err = ErrTimeout
   300  		case context.Canceled:
   301  			err = errors.TraceNew("StartTunnel canceled")
   302  		default:
   303  			err = errors.TraceNew("controller.Run exited unexpectedly")
   304  		}
   305  		select {
   306  		case errored <- err:
   307  		default:
   308  		}
   309  	}()
   310  
   311  	// Wait for an active tunnel or error
   312  	select {
   313  	case <-connected:
   314  		return tunnel, nil
   315  	case err := <-errored:
   316  		tunnel.Stop()
   317  		if err != ErrTimeout {
   318  			err = errors.TraceMsg(err, "tunnel start produced error")
   319  		}
   320  		return nil, err
   321  	}
   322  }
   323  
   324  // Stop stops/disconnects/shuts down the tunnel. It is safe to call when not connected.
   325  // Not safe to call concurrently with Start.
   326  func (tunnel *PsiphonTunnel) Stop() {
   327  	if tunnel.stopController == nil {
   328  		return
   329  	}
   330  	tunnel.stopController()
   331  	tunnel.controllerWaitGroup.Wait()
   332  	tunnel.embeddedServerListWaitGroup.Wait()
   333  	psiphon.CloseDataStore()
   334  }