/* * Copyright (c) 2015, Psiphon Inc. * All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package main import ( "context" "flag" "fmt" "io" "io/ioutil" "os" "os/signal" "strings" "sync" "syscall" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/buildinfo" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/tun" ) func main() { // Define command-line parameters var configFilename string flag.StringVar(&configFilename, "config", "", "configuration input file") var dataRootDirectory string flag.StringVar(&dataRootDirectory, "dataRootDirectory", "", "directory where persistent files will be stored") var embeddedServerEntryListFilename string flag.StringVar(&embeddedServerEntryListFilename, "serverList", "", "embedded server entry list input file") var formatNotices bool flag.BoolVar(&formatNotices, "formatNotices", false, "emit notices in human-readable format") var interfaceName string flag.StringVar(&interfaceName, "listenInterface", "", "bind local proxies to specified interface") var versionDetails bool 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, tunDNSServers string if tun.IsSupported() { // When tunDevice is specified, a packet tunnel is run and packets are relayed between // the specified tun device and the server. // // The tun device is expected to exist and should be configured with an IP address and // routing. // // The tunBindInterface/tunPrimaryDNS/tunSecondaryDNS parameters are used to bypass any // tun device routing when connecting to Psiphon servers. // // For transparent tunneled DNS, set the host or DNS clients to use the address specfied // in tun.GetTransparentDNSResolverIPv4Address(). // // Packet tunnel mode is supported only on certains platforms. flag.StringVar(&tunDevice, "tunDevice", "", "run packet tunnel for specified tun device") flag.StringVar(&tunBindInterface, "tunBindInterface", tun.DEFAULT_PUBLIC_INTERFACE_NAME, "bypass tun device via specified interface") flag.StringVar(&tunDNSServers, "tunDNSServers", "8.8.8.8,8.8.4.4", "Comma-delimited list of tun bypass DNS server IP addresses") } var noticeFilename string flag.StringVar(¬iceFilename, "notices", "", "notices output file (defaults to stderr)") var useNoticeFiles bool useNoticeFilesUsage := fmt.Sprintf("output homepage notices and rotating notices to /%s and /%s respectively", psiphon.HomepageFilename, psiphon.NoticesFilename) flag.BoolVar(&useNoticeFiles, "useNoticeFiles", false, useNoticeFilesUsage) var rotatingFileSize int flag.IntVar(&rotatingFileSize, "rotatingFileSize", 1<<20, "rotating notices file size") var rotatingSyncFrequency int flag.IntVar(&rotatingSyncFrequency, "rotatingSyncFrequency", 100, "rotating notices file sync frequency") flag.Parse() if versionDetails { b := buildinfo.GetBuildInfo() fmt.Printf( "Psiphon Console Client\n Build Date: %s\n Built With: %s\n Repository: %s\n Revision: %s\n", b.BuildDate, b.GoVersion, b.BuildRepo, b.BuildRev) os.Exit(0) } // Initialize notice output var noticeWriter io.Writer noticeWriter = os.Stderr if noticeFilename != "" { noticeFile, err := os.OpenFile(noticeFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) if err != nil { fmt.Printf("error opening notice file: %s\n", err) os.Exit(1) } defer noticeFile.Close() noticeWriter = noticeFile } if formatNotices { noticeWriter = psiphon.NewNoticeConsoleRewriter(noticeWriter) } psiphon.SetNoticeWriter(noticeWriter) // Handle required config file parameter // EmitDiagnosticNotices is set by LoadConfig; force to true // and emit diagnostics when LoadConfig-related errors occur. if configFilename == "" { psiphon.SetEmitDiagnosticNotices(true, false) psiphon.NoticeError("configuration file is required") os.Exit(1) } configFileContents, err := ioutil.ReadFile(configFilename) if err != nil { psiphon.SetEmitDiagnosticNotices(true, false) psiphon.NoticeError("error loading configuration file: %s", err) os.Exit(1) } config, err := psiphon.LoadConfig(configFileContents) if err != nil { psiphon.SetEmitDiagnosticNotices(true, false) psiphon.NoticeError("error processing configuration file: %s", err) os.Exit(1) } // Set data root directory if dataRootDirectory != "" { config.DataRootDirectory = dataRootDirectory } if interfaceName != "" { config.ListenInterface = interfaceName } // Configure notice files if useNoticeFiles { config.UseNoticeFiles = &psiphon.UseNoticeFiles{ RotatingFileSize: rotatingFileSize, RotatingSyncFrequency: rotatingSyncFrequency, } } // Configure packet tunnel, including updating the config. if tun.IsSupported() && tunDevice != "" { tunDeviceFile, err := configurePacketTunnel( config, tunDevice, tunBindInterface, strings.Split(tunDNSServers, ",")) if err != nil { psiphon.SetEmitDiagnosticNotices(true, false) psiphon.NoticeError("error configuring packet tunnel: %s", err) os.Exit(1) } defer tunDeviceFile.Close() } // All config fields should be set before calling Commit. err = config.Commit(true) if err != nil { psiphon.SetEmitDiagnosticNotices(true, false) psiphon.NoticeError("error loading configuration file: %s", err) os.Exit(1) } // BuildInfo is a diagnostic notice, so emit only after config.Commit // sets EmitDiagnosticNotices. psiphon.NoticeBuildInfo() var worker Worker if feedbackUpload { // Feedback upload mode worker = &FeedbackWorker{ feedbackUploadPath: feedbackUploadPath, } } else { // Tunnel mode worker = &TunnelWorker{ embeddedServerEntryListFilename: embeddedServerEntryListFilename, } } workCtx, stopWork := context.WithCancel(context.Background()) defer stopWork() err = worker.Init(workCtx, config) if err != nil { psiphon.NoticeError("error in init: %s", err) os.Exit(1) } workWaitGroup := new(sync.WaitGroup) workWaitGroup.Add(1) go func() { 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. stopWork() }() systemStopSignal := make(chan os.Signal, 1) signal.Notify(systemStopSignal, os.Interrupt, syscall.SIGTERM) // writeProfilesSignal is nil and non-functional on Windows writeProfilesSignal := makeSIGUSR2Channel() // Wait for an OS signal or a Run stop signal, then stop Psiphon and exit for exit := false; !exit; { select { case <-writeProfilesSignal: psiphon.NoticeInfo("write profiles") profileSampleDurationSeconds := 5 common.WriteRuntimeProfiles( psiphon.NoticeCommonLogger(), config.DataRootDirectory, "", profileSampleDurationSeconds, profileSampleDurationSeconds) case <-systemStopSignal: psiphon.NoticeInfo("shutdown by system") stopWork() workWaitGroup.Wait() exit = true case <-workCtx.Done(): psiphon.NoticeInfo("shutdown by controller") exit = true } } } func configurePacketTunnel( config *psiphon.Config, tunDevice string, tunBindInterface string, tunDNSServers []string) (*os.File, error) { file, _, err := tun.OpenTunDevice(tunDevice) if err != nil { return nil, errors.Trace(err) } provider := &tunProvider{ bindInterface: tunBindInterface, dnsServers: tunDNSServers, } config.PacketTunnelTunFileDescriptor = int(file.Fd()) config.DeviceBinder = provider config.DNSServerGetter = provider return file, nil } type tunProvider struct { bindInterface string dnsServers []string } // BindToDevice implements the psiphon.DeviceBinder interface. func (p *tunProvider) BindToDevice(fileDescriptor int) (string, error) { return p.bindInterface, tun.BindToDevice(fileDescriptor, p.bindInterface) } // GetDNSServers implements the psiphon.DNSServerGetter interface. func (p *tunProvider) GetDNSServers() []string { return p.dnsServers } // 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(ctx context.Context, 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 embeddedServerListWaitGroup *sync.WaitGroup controller *psiphon.Controller } // Init implements the Worker interface. func (w *TunnelWorker) Init(ctx context.Context, config *psiphon.Config) error { // Initialize data store err := psiphon.OpenDataStore(config) if err != nil { psiphon.NoticeError("error initializing datastore: %s", err) os.Exit(1) } // 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. // // The import will be interrupted if it's still running when the controller // is stopped. if w.embeddedServerEntryListFilename != "" { w.embeddedServerListWaitGroup = new(sync.WaitGroup) w.embeddedServerListWaitGroup.Add(1) go func() { defer w.embeddedServerListWaitGroup.Done() err := psiphon.ImportEmbeddedServerEntries( ctx, config, w.embeddedServerEntryListFilename, "") if err != nil { psiphon.NoticeError("error importing embedded server entry list: %s", err) return } }() if !psiphon.HasServerEntries() { psiphon.NoticeInfo("awaiting embedded server entry list import") w.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() if w.embeddedServerListWaitGroup != nil { defer w.embeddedServerListWaitGroup.Wait() } 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(ctx context.Context, config *psiphon.Config) error { // The datastore is not opened here, with psiphon.OpenDatastore, // because it is opened/closed transiently in the psiphon.SendFeedback // operation. We do not want to contest database access incase another // process needs to use the database. E.g. a process running in tunnel // mode, which will fail if it cannot aquire a lock on the database // within a short period of time. 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.TraceMsg(err, "FeedbackUpload: read stdin failed") } if len(diagnostics) == 0 { return errors.TraceNew("FeedbackUpload: error zero bytes of diagnostics read from stdin") } 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 }