clientlib_test.go 13 KB

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