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(¬iceFilename, "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 }