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