main.go 14 KB

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