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