Bladeren bron

Add feedback upload mode to ConsoleClient

ConsoleClient now can be run in feedback upload mode which facilitates
the encryption and upload of diagnostic data. Feedback upload mode is
selected by providing the --feedbackUpload command line flag.
mirokuratczyk 5 jaren geleden
bovenliggende
commit
49c2100644
1 gewijzigde bestanden met toevoegingen van 166 en 63 verwijderingen
  1. 166 63
      ConsoleClient/main.go

+ 166 - 63
ConsoleClient/main.go

@@ -64,6 +64,20 @@ func main() {
 	flag.BoolVar(&versionDetails, "version", false, "print build information and exit")
 	flag.BoolVar(&versionDetails, "v", false, "print build information and exit")
 
+	var feedbackUpload bool
+	flag.BoolVar(&feedbackUpload, "feedbackUpload", false,
+		"Run in feedback upload mode to send a feedback package to Psiphon Inc.\n"+
+			"The feedback package will be read as a UTF-8 encoded string from stdin.\n"+
+			"Informational notices will be written to stdout. If the upload succeeds,\n"+
+			"the process will exit with status code 0; otherwise, the process will\n"+
+			"exit with status code 1. A feedback compatible config must be specified\n"+
+			"with the \"-config\" flag. Config must be provided by Psiphon Inc.")
+
+	var feedbackUploadPath string
+	flag.StringVar(&feedbackUploadPath, "feedbackUploadPath", "",
+		"The path at which to upload the feedback package when the \"-feedbackUpload\"\n"+
+			"flag is provided. Must be provided by Psiphon Inc.")
+
 	var tunDevice, tunBindInterface, tunPrimaryDNS, tunSecondaryDNS string
 	if tun.IsSupported() {
 
@@ -221,79 +235,44 @@ func main() {
 
 	psiphon.NoticeBuildInfo()
 
-	// Initialize data store
-
-	err = psiphon.OpenDataStore(config)
-	if err != nil {
-		psiphon.NoticeError("error initializing datastore: %s", err)
-		os.Exit(1)
-	}
-	defer psiphon.CloseDataStore()
-
-	// Handle optional embedded server list file parameter
-	// If specified, the embedded server list is loaded and stored. When there
-	// are no server candidates at all, we wait for this import to complete
-	// before starting the Psiphon controller. Otherwise, we import while
-	// concurrently starting the controller to minimize delay before attempting
-	// to connect to existing candidate servers.
-	// If the import fails, an error notice is emitted, but the controller is
-	// still started: either existing candidate servers may suffice, or the
-	// remote server list fetch may obtain candidate servers.
-	if embeddedServerEntryListFilename != "" {
-		embeddedServerListWaitGroup := new(sync.WaitGroup)
-		embeddedServerListWaitGroup.Add(1)
-		go func() {
-			defer embeddedServerListWaitGroup.Done()
-			serverEntryList, err := ioutil.ReadFile(embeddedServerEntryListFilename)
-			if err != nil {
-				psiphon.NoticeError("error loading embedded server entry list file: %s", err)
-				return
-			}
-			// TODO: stream embedded server list data? also, the cast makes an unnecessary copy of a large buffer?
-			serverEntries, err := protocol.DecodeServerEntryList(
-				string(serverEntryList),
-				common.GetCurrentTimestamp(),
-				protocol.SERVER_ENTRY_SOURCE_EMBEDDED)
-			if err != nil {
-				psiphon.NoticeError("error decoding embedded server entry list file: %s", err)
-				return
-			}
-			// Since embedded server list entries may become stale, they will not
-			// overwrite existing stored entries for the same server.
-			err = psiphon.StoreServerEntries(config, serverEntries, false)
-			if err != nil {
-				psiphon.NoticeError("error storing embedded server entry list data: %s", err)
-				return
-			}
-		}()
+	var worker Worker
 
-		if psiphon.CountServerEntries() == 0 {
-			embeddedServerListWaitGroup.Wait()
-		} else {
-			defer embeddedServerListWaitGroup.Wait()
+	if feedbackUpload {
+		// Feedback upload mode
+		worker = &FeedbackWorker{
+			feedbackUploadPath: feedbackUploadPath,
+		}
+	} else {
+		// Tunnel mode
+		worker = &TunnelWorker{
+			embeddedServerEntryListFilename: embeddedServerEntryListFilename,
 		}
 	}
 
-	// Run Psiphon
-
-	controller, err := psiphon.NewController(config)
+	err = worker.Init(config)
 	if err != nil {
-		psiphon.NoticeError("error creating controller: %s", err)
+		psiphon.NoticeError("error in init: %s", err)
 		os.Exit(1)
 	}
 
-	controllerCtx, stopController := context.WithCancel(context.Background())
-	defer stopController()
+	workCtx, stopWork := context.WithCancel(context.Background())
+	defer stopWork()
 
-	controllerWaitGroup := new(sync.WaitGroup)
-	controllerWaitGroup.Add(1)
+	workWaitGroup := new(sync.WaitGroup)
+	workWaitGroup.Add(1)
 	go func() {
-		defer controllerWaitGroup.Done()
-		controller.Run(controllerCtx)
+		defer workWaitGroup.Done()
+
+		err := worker.Run(workCtx)
+		if err != nil {
+			psiphon.NoticeError("%s", err)
+			stopWork()
+			os.Exit(1)
+		}
 
 		// Signal the <-controllerCtx.Done() case below. If the <-systemStopSignal
 		// case already called stopController, this is a noop.
-		stopController()
+		stopWork()
 	}()
 
 	systemStopSignal := make(chan os.Signal, 1)
@@ -317,10 +296,10 @@ func main() {
 				profileSampleDurationSeconds)
 		case <-systemStopSignal:
 			psiphon.NoticeInfo("shutdown by system")
-			stopController()
-			controllerWaitGroup.Wait()
+			stopWork()
+			workWaitGroup.Wait()
 			exit = true
-		case <-controllerCtx.Done():
+		case <-workCtx.Done():
 			psiphon.NoticeInfo("shutdown by controller")
 			exit = true
 		}
@@ -369,3 +348,127 @@ func (p *tunProvider) GetPrimaryDnsServer() string {
 func (p *tunProvider) GetSecondaryDnsServer() string {
 	return p.secondaryDNS
 }
+
+// Worker creates a protocol around the different run modes provided by the
+// compiled executable.
+type Worker interface {
+	// Init is called once for the worker to perform any initialization.
+	Init(config *psiphon.Config) error
+	// Run is called once, after Init(..), for the worker to perform its
+	// work. The provided context should control the lifetime of the work
+	// being performed.
+	Run(ctx context.Context) error
+}
+
+// TunnelWorker is the Worker protocol implementation used for tunnel mode.
+type TunnelWorker struct {
+	embeddedServerEntryListFilename string
+	controller                      *psiphon.Controller
+}
+
+// Init implements the Worker interface.
+func (w *TunnelWorker) Init(config *psiphon.Config) error {
+
+	// Initialize data store
+
+	err := psiphon.OpenDataStore(config)
+	if err != nil {
+		psiphon.NoticeError("error initializing datastore: %s", err)
+		os.Exit(1)
+	}
+
+	// Handle optional embedded server list file parameter
+	// If specified, the embedded server list is loaded and stored. When there
+	// are no server candidates at all, we wait for this import to complete
+	// before starting the Psiphon controller. Otherwise, we import while
+	// concurrently starting the controller to minimize delay before attempting
+	// to connect to existing candidate servers.
+	// If the import fails, an error notice is emitted, but the controller is
+	// still started: either existing candidate servers may suffice, or the
+	// remote server list fetch may obtain candidate servers.
+	if w.embeddedServerEntryListFilename != "" {
+		embeddedServerListWaitGroup := new(sync.WaitGroup)
+		embeddedServerListWaitGroup.Add(1)
+		go func() {
+			defer embeddedServerListWaitGroup.Done()
+			serverEntryList, err := ioutil.ReadFile(w.embeddedServerEntryListFilename)
+			if err != nil {
+				psiphon.NoticeError("error loading embedded server entry list file: %s", err)
+				return
+			}
+			// TODO: stream embedded server list data? also, the cast makes an unnecessary copy of a large buffer?
+			serverEntries, err := protocol.DecodeServerEntryList(
+				string(serverEntryList),
+				common.GetCurrentTimestamp(),
+				protocol.SERVER_ENTRY_SOURCE_EMBEDDED)
+			if err != nil {
+				psiphon.NoticeError("error decoding embedded server entry list file: %s", err)
+				return
+			}
+			// Since embedded server list entries may become stale, they will not
+			// overwrite existing stored entries for the same server.
+			err = psiphon.StoreServerEntries(config, serverEntries, false)
+			if err != nil {
+				psiphon.NoticeError("error storing embedded server entry list data: %s", err)
+				return
+			}
+		}()
+
+		if psiphon.CountServerEntries() == 0 {
+			embeddedServerListWaitGroup.Wait()
+		} else {
+			defer embeddedServerListWaitGroup.Wait()
+		}
+	}
+
+	controller, err := psiphon.NewController(config)
+	if err != nil {
+		psiphon.NoticeError("error creating controller: %s", err)
+		return errors.Trace(err)
+	}
+	w.controller = controller
+
+	return nil
+}
+
+// Run implements the Worker interface.
+func (w *TunnelWorker) Run(ctx context.Context) error {
+	defer psiphon.CloseDataStore()
+
+	w.controller.Run(ctx)
+	return nil
+}
+
+// FeedbackWorker is the Worker protocol implementation used for feedback
+// upload mode.
+type FeedbackWorker struct {
+	config             *psiphon.Config
+	feedbackUploadPath string
+}
+
+// Init implements the Worker interface.
+func (f *FeedbackWorker) Init(config *psiphon.Config) error {
+
+	f.config = config
+
+	return nil
+}
+
+// Run implements the Worker interface.
+func (f *FeedbackWorker) Run(ctx context.Context) error {
+
+	// TODO: cancel blocking read when worker context cancelled?
+	diagnostics, err := ioutil.ReadAll(os.Stdin)
+	if err != nil {
+		return errors.Trace(err)
+	}
+
+	err = psiphon.SendFeedback(ctx, f.config, string(diagnostics), f.feedbackUploadPath)
+	if err != nil {
+		return errors.TraceMsg(err, "FeedbackUpload: upload failed")
+	}
+
+	psiphon.NoticeInfo("FeedbackUpload: upload succeeded")
+
+	return nil
+}