github.com/psiphon-labs/psiphon-tunnel-core@v2.0.28+incompatible/ConsoleClient/main.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 main
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"encoding/json"
    26  	"flag"
    27  	"fmt"
    28  	"io"
    29  	"io/ioutil"
    30  	"os"
    31  	"os/signal"
    32  	"sort"
    33  	"strings"
    34  	"sync"
    35  	"syscall"
    36  
    37  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
    38  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common"
    39  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo"
    40  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
    41  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tun"
    42  )
    43  
    44  func main() {
    45  
    46  	// Define command-line parameters
    47  
    48  	var configFilename string
    49  	flag.StringVar(&configFilename, "config", "", "configuration input file")
    50  
    51  	var dataRootDirectory string
    52  	flag.StringVar(&dataRootDirectory, "dataRootDirectory", "", "directory where persistent files will be stored")
    53  
    54  	var embeddedServerEntryListFilename string
    55  	flag.StringVar(&embeddedServerEntryListFilename, "serverList", "", "embedded server entry list input file")
    56  
    57  	var formatNotices bool
    58  	flag.BoolVar(&formatNotices, "formatNotices", false, "emit notices in human-readable format")
    59  
    60  	var interfaceName string
    61  	flag.StringVar(&interfaceName, "listenInterface", "", "bind local proxies to specified interface")
    62  
    63  	var versionDetails bool
    64  	flag.BoolVar(&versionDetails, "version", false, "print build information and exit")
    65  	flag.BoolVar(&versionDetails, "v", false, "print build information and exit")
    66  
    67  	var feedbackUpload bool
    68  	flag.BoolVar(&feedbackUpload, "feedbackUpload", false,
    69  		"Run in feedback upload mode to send a feedback package to Psiphon Inc.\n"+
    70  			"The feedback package will be read as a UTF-8 encoded string from stdin.\n"+
    71  			"Informational notices will be written to stdout. If the upload succeeds,\n"+
    72  			"the process will exit with status code 0; otherwise, the process will\n"+
    73  			"exit with status code 1. A feedback compatible config must be specified\n"+
    74  			"with the \"-config\" flag. Config must be provided by Psiphon Inc.")
    75  
    76  	var feedbackUploadPath string
    77  	flag.StringVar(&feedbackUploadPath, "feedbackUploadPath", "",
    78  		"The path at which to upload the feedback package when the \"-feedbackUpload\"\n"+
    79  			"flag is provided. Must be provided by Psiphon Inc.")
    80  
    81  	var tunDevice, tunBindInterface, tunDNSServers string
    82  	if tun.IsSupported() {
    83  
    84  		// When tunDevice is specified, a packet tunnel is run and packets are relayed between
    85  		// the specified tun device and the server.
    86  		//
    87  		// The tun device is expected to exist and should be configured with an IP address and
    88  		// routing.
    89  		//
    90  		// The tunBindInterface/tunPrimaryDNS/tunSecondaryDNS parameters are used to bypass any
    91  		// tun device routing when connecting to Psiphon servers.
    92  		//
    93  		// For transparent tunneled DNS, set the host or DNS clients to use the address specfied
    94  		// in tun.GetTransparentDNSResolverIPv4Address().
    95  		//
    96  		// Packet tunnel mode is supported only on certains platforms.
    97  
    98  		flag.StringVar(&tunDevice, "tunDevice", "", "run packet tunnel for specified tun device")
    99  		flag.StringVar(&tunBindInterface, "tunBindInterface", tun.DEFAULT_PUBLIC_INTERFACE_NAME, "bypass tun device via specified interface")
   100  		flag.StringVar(&tunDNSServers, "tunDNSServers", "8.8.8.8,8.8.4.4", "Comma-delimited list of tun bypass DNS server IP addresses")
   101  	}
   102  
   103  	var noticeFilename string
   104  	flag.StringVar(&noticeFilename, "notices", "", "notices output file (defaults to stderr)")
   105  
   106  	var useNoticeFiles bool
   107  	useNoticeFilesUsage := fmt.Sprintf("output homepage notices and rotating notices to <dataRootDirectory>/%s and <dataRootDirectory>/%s respectively", psiphon.HomepageFilename, psiphon.NoticesFilename)
   108  	flag.BoolVar(&useNoticeFiles, "useNoticeFiles", false, useNoticeFilesUsage)
   109  
   110  	var rotatingFileSize int
   111  	flag.IntVar(&rotatingFileSize, "rotatingFileSize", 1<<20, "rotating notices file size")
   112  
   113  	var rotatingSyncFrequency int
   114  	flag.IntVar(&rotatingSyncFrequency, "rotatingSyncFrequency", 100, "rotating notices file sync frequency")
   115  
   116  	flag.Parse()
   117  
   118  	if versionDetails {
   119  		b := buildinfo.GetBuildInfo()
   120  
   121  		var printableDependencies bytes.Buffer
   122  		var dependencyMap map[string]string
   123  		longestRepoUrl := 0
   124  		json.Unmarshal(b.Dependencies, &dependencyMap)
   125  
   126  		sortedRepoUrls := make([]string, 0, len(dependencyMap))
   127  		for repoUrl := range dependencyMap {
   128  			repoUrlLength := len(repoUrl)
   129  			if repoUrlLength > longestRepoUrl {
   130  				longestRepoUrl = repoUrlLength
   131  			}
   132  
   133  			sortedRepoUrls = append(sortedRepoUrls, repoUrl)
   134  		}
   135  		sort.Strings(sortedRepoUrls)
   136  
   137  		for repoUrl := range sortedRepoUrls {
   138  			printableDependencies.WriteString(fmt.Sprintf("    %s  ", sortedRepoUrls[repoUrl]))
   139  			for i := 0; i < (longestRepoUrl - len(sortedRepoUrls[repoUrl])); i++ {
   140  				printableDependencies.WriteString(" ")
   141  			}
   142  			printableDependencies.WriteString(fmt.Sprintf("%s\n", dependencyMap[sortedRepoUrls[repoUrl]]))
   143  		}
   144  
   145  		fmt.Printf("Psiphon Console Client\n  Build Date: %s\n  Built With: %s\n  Repository: %s\n  Revision: %s\n  Dependencies:\n%s\n", b.BuildDate, b.GoVersion, b.BuildRepo, b.BuildRev, printableDependencies.String())
   146  		os.Exit(0)
   147  	}
   148  
   149  	// Initialize notice output
   150  
   151  	var noticeWriter io.Writer
   152  	noticeWriter = os.Stderr
   153  
   154  	if noticeFilename != "" {
   155  		noticeFile, err := os.OpenFile(noticeFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
   156  		if err != nil {
   157  			fmt.Printf("error opening notice file: %s\n", err)
   158  			os.Exit(1)
   159  		}
   160  		defer noticeFile.Close()
   161  		noticeWriter = noticeFile
   162  	}
   163  
   164  	if formatNotices {
   165  		noticeWriter = psiphon.NewNoticeConsoleRewriter(noticeWriter)
   166  	}
   167  	psiphon.SetNoticeWriter(noticeWriter)
   168  
   169  	// Handle required config file parameter
   170  
   171  	// EmitDiagnosticNotices is set by LoadConfig; force to true
   172  	// and emit diagnostics when LoadConfig-related errors occur.
   173  
   174  	if configFilename == "" {
   175  		psiphon.SetEmitDiagnosticNotices(true, false)
   176  		psiphon.NoticeError("configuration file is required")
   177  		os.Exit(1)
   178  	}
   179  	configFileContents, err := ioutil.ReadFile(configFilename)
   180  	if err != nil {
   181  		psiphon.SetEmitDiagnosticNotices(true, false)
   182  		psiphon.NoticeError("error loading configuration file: %s", err)
   183  		os.Exit(1)
   184  	}
   185  	config, err := psiphon.LoadConfig(configFileContents)
   186  	if err != nil {
   187  		psiphon.SetEmitDiagnosticNotices(true, false)
   188  		psiphon.NoticeError("error processing configuration file: %s", err)
   189  		os.Exit(1)
   190  	}
   191  
   192  	// Set data root directory
   193  	if dataRootDirectory != "" {
   194  		config.DataRootDirectory = dataRootDirectory
   195  	}
   196  
   197  	if interfaceName != "" {
   198  		config.ListenInterface = interfaceName
   199  	}
   200  
   201  	// Configure notice files
   202  
   203  	if useNoticeFiles {
   204  		config.UseNoticeFiles = &psiphon.UseNoticeFiles{
   205  			RotatingFileSize:      rotatingFileSize,
   206  			RotatingSyncFrequency: rotatingSyncFrequency,
   207  		}
   208  	}
   209  
   210  	// Configure packet tunnel, including updating the config.
   211  
   212  	if tun.IsSupported() && tunDevice != "" {
   213  		tunDeviceFile, err := configurePacketTunnel(
   214  			config, tunDevice, tunBindInterface, strings.Split(tunDNSServers, ","))
   215  		if err != nil {
   216  			psiphon.SetEmitDiagnosticNotices(true, false)
   217  			psiphon.NoticeError("error configuring packet tunnel: %s", err)
   218  			os.Exit(1)
   219  		}
   220  		defer tunDeviceFile.Close()
   221  	}
   222  
   223  	// All config fields should be set before calling Commit.
   224  
   225  	err = config.Commit(true)
   226  	if err != nil {
   227  		psiphon.SetEmitDiagnosticNotices(true, false)
   228  		psiphon.NoticeError("error loading configuration file: %s", err)
   229  		os.Exit(1)
   230  	}
   231  
   232  	// BuildInfo is a diagnostic notice, so emit only after config.Commit
   233  	// sets EmitDiagnosticNotices.
   234  
   235  	psiphon.NoticeBuildInfo()
   236  
   237  	var worker Worker
   238  
   239  	if feedbackUpload {
   240  		// Feedback upload mode
   241  		worker = &FeedbackWorker{
   242  			feedbackUploadPath: feedbackUploadPath,
   243  		}
   244  	} else {
   245  		// Tunnel mode
   246  		worker = &TunnelWorker{
   247  			embeddedServerEntryListFilename: embeddedServerEntryListFilename,
   248  		}
   249  	}
   250  
   251  	workCtx, stopWork := context.WithCancel(context.Background())
   252  	defer stopWork()
   253  
   254  	err = worker.Init(workCtx, config)
   255  	if err != nil {
   256  		psiphon.NoticeError("error in init: %s", err)
   257  		os.Exit(1)
   258  	}
   259  
   260  	workWaitGroup := new(sync.WaitGroup)
   261  	workWaitGroup.Add(1)
   262  	go func() {
   263  		defer workWaitGroup.Done()
   264  
   265  		err := worker.Run(workCtx)
   266  		if err != nil {
   267  			psiphon.NoticeError("%s", err)
   268  			stopWork()
   269  			os.Exit(1)
   270  		}
   271  
   272  		// Signal the <-controllerCtx.Done() case below. If the <-systemStopSignal
   273  		// case already called stopController, this is a noop.
   274  		stopWork()
   275  	}()
   276  
   277  	systemStopSignal := make(chan os.Signal, 1)
   278  	signal.Notify(systemStopSignal, os.Interrupt, syscall.SIGTERM)
   279  
   280  	// writeProfilesSignal is nil and non-functional on Windows
   281  	writeProfilesSignal := makeSIGUSR2Channel()
   282  
   283  	// Wait for an OS signal or a Run stop signal, then stop Psiphon and exit
   284  
   285  	for exit := false; !exit; {
   286  		select {
   287  		case <-writeProfilesSignal:
   288  			psiphon.NoticeInfo("write profiles")
   289  			profileSampleDurationSeconds := 5
   290  			common.WriteRuntimeProfiles(
   291  				psiphon.NoticeCommonLogger(),
   292  				config.DataRootDirectory,
   293  				"",
   294  				profileSampleDurationSeconds,
   295  				profileSampleDurationSeconds)
   296  		case <-systemStopSignal:
   297  			psiphon.NoticeInfo("shutdown by system")
   298  			stopWork()
   299  			workWaitGroup.Wait()
   300  			exit = true
   301  		case <-workCtx.Done():
   302  			psiphon.NoticeInfo("shutdown by controller")
   303  			exit = true
   304  		}
   305  	}
   306  }
   307  
   308  func configurePacketTunnel(
   309  	config *psiphon.Config,
   310  	tunDevice string,
   311  	tunBindInterface string,
   312  	tunDNSServers []string) (*os.File, error) {
   313  
   314  	file, _, err := tun.OpenTunDevice(tunDevice)
   315  	if err != nil {
   316  		return nil, errors.Trace(err)
   317  	}
   318  
   319  	provider := &tunProvider{
   320  		bindInterface: tunBindInterface,
   321  		dnsServers:    tunDNSServers,
   322  	}
   323  
   324  	config.PacketTunnelTunFileDescriptor = int(file.Fd())
   325  	config.DeviceBinder = provider
   326  	config.DNSServerGetter = provider
   327  
   328  	return file, nil
   329  }
   330  
   331  type tunProvider struct {
   332  	bindInterface string
   333  	dnsServers    []string
   334  }
   335  
   336  // BindToDevice implements the psiphon.DeviceBinder interface.
   337  func (p *tunProvider) BindToDevice(fileDescriptor int) (string, error) {
   338  	return p.bindInterface, tun.BindToDevice(fileDescriptor, p.bindInterface)
   339  }
   340  
   341  // GetDNSServers implements the psiphon.DNSServerGetter interface.
   342  func (p *tunProvider) GetDNSServers() []string {
   343  	return p.dnsServers
   344  }
   345  
   346  // Worker creates a protocol around the different run modes provided by the
   347  // compiled executable.
   348  type Worker interface {
   349  	// Init is called once for the worker to perform any initialization.
   350  	Init(ctx context.Context, config *psiphon.Config) error
   351  	// Run is called once, after Init(..), for the worker to perform its
   352  	// work. The provided context should control the lifetime of the work
   353  	// being performed.
   354  	Run(ctx context.Context) error
   355  }
   356  
   357  // TunnelWorker is the Worker protocol implementation used for tunnel mode.
   358  type TunnelWorker struct {
   359  	embeddedServerEntryListFilename string
   360  	embeddedServerListWaitGroup     *sync.WaitGroup
   361  	controller                      *psiphon.Controller
   362  }
   363  
   364  // Init implements the Worker interface.
   365  func (w *TunnelWorker) Init(ctx context.Context, config *psiphon.Config) error {
   366  
   367  	// Initialize data store
   368  
   369  	err := psiphon.OpenDataStore(config)
   370  	if err != nil {
   371  		psiphon.NoticeError("error initializing datastore: %s", err)
   372  		os.Exit(1)
   373  	}
   374  
   375  	// If specified, the embedded server list is loaded and stored. When there
   376  	// are no server candidates at all, we wait for this import to complete
   377  	// before starting the Psiphon controller. Otherwise, we import while
   378  	// concurrently starting the controller to minimize delay before attempting
   379  	// to connect to existing candidate servers.
   380  	//
   381  	// If the import fails, an error notice is emitted, but the controller is
   382  	// still started: either existing candidate servers may suffice, or the
   383  	// remote server list fetch may obtain candidate servers.
   384  	//
   385  	// The import will be interrupted if it's still running when the controller
   386  	// is stopped.
   387  	if w.embeddedServerEntryListFilename != "" {
   388  		w.embeddedServerListWaitGroup = new(sync.WaitGroup)
   389  		w.embeddedServerListWaitGroup.Add(1)
   390  		go func() {
   391  			defer w.embeddedServerListWaitGroup.Done()
   392  
   393  			err := psiphon.ImportEmbeddedServerEntries(
   394  				ctx,
   395  				config,
   396  				w.embeddedServerEntryListFilename,
   397  				"")
   398  
   399  			if err != nil {
   400  				psiphon.NoticeError("error importing embedded server entry list: %s", err)
   401  				return
   402  			}
   403  		}()
   404  
   405  		if !psiphon.HasServerEntries() {
   406  			psiphon.NoticeInfo("awaiting embedded server entry list import")
   407  			w.embeddedServerListWaitGroup.Wait()
   408  		}
   409  	}
   410  
   411  	controller, err := psiphon.NewController(config)
   412  	if err != nil {
   413  		psiphon.NoticeError("error creating controller: %s", err)
   414  		return errors.Trace(err)
   415  	}
   416  	w.controller = controller
   417  
   418  	return nil
   419  }
   420  
   421  // Run implements the Worker interface.
   422  func (w *TunnelWorker) Run(ctx context.Context) error {
   423  	defer psiphon.CloseDataStore()
   424  	if w.embeddedServerListWaitGroup != nil {
   425  		defer w.embeddedServerListWaitGroup.Wait()
   426  	}
   427  
   428  	w.controller.Run(ctx)
   429  	return nil
   430  }
   431  
   432  // FeedbackWorker is the Worker protocol implementation used for feedback
   433  // upload mode.
   434  type FeedbackWorker struct {
   435  	config             *psiphon.Config
   436  	feedbackUploadPath string
   437  }
   438  
   439  // Init implements the Worker interface.
   440  func (f *FeedbackWorker) Init(ctx context.Context, config *psiphon.Config) error {
   441  
   442  	// The datastore is not opened here, with psiphon.OpenDatastore,
   443  	// because it is opened/closed transiently in the psiphon.SendFeedback
   444  	// operation. We do not want to contest database access incase another
   445  	// process needs to use the database. E.g. a process running in tunnel
   446  	// mode, which will fail if it cannot aquire a lock on the database
   447  	// within a short period of time.
   448  
   449  	f.config = config
   450  
   451  	return nil
   452  }
   453  
   454  // Run implements the Worker interface.
   455  func (f *FeedbackWorker) Run(ctx context.Context) error {
   456  
   457  	// TODO: cancel blocking read when worker context cancelled?
   458  	diagnostics, err := ioutil.ReadAll(os.Stdin)
   459  	if err != nil {
   460  		return errors.TraceMsg(err, "FeedbackUpload: read stdin failed")
   461  	}
   462  
   463  	if len(diagnostics) == 0 {
   464  		return errors.TraceNew("FeedbackUpload: error zero bytes of diagnostics read from stdin")
   465  	}
   466  
   467  	err = psiphon.SendFeedback(ctx, f.config, string(diagnostics), f.feedbackUploadPath)
   468  	if err != nil {
   469  		return errors.TraceMsg(err, "FeedbackUpload: upload failed")
   470  	}
   471  
   472  	psiphon.NoticeInfo("FeedbackUpload: upload succeeded")
   473  
   474  	return nil
   475  }