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

     1  /*
     2   * Copyright (c) 2015, 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 psi
    21  
    22  // This package is a shim between Java/Obj-C and the "psiphon" package. Due to limitations
    23  // on what Go types may be exposed (http://godoc.org/golang.org/x/mobile/cmd/gobind),
    24  // a psiphon.Controller cannot be directly used by Java. This shim exposes a trivial
    25  // Start/Stop interface on top of a single Controller instance.
    26  
    27  import (
    28  	"context"
    29  	"encoding/json"
    30  	"fmt"
    31  	"os"
    32  	"path/filepath"
    33  	"strings"
    34  	"sync"
    35  
    36  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
    37  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    38  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo"
    39  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tun"
    40  )
    41  
    42  type PsiphonProviderNoticeHandler interface {
    43  	Notice(noticeJSON string)
    44  }
    45  
    46  type PsiphonProviderNetwork interface {
    47  	HasNetworkConnectivity() int
    48  	GetNetworkID() string
    49  	IPv6Synthesize(IPv4Addr string) string
    50  	HasIPv6Route() int
    51  }
    52  
    53  type PsiphonProvider interface {
    54  	PsiphonProviderNoticeHandler
    55  	PsiphonProviderNetwork
    56  	BindToDevice(fileDescriptor int) (string, error)
    57  
    58  	// TODO: move GetDNSServersAsString to PsiphonProviderNetwork to
    59  	// facilitate custom tunnel-core resolver support in SendFeedback.
    60  
    61  	// GetDNSServersAsString must return a comma-delimited list of DNS server
    62  	// addresses. A single string return value is used since gobind does not
    63  	// support string slice types.
    64  	GetDNSServersAsString() string
    65  }
    66  
    67  type PsiphonProviderFeedbackHandler interface {
    68  	SendFeedbackCompleted(err error)
    69  }
    70  
    71  func NoticeUserLog(message string) {
    72  	psiphon.NoticeUserLog(message)
    73  }
    74  
    75  // HomepageFilePath returns the path where homepage files will be paved.
    76  //
    77  // rootDataDirectoryPath is the configured data root directory.
    78  //
    79  // Note: homepage files will only be paved if UseNoticeFiles is set in the
    80  // config passed to Start().
    81  func HomepageFilePath(rootDataDirectoryPath string) string {
    82  	return filepath.Join(rootDataDirectoryPath, psiphon.PsiphonDataDirectoryName, psiphon.HomepageFilename)
    83  }
    84  
    85  // NoticesFilePath returns the path where the notices file will be paved.
    86  //
    87  // rootDataDirectoryPath is the configured data root directory.
    88  //
    89  // Note: notices will only be paved if UseNoticeFiles is set in the config
    90  // passed to Start().
    91  func NoticesFilePath(rootDataDirectoryPath string) string {
    92  	return filepath.Join(rootDataDirectoryPath, psiphon.PsiphonDataDirectoryName, psiphon.NoticesFilename)
    93  }
    94  
    95  // OldNoticesFilePath returns the path where the notices file is moved to when
    96  // file rotation occurs.
    97  //
    98  // rootDataDirectoryPath is the configured data root directory.
    99  //
   100  // Note: notices will only be paved if UseNoticeFiles is set in the config
   101  // passed to Start().
   102  func OldNoticesFilePath(rootDataDirectoryPath string) string {
   103  	return filepath.Join(rootDataDirectoryPath, psiphon.PsiphonDataDirectoryName, psiphon.OldNoticesFilename)
   104  }
   105  
   106  // UpgradeDownloadFilePath returns the path where the downloaded upgrade file
   107  // will be paved.
   108  //
   109  // rootDataDirectoryPath is the configured data root directory.
   110  //
   111  // Note: upgrades will only be paved if UpgradeDownloadURLs is set in the config
   112  // passed to Start() and there are upgrades available.
   113  func UpgradeDownloadFilePath(rootDataDirectoryPath string) string {
   114  	return filepath.Join(rootDataDirectoryPath, psiphon.PsiphonDataDirectoryName, psiphon.UpgradeDownloadFilename)
   115  }
   116  
   117  var controllerMutex sync.Mutex
   118  var embeddedServerListWaitGroup *sync.WaitGroup
   119  var controller *psiphon.Controller
   120  var controllerCtx context.Context
   121  var stopController context.CancelFunc
   122  var controllerWaitGroup *sync.WaitGroup
   123  
   124  func Start(
   125  	configJson string,
   126  	embeddedServerEntryList string,
   127  	embeddedServerEntryListFilename string,
   128  	provider PsiphonProvider,
   129  	useDeviceBinder bool,
   130  	useIPv6Synthesizer bool,
   131  	useHasIPv6RouteGetter bool) error {
   132  
   133  	controllerMutex.Lock()
   134  	defer controllerMutex.Unlock()
   135  
   136  	if controller != nil {
   137  		return fmt.Errorf("already started")
   138  	}
   139  
   140  	// Clients may toggle Stop/Start immediately to apply new config settings
   141  	// such as EgressRegion or Authorizations. When this restart is within the
   142  	// same process and in a memory contrained environment, it is useful to
   143  	// force garbage collection here to reclaim memory used by the previous
   144  	// Controller.
   145  	psiphon.DoGarbageCollection()
   146  
   147  	// Wrap the provider in a layer that locks a mutex before calling a provider function.
   148  	// As the provider callbacks are Java/Obj-C via gomobile, they are cgo calls that
   149  	// can cause OS threads to be spawned. The mutex prevents many calling goroutines from
   150  	// causing unbounded numbers of OS threads to be spawned.
   151  	// TODO: replace the mutex with a semaphore, to allow a larger but still bounded concurrent
   152  	// number of calls to the provider?
   153  	wrappedProvider := newMutexPsiphonProvider(provider)
   154  
   155  	config, err := psiphon.LoadConfig([]byte(configJson))
   156  	if err != nil {
   157  		return fmt.Errorf("error loading configuration file: %s", err)
   158  	}
   159  
   160  	// Set up callbacks.
   161  
   162  	config.NetworkConnectivityChecker = wrappedProvider
   163  	config.NetworkIDGetter = wrappedProvider
   164  	config.DNSServerGetter = wrappedProvider
   165  
   166  	if useDeviceBinder {
   167  		config.DeviceBinder = wrappedProvider
   168  	}
   169  
   170  	if useIPv6Synthesizer {
   171  		config.IPv6Synthesizer = wrappedProvider
   172  	}
   173  
   174  	if useHasIPv6RouteGetter {
   175  		config.HasIPv6RouteGetter = wrappedProvider
   176  	}
   177  
   178  	// All config fields should be set before calling Commit.
   179  
   180  	err = config.Commit(true)
   181  	if err != nil {
   182  		return fmt.Errorf("error committing configuration file: %s", err)
   183  	}
   184  
   185  	psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver(
   186  		func(notice []byte) {
   187  			wrappedProvider.Notice(string(notice))
   188  		}))
   189  
   190  	// BuildInfo is a diagnostic notice, so emit only after config.Commit
   191  	// sets EmitDiagnosticNotices.
   192  
   193  	psiphon.NoticeBuildInfo()
   194  
   195  	err = psiphon.OpenDataStore(config)
   196  	if err != nil {
   197  		return fmt.Errorf("error initializing datastore: %s", err)
   198  	}
   199  
   200  	controllerCtx, stopController = context.WithCancel(context.Background())
   201  
   202  	// If specified, the embedded server list is loaded and stored. When there
   203  	// are no server candidates at all, we wait for this import to complete
   204  	// before starting the Psiphon controller. Otherwise, we import while
   205  	// concurrently starting the controller to minimize delay before attempting
   206  	// to connect to existing candidate servers.
   207  	//
   208  	// If the import fails, an error notice is emitted, but the controller is
   209  	// still started: either existing candidate servers may suffice, or the
   210  	// remote server list fetch may obtain candidate servers.
   211  	//
   212  	// The import will be interrupted if it's still running when the controller
   213  	// is stopped.
   214  	embeddedServerListWaitGroup = new(sync.WaitGroup)
   215  	embeddedServerListWaitGroup.Add(1)
   216  	go func() {
   217  		defer embeddedServerListWaitGroup.Done()
   218  
   219  		err := psiphon.ImportEmbeddedServerEntries(
   220  			controllerCtx,
   221  			config,
   222  			embeddedServerEntryListFilename,
   223  			embeddedServerEntryList)
   224  		if err != nil {
   225  			psiphon.NoticeError("error importing embedded server entry list: %s", err)
   226  			return
   227  		}
   228  	}()
   229  	if !psiphon.HasServerEntries() {
   230  		psiphon.NoticeInfo("awaiting embedded server entry list import")
   231  		embeddedServerListWaitGroup.Wait()
   232  	}
   233  
   234  	controller, err = psiphon.NewController(config)
   235  	if err != nil {
   236  		stopController()
   237  		embeddedServerListWaitGroup.Wait()
   238  		psiphon.CloseDataStore()
   239  		return fmt.Errorf("error initializing controller: %s", err)
   240  	}
   241  
   242  	controllerWaitGroup = new(sync.WaitGroup)
   243  	controllerWaitGroup.Add(1)
   244  	go func() {
   245  		defer controllerWaitGroup.Done()
   246  		controller.Run(controllerCtx)
   247  	}()
   248  
   249  	return nil
   250  }
   251  
   252  func Stop() {
   253  
   254  	controllerMutex.Lock()
   255  	defer controllerMutex.Unlock()
   256  
   257  	if controller != nil {
   258  		stopController()
   259  		controllerWaitGroup.Wait()
   260  		embeddedServerListWaitGroup.Wait()
   261  		psiphon.CloseDataStore()
   262  		controller = nil
   263  		controllerCtx = nil
   264  		stopController = nil
   265  		controllerWaitGroup = nil
   266  	}
   267  }
   268  
   269  // ReconnectTunnel initiates a reconnect of the current tunnel, if one is
   270  // running.
   271  func ReconnectTunnel() {
   272  
   273  	controllerMutex.Lock()
   274  	defer controllerMutex.Unlock()
   275  
   276  	if controller != nil {
   277  		controller.TerminateNextActiveTunnel()
   278  	}
   279  }
   280  
   281  // SetDynamicConfig overrides the sponsor ID and authorizations fields set in
   282  // the config passed to Start. SetDynamicConfig has no effect if no Controller
   283  // is started.
   284  //
   285  // The input newAuthorizationsList is a space-delimited list of base64
   286  // authorizations. This is a workaround for gobind type limitations.
   287  func SetDynamicConfig(newSponsorID, newAuthorizationsList string) {
   288  
   289  	controllerMutex.Lock()
   290  	defer controllerMutex.Unlock()
   291  
   292  	if controller != nil {
   293  
   294  		var authorizations []string
   295  		if len(newAuthorizationsList) > 0 {
   296  			authorizations = strings.Split(newAuthorizationsList, " ")
   297  		}
   298  
   299  		controller.SetDynamicConfig(
   300  			newSponsorID,
   301  			authorizations)
   302  	}
   303  }
   304  
   305  // ExportExchangePayload creates a payload for client-to-client server
   306  // connection info exchange.
   307  //
   308  // ExportExchangePayload will succeed only when Psiphon is running, between
   309  // Start and Stop.
   310  //
   311  // The return value is a payload that may be exchanged with another client;
   312  // when "", the export failed and a diagnostic has been logged.
   313  func ExportExchangePayload() string {
   314  
   315  	controllerMutex.Lock()
   316  	defer controllerMutex.Unlock()
   317  
   318  	if controller == nil {
   319  		return ""
   320  	}
   321  
   322  	return controller.ExportExchangePayload()
   323  }
   324  
   325  // ImportExchangePayload imports a payload generated by ExportExchangePayload.
   326  //
   327  // If an import occurs when Psiphon is working to establsh a tunnel, the newly
   328  // imported server entry is prioritized.
   329  //
   330  // The return value indicates a successful import. If the import failed, a a
   331  // diagnostic notice has been logged.
   332  func ImportExchangePayload(payload string) bool {
   333  
   334  	controllerMutex.Lock()
   335  	defer controllerMutex.Unlock()
   336  
   337  	if controller == nil {
   338  		return false
   339  	}
   340  
   341  	return controller.ImportExchangePayload(payload)
   342  }
   343  
   344  var sendFeedbackMutex sync.Mutex
   345  var sendFeedbackCtx context.Context
   346  var stopSendFeedback context.CancelFunc
   347  var sendFeedbackWaitGroup *sync.WaitGroup
   348  
   349  // StartSendFeedback encrypts the provided diagnostics and then attempts to
   350  // upload the encrypted diagnostics to one of the feedback upload locations
   351  // supplied by the provided config or tactics.
   352  //
   353  // Returns immediately after starting the operation in a goroutine. The
   354  // operation has completed when SendFeedbackCompleted(error) is called on the
   355  // provided PsiphonProviderFeedbackHandler; if error is non-nil, then the
   356  // operation failed.
   357  //
   358  // Only one active upload is supported at a time. An ongoing upload will be
   359  // cancelled if this function is called again before it completes.
   360  //
   361  // Warnings:
   362  // - Should not be used with Start concurrently in the same process
   363  // - An ongoing feedback upload started with StartSendFeedback should be
   364  //   stopped with StopSendFeedback before the process exists. This ensures that
   365  //   any underlying resources are cleaned up; failing to do so may result in
   366  //   data store corruption or other undefined behavior.
   367  // - Start and StartSendFeedback both make an attempt to migrate persistent
   368  //   files from legacy locations in a one-time operation. If these functions
   369  //   are called in parallel, then there is a chance that the migration attempts
   370  //   could execute at the same time and result in non-fatal errors in one, or
   371  //   both, of the migration operations.
   372  // - Calling StartSendFeedback or StopSendFeedback on the same call stack
   373  //   that the PsiphonProviderFeedbackHandler.SendFeedbackCompleted() callback
   374  //   is delivered on can cause a deadlock. I.E. the callback code must return
   375  //   so the wait group can complete and the lock acquired in StopSendFeedback
   376  //   can be released.
   377  func StartSendFeedback(
   378  	configJson,
   379  	diagnosticsJson,
   380  	uploadPath string,
   381  	feedbackHandler PsiphonProviderFeedbackHandler,
   382  	networkInfoProvider PsiphonProviderNetwork,
   383  	noticeHandler PsiphonProviderNoticeHandler,
   384  	useIPv6Synthesizer bool,
   385  	useHasIPv6RouteGetter bool) error {
   386  
   387  	// Cancel any ongoing uploads.
   388  	StopSendFeedback()
   389  
   390  	sendFeedbackMutex.Lock()
   391  	defer sendFeedbackMutex.Unlock()
   392  
   393  	sendFeedbackCtx, stopSendFeedback = context.WithCancel(context.Background())
   394  
   395  	// Unlike in Start, the provider is not wrapped in a newMutexPsiphonProvider
   396  	// or equivilent, as SendFeedback is not expected to be used in a memory
   397  	// constrained environment.
   398  
   399  	psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver(
   400  		func(notice []byte) {
   401  			noticeHandler.Notice(string(notice))
   402  		}))
   403  
   404  	config, err := psiphon.LoadConfig([]byte(configJson))
   405  	if err != nil {
   406  		return fmt.Errorf("error loading configuration file: %s", err)
   407  	}
   408  
   409  	// Set up callbacks.
   410  
   411  	config.NetworkConnectivityChecker = networkInfoProvider
   412  	config.NetworkIDGetter = networkInfoProvider
   413  
   414  	if useIPv6Synthesizer {
   415  		config.IPv6Synthesizer = networkInfoProvider
   416  	}
   417  
   418  	if useHasIPv6RouteGetter {
   419  		config.HasIPv6RouteGetter = networkInfoProvider
   420  	}
   421  
   422  	// Limitation: config.DNSServerGetter is not set up in the SendFeedback
   423  	// case, as we don't currently implement network path and system DNS
   424  	// server monitoring for SendFeedback in the platform code. To ensure we
   425  	// fallback to the system resolver and don't always use the custom
   426  	// resolver with alternate DNS servers, clear that config field (this may
   427  	// still be set via tactics).
   428  	config.DNSResolverAlternateServers = nil
   429  
   430  	// All config fields should be set before calling Commit.
   431  
   432  	err = config.Commit(true)
   433  	if err != nil {
   434  		return fmt.Errorf("error committing configuration file: %s", err)
   435  	}
   436  
   437  	sendFeedbackWaitGroup = new(sync.WaitGroup)
   438  	sendFeedbackWaitGroup.Add(1)
   439  	go func() {
   440  		defer sendFeedbackWaitGroup.Done()
   441  		err := psiphon.SendFeedback(sendFeedbackCtx, config,
   442  			diagnosticsJson, uploadPath)
   443  		feedbackHandler.SendFeedbackCompleted(err)
   444  	}()
   445  
   446  	return nil
   447  }
   448  
   449  // StopSendFeedback interrupts an in-progress feedback upload operation
   450  // started with `StartSendFeedback`.
   451  //
   452  // Warning: should not be used with Start concurrently in the same process.
   453  func StopSendFeedback() {
   454  
   455  	sendFeedbackMutex.Lock()
   456  	defer sendFeedbackMutex.Unlock()
   457  
   458  	if stopSendFeedback != nil {
   459  		stopSendFeedback()
   460  		sendFeedbackWaitGroup.Wait()
   461  		sendFeedbackCtx = nil
   462  		stopSendFeedback = nil
   463  		sendFeedbackWaitGroup = nil
   464  		// Allow the notice receiver to be deallocated.
   465  		psiphon.SetNoticeWriter(os.Stderr)
   466  	}
   467  }
   468  
   469  // Get build info from tunnel-core
   470  func GetBuildInfo() string {
   471  	buildInfo, err := json.Marshal(buildinfo.GetBuildInfo())
   472  	if err != nil {
   473  		return ""
   474  	}
   475  	return string(buildInfo)
   476  }
   477  
   478  func GetPacketTunnelMTU() int {
   479  	return tun.DEFAULT_MTU
   480  }
   481  
   482  func GetPacketTunnelDNSResolverIPv4Address() string {
   483  	return tun.GetTransparentDNSResolverIPv4Address().String()
   484  }
   485  
   486  func GetPacketTunnelDNSResolverIPv6Address() string {
   487  	return tun.GetTransparentDNSResolverIPv6Address().String()
   488  }
   489  
   490  // WriteRuntimeProfiles writes Go runtime profile information to a set of
   491  // files in the specified output directory. See common.WriteRuntimeProfiles
   492  // for more details.
   493  //
   494  // If called before Start, log notices will emit to stderr.
   495  func WriteRuntimeProfiles(outputDirectory string, cpuSampleDurationSeconds, blockSampleDurationSeconds int) {
   496  	common.WriteRuntimeProfiles(
   497  		psiphon.NoticeCommonLogger(),
   498  		outputDirectory,
   499  		"",
   500  		cpuSampleDurationSeconds,
   501  		blockSampleDurationSeconds)
   502  }
   503  
   504  type mutexPsiphonProvider struct {
   505  	sync.Mutex
   506  	p PsiphonProvider
   507  }
   508  
   509  func newMutexPsiphonProvider(p PsiphonProvider) *mutexPsiphonProvider {
   510  	return &mutexPsiphonProvider{p: p}
   511  }
   512  
   513  func (p *mutexPsiphonProvider) Notice(noticeJSON string) {
   514  	p.Lock()
   515  	defer p.Unlock()
   516  	p.p.Notice(noticeJSON)
   517  }
   518  
   519  func (p *mutexPsiphonProvider) HasNetworkConnectivity() int {
   520  	p.Lock()
   521  	defer p.Unlock()
   522  	return p.p.HasNetworkConnectivity()
   523  }
   524  
   525  func (p *mutexPsiphonProvider) BindToDevice(fileDescriptor int) (string, error) {
   526  	p.Lock()
   527  	defer p.Unlock()
   528  	return p.p.BindToDevice(fileDescriptor)
   529  }
   530  
   531  func (p *mutexPsiphonProvider) IPv6Synthesize(IPv4Addr string) string {
   532  	p.Lock()
   533  	defer p.Unlock()
   534  	return p.p.IPv6Synthesize(IPv4Addr)
   535  }
   536  
   537  func (p *mutexPsiphonProvider) HasIPv6Route() int {
   538  	p.Lock()
   539  	defer p.Unlock()
   540  	return p.p.HasIPv6Route()
   541  }
   542  
   543  func (p *mutexPsiphonProvider) GetDNSServersAsString() string {
   544  	p.Lock()
   545  	defer p.Unlock()
   546  	return p.p.GetDNSServersAsString()
   547  }
   548  
   549  func (p *mutexPsiphonProvider) GetDNSServers() []string {
   550  	p.Lock()
   551  	defer p.Unlock()
   552  	s := p.p.GetDNSServersAsString()
   553  	if s == "" {
   554  		return []string{}
   555  	}
   556  	return strings.Split(s, ",")
   557  }
   558  
   559  func (p *mutexPsiphonProvider) GetNetworkID() string {
   560  	p.Lock()
   561  	defer p.Unlock()
   562  	return p.p.GetNetworkID()
   563  }