clientlib_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. /*
  2. * Copyright (c) 2018, 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 clientlib
  20. import (
  21. "context"
  22. "encoding/json"
  23. "errors"
  24. "os"
  25. "strings"
  26. "testing"
  27. "time"
  28. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
  29. )
  30. func setupConfig(t *testing.T, disableFetcher bool) []byte {
  31. configJSON, err := os.ReadFile("../../psiphon/controller_test.config")
  32. if err != nil {
  33. // What to do if config file is not present?
  34. t.Skipf("error loading configuration file: %s", err)
  35. }
  36. var config map[string]interface{}
  37. err = json.Unmarshal(configJSON, &config)
  38. if err != nil {
  39. t.Fatalf("json.Unmarshal failed: %v", err)
  40. }
  41. if disableFetcher {
  42. config["DisableRemoteServerListFetcher"] = true
  43. }
  44. configJSON, err = json.Marshal(config)
  45. if err != nil {
  46. t.Fatalf("json.Marshal failed: %v", err)
  47. }
  48. return configJSON
  49. }
  50. func TestStartTunnel(t *testing.T) {
  51. // TODO: More comprehensive tests. This is only a smoke test.
  52. configJSON := setupConfig(t, false)
  53. configJSONNoFetcher := setupConfig(t, true)
  54. clientPlatform := "clientlib_test.go"
  55. networkID := "UNKNOWN"
  56. timeout := 60
  57. quickTimeout := 1
  58. trueVal := true
  59. // Initialize a fresh datastore and create a modified config which cannot
  60. // connect without known servers, to be used in timeout cases.
  61. testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
  62. if err != nil {
  63. t.Fatalf("ioutil.TempDir failed: %v", err)
  64. }
  65. defer os.RemoveAll(testDataDirName)
  66. paramsDeltaErr := func(err error) bool {
  67. return strings.Contains(err.Error(), "SetParameters failed for delta")
  68. }
  69. timeoutErr := func(err error) bool {
  70. return errors.Is(err, ErrTimeout)
  71. }
  72. type args struct {
  73. ctxTimeout time.Duration
  74. configJSON []byte
  75. embeddedServerEntryList string
  76. params Parameters
  77. paramsDelta ParametersDelta
  78. noticeReceiver func(NoticeEvent)
  79. }
  80. tests := []struct {
  81. name string
  82. args args
  83. wantTunnel bool
  84. expectedErr func(error) bool
  85. }{
  86. {
  87. name: "Failure: context timeout",
  88. args: args{
  89. ctxTimeout: 10 * time.Millisecond,
  90. configJSON: configJSONNoFetcher,
  91. embeddedServerEntryList: "",
  92. params: Parameters{
  93. DataRootDirectory: &testDataDirName,
  94. ClientPlatform: &clientPlatform,
  95. NetworkID: &networkID,
  96. EstablishTunnelTimeoutSeconds: &timeout,
  97. },
  98. paramsDelta: nil,
  99. noticeReceiver: nil,
  100. },
  101. wantTunnel: false,
  102. expectedErr: timeoutErr,
  103. },
  104. {
  105. name: "Failure: config timeout",
  106. args: args{
  107. ctxTimeout: 0,
  108. configJSON: configJSONNoFetcher,
  109. embeddedServerEntryList: "",
  110. params: Parameters{
  111. DataRootDirectory: &testDataDirName,
  112. ClientPlatform: &clientPlatform,
  113. NetworkID: &networkID,
  114. EstablishTunnelTimeoutSeconds: &quickTimeout,
  115. },
  116. paramsDelta: nil,
  117. noticeReceiver: nil,
  118. },
  119. wantTunnel: false,
  120. expectedErr: timeoutErr,
  121. },
  122. {
  123. name: "Success: simple",
  124. args: args{
  125. ctxTimeout: 0,
  126. configJSON: configJSON,
  127. embeddedServerEntryList: "",
  128. params: Parameters{
  129. DataRootDirectory: &testDataDirName,
  130. ClientPlatform: &clientPlatform,
  131. NetworkID: &networkID,
  132. EstablishTunnelTimeoutSeconds: &timeout,
  133. },
  134. paramsDelta: nil,
  135. noticeReceiver: nil,
  136. },
  137. wantTunnel: true,
  138. expectedErr: nil,
  139. },
  140. {
  141. name: "Success: disable SOCKS proxy",
  142. args: args{
  143. ctxTimeout: 0,
  144. configJSON: configJSON,
  145. embeddedServerEntryList: "",
  146. params: Parameters{
  147. DataRootDirectory: &testDataDirName,
  148. ClientPlatform: &clientPlatform,
  149. NetworkID: &networkID,
  150. EstablishTunnelTimeoutSeconds: &timeout,
  151. DisableLocalSocksProxy: &trueVal,
  152. },
  153. paramsDelta: nil,
  154. noticeReceiver: nil,
  155. },
  156. wantTunnel: true,
  157. expectedErr: nil,
  158. },
  159. {
  160. name: "Success: disable HTTP proxy",
  161. args: args{
  162. ctxTimeout: 0,
  163. configJSON: configJSON,
  164. embeddedServerEntryList: "",
  165. params: Parameters{
  166. DataRootDirectory: &testDataDirName,
  167. ClientPlatform: &clientPlatform,
  168. NetworkID: &networkID,
  169. EstablishTunnelTimeoutSeconds: &timeout,
  170. DisableLocalHTTPProxy: &trueVal,
  171. },
  172. paramsDelta: nil,
  173. noticeReceiver: nil,
  174. },
  175. wantTunnel: true,
  176. expectedErr: nil,
  177. },
  178. {
  179. name: "Success: disable SOCKS and HTTP proxies",
  180. args: args{
  181. ctxTimeout: 0,
  182. configJSON: configJSON,
  183. embeddedServerEntryList: "",
  184. params: Parameters{
  185. DataRootDirectory: &testDataDirName,
  186. ClientPlatform: &clientPlatform,
  187. NetworkID: &networkID,
  188. EstablishTunnelTimeoutSeconds: &timeout,
  189. DisableLocalSocksProxy: &trueVal,
  190. DisableLocalHTTPProxy: &trueVal,
  191. },
  192. paramsDelta: nil,
  193. noticeReceiver: nil,
  194. },
  195. wantTunnel: true,
  196. expectedErr: nil,
  197. },
  198. {
  199. name: "Success: good ParametersDelta",
  200. args: args{
  201. ctxTimeout: 0,
  202. configJSON: configJSON,
  203. embeddedServerEntryList: "",
  204. params: Parameters{
  205. DataRootDirectory: &testDataDirName,
  206. ClientPlatform: &clientPlatform,
  207. NetworkID: &networkID,
  208. EstablishTunnelTimeoutSeconds: &timeout,
  209. },
  210. paramsDelta: ParametersDelta{"NetworkLatencyMultiplierMin": 1},
  211. noticeReceiver: nil,
  212. },
  213. wantTunnel: true,
  214. expectedErr: nil,
  215. },
  216. {
  217. name: "Failure: bad ParametersDelta",
  218. args: args{
  219. ctxTimeout: 0,
  220. configJSON: configJSON,
  221. embeddedServerEntryList: "",
  222. params: Parameters{
  223. DataRootDirectory: &testDataDirName,
  224. ClientPlatform: &clientPlatform,
  225. NetworkID: &networkID,
  226. EstablishTunnelTimeoutSeconds: &timeout,
  227. },
  228. paramsDelta: ParametersDelta{"invalidParam": 1},
  229. noticeReceiver: nil,
  230. },
  231. wantTunnel: false,
  232. expectedErr: paramsDeltaErr,
  233. },
  234. }
  235. for _, tt := range tests {
  236. t.Run(tt.name, func(t *testing.T) {
  237. ctx := context.Background()
  238. var cancelFunc context.CancelFunc
  239. if tt.args.ctxTimeout > 0 {
  240. ctx, cancelFunc = context.WithTimeout(ctx, tt.args.ctxTimeout)
  241. }
  242. tunnel, err := StartTunnel(
  243. ctx,
  244. tt.args.configJSON,
  245. tt.args.embeddedServerEntryList,
  246. tt.args.params,
  247. tt.args.paramsDelta,
  248. tt.args.noticeReceiver)
  249. gotTunnel := (tunnel != nil)
  250. if cancelFunc != nil {
  251. cancelFunc()
  252. }
  253. if tunnel != nil {
  254. tunnel.Stop()
  255. }
  256. if gotTunnel != tt.wantTunnel {
  257. t.Errorf("StartTunnel() gotTunnel = %v, wantTunnel %v", err, tt.wantTunnel)
  258. }
  259. if tt.expectedErr == nil {
  260. if err != nil {
  261. t.Fatalf("StartTunnel() returned unexpected error: %v", err)
  262. }
  263. } else if !tt.expectedErr(err) {
  264. t.Fatalf("StartTunnel() error: %v", err)
  265. return
  266. }
  267. if err != nil {
  268. return
  269. }
  270. if tunnel == nil {
  271. return
  272. }
  273. if tt.args.params.DisableLocalSocksProxy != nil && *tt.args.params.DisableLocalSocksProxy {
  274. if tunnel.SOCKSProxyPort != 0 {
  275. t.Fatalf("should not have started SOCKS proxy")
  276. }
  277. } else {
  278. if tunnel.SOCKSProxyPort == 0 {
  279. t.Fatalf("failed to start SOCKS proxy")
  280. }
  281. }
  282. if tt.args.params.DisableLocalHTTPProxy != nil && *tt.args.params.DisableLocalHTTPProxy {
  283. if tunnel.HTTPProxyPort != 0 {
  284. t.Fatalf("should not have started HTTP proxy")
  285. }
  286. } else {
  287. if tunnel.HTTPProxyPort == 0 {
  288. t.Fatalf("failed to start HTTP proxy")
  289. }
  290. }
  291. })
  292. }
  293. }
  294. func TestMultipleStartTunnel(t *testing.T) {
  295. configJSON := setupConfig(t, false)
  296. testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
  297. if err != nil {
  298. t.Fatalf("ioutil.TempDir failed: %v", err)
  299. }
  300. defer os.RemoveAll(testDataDirName)
  301. ctx := context.Background()
  302. tunnel1, err := StartTunnel(
  303. ctx,
  304. configJSON,
  305. "",
  306. Parameters{DataRootDirectory: &testDataDirName},
  307. nil,
  308. nil)
  309. if err != nil {
  310. t.Fatalf("first StartTunnel() error = %v", err)
  311. }
  312. // We have not stopped the tunnel, so a second StartTunnel() should fail
  313. _, err = StartTunnel(
  314. ctx,
  315. configJSON,
  316. "",
  317. Parameters{DataRootDirectory: &testDataDirName},
  318. nil,
  319. nil)
  320. if err != errMultipleStart {
  321. t.Fatalf("second StartTunnel() should have failed with errMultipleStart; got %v", err)
  322. }
  323. // Stop the tunnel and try again
  324. tunnel1.Stop()
  325. tunnel3, err := StartTunnel(
  326. ctx,
  327. configJSON,
  328. "",
  329. Parameters{DataRootDirectory: &testDataDirName},
  330. nil,
  331. nil)
  332. if err != nil {
  333. t.Fatalf("third StartTunnel() error = %v", err)
  334. }
  335. // Stop the tunnel so it doesn't interfere with other tests
  336. tunnel3.Stop()
  337. }
  338. func TestPsiphonTunnel_Dial(t *testing.T) {
  339. configJSON := setupConfig(t, false)
  340. trueVal := true
  341. testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
  342. if err != nil {
  343. t.Fatalf("ioutil.TempDir failed: %v", err)
  344. }
  345. defer os.RemoveAll(testDataDirName)
  346. type args struct {
  347. remoteAddr string
  348. }
  349. tests := []struct {
  350. name string
  351. args args
  352. wantErr bool
  353. tunnelStopped bool
  354. }{
  355. {
  356. name: "Success: psiphon.ca",
  357. args: args{remoteAddr: "psiphon.ca:443"},
  358. wantErr: false,
  359. },
  360. {
  361. name: "Failure: invalid address",
  362. args: args{remoteAddr: "psiphon.ca:99999"},
  363. wantErr: true,
  364. },
  365. {
  366. name: "Failure: tunnel not started",
  367. args: args{remoteAddr: "psiphon.ca:443"},
  368. wantErr: true,
  369. tunnelStopped: true,
  370. },
  371. }
  372. for _, tt := range tests {
  373. t.Run(tt.name, func(t *testing.T) {
  374. tunnel, err := StartTunnel(
  375. context.Background(),
  376. configJSON,
  377. "",
  378. Parameters{
  379. DataRootDirectory: &testDataDirName,
  380. // Don't need local proxies for dial tests
  381. // (and this is likely the configuration that will be used by consumers of the library who utilitize Dial).
  382. DisableLocalSocksProxy: &trueVal,
  383. DisableLocalHTTPProxy: &trueVal,
  384. },
  385. nil,
  386. nil)
  387. if err != nil {
  388. t.Fatalf("StartTunnel() error = %v", err)
  389. }
  390. defer tunnel.Stop()
  391. if tt.tunnelStopped {
  392. tunnel.Stop()
  393. }
  394. conn, err := tunnel.Dial(tt.args.remoteAddr)
  395. if (err != nil) != tt.wantErr {
  396. t.Fatalf("PsiphonTunnel.Dial() error = %v, wantErr %v", err, tt.wantErr)
  397. return
  398. }
  399. if tt.wantErr != (conn == nil) {
  400. t.Fatalf("PsiphonTunnel.Dial() conn = %v, wantConn %v", conn, !tt.wantErr)
  401. }
  402. })
  403. }
  404. }
  405. // We had a problem where config-related notices were being printed to stderr before we
  406. // set the NoticeWriter. We want to make sure that no longer happens.
  407. func TestStartTunnelNoOutput(t *testing.T) {
  408. // Before starting the tunnel, set up a notice receiver. If it receives anything at
  409. // all, that means that it would have been printed to stderr.
  410. err := psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver(
  411. func(notice []byte) {
  412. t.Fatalf("Received notice: %v", string(notice))
  413. }))
  414. if err != nil {
  415. t.Fatalf("psiphon.SetNoticeWriter failed: %v", err)
  416. }
  417. defer psiphon.ResetNoticeWriter()
  418. configJSON := setupConfig(t, false)
  419. testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
  420. if err != nil {
  421. t.Fatalf("ioutil.TempDir failed: %v", err)
  422. }
  423. defer os.RemoveAll(testDataDirName)
  424. psiphon.ResetNoticeWriter()
  425. ctx := context.Background()
  426. tunnel, err := StartTunnel(
  427. ctx,
  428. configJSON,
  429. "",
  430. Parameters{DataRootDirectory: &testDataDirName},
  431. nil,
  432. nil)
  433. if err != nil {
  434. t.Fatalf("StartTunnel() error = %v", err)
  435. }
  436. tunnel.Stop()
  437. }
  438. // We had a problem where a very early error could result in `started` being set to true
  439. // and not be set back to false, preventing StartTunnel from being re-callable.
  440. func TestStartTunnelReentry(t *testing.T) {
  441. testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
  442. if err != nil {
  443. t.Fatalf("ioutil.TempDir failed: %v", err)
  444. }
  445. defer os.RemoveAll(testDataDirName)
  446. configJSON := []byte("BAD CONFIG JSON")
  447. ctx := context.Background()
  448. _, err = StartTunnel(
  449. ctx,
  450. configJSON,
  451. "",
  452. Parameters{DataRootDirectory: &testDataDirName},
  453. nil,
  454. nil)
  455. if err == nil {
  456. t.Fatalf("expected config error")
  457. }
  458. // Call again with a good config. Should work.
  459. configJSON = setupConfig(t, false)
  460. tunnel, err := StartTunnel(
  461. ctx,
  462. configJSON,
  463. "",
  464. Parameters{DataRootDirectory: &testDataDirName},
  465. nil,
  466. nil)
  467. if err != nil {
  468. t.Fatalf("StartTunnel() error = %v", err)
  469. }
  470. tunnel.Stop()
  471. }