clientlib_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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. "os"
  24. "testing"
  25. "time"
  26. "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
  27. )
  28. func TestStartTunnel(t *testing.T) {
  29. // TODO: More comprehensive tests. This is only a smoke test.
  30. clientPlatform := "clientlib_test.go"
  31. networkID := "UNKNOWN"
  32. timeout := 60
  33. quickTimeout := 1
  34. trueVal := true
  35. configJSON, err := os.ReadFile("../../psiphon/controller_test.config")
  36. if err != nil {
  37. // Skip, don't fail, if config file is not present
  38. t.Skipf("error loading configuration file: %s", err)
  39. }
  40. // Initialize a fresh datastore and create a modified config which cannot
  41. // connect without known servers, to be used in timeout cases.
  42. testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
  43. if err != nil {
  44. t.Fatalf("ioutil.TempDir failed: %v", err)
  45. }
  46. defer os.RemoveAll(testDataDirName)
  47. var config map[string]interface{}
  48. err = json.Unmarshal(configJSON, &config)
  49. if err != nil {
  50. t.Fatalf("json.Unmarshal failed: %v", err)
  51. }
  52. // Use the legacy encoding to both exercise that case, and facilitate a
  53. // gradual network upgrade to new encoding support.
  54. config["TargetAPIEncoding"] = protocol.PSIPHON_API_ENCODING_JSON
  55. configJSON, err = json.Marshal(config)
  56. if err != nil {
  57. t.Fatalf("json.Marshal failed: %v", err)
  58. }
  59. config["DisableRemoteServerListFetcher"] = true
  60. configJSONNoFetcher, err := json.Marshal(config)
  61. if err != nil {
  62. t.Fatalf("json.Marshal failed: %v", err)
  63. }
  64. type args struct {
  65. ctxTimeout time.Duration
  66. configJSON []byte
  67. embeddedServerEntryList string
  68. params Parameters
  69. paramsDelta ParametersDelta
  70. noticeReceiver func(NoticeEvent)
  71. }
  72. tests := []struct {
  73. name string
  74. args args
  75. wantTunnel bool
  76. expectedErr error
  77. }{
  78. {
  79. name: "Failure: context timeout",
  80. args: args{
  81. ctxTimeout: 10 * time.Millisecond,
  82. configJSON: configJSONNoFetcher,
  83. embeddedServerEntryList: "",
  84. params: Parameters{
  85. DataRootDirectory: &testDataDirName,
  86. ClientPlatform: &clientPlatform,
  87. NetworkID: &networkID,
  88. EstablishTunnelTimeoutSeconds: &timeout,
  89. },
  90. paramsDelta: nil,
  91. noticeReceiver: nil,
  92. },
  93. wantTunnel: false,
  94. expectedErr: ErrTimeout,
  95. },
  96. {
  97. name: "Failure: config timeout",
  98. args: args{
  99. ctxTimeout: 0,
  100. configJSON: configJSONNoFetcher,
  101. embeddedServerEntryList: "",
  102. params: Parameters{
  103. DataRootDirectory: &testDataDirName,
  104. ClientPlatform: &clientPlatform,
  105. NetworkID: &networkID,
  106. EstablishTunnelTimeoutSeconds: &quickTimeout,
  107. },
  108. paramsDelta: nil,
  109. noticeReceiver: nil,
  110. },
  111. wantTunnel: false,
  112. expectedErr: ErrTimeout,
  113. },
  114. {
  115. name: "Success: simple",
  116. args: args{
  117. ctxTimeout: 0,
  118. configJSON: configJSON,
  119. embeddedServerEntryList: "",
  120. params: Parameters{
  121. DataRootDirectory: &testDataDirName,
  122. ClientPlatform: &clientPlatform,
  123. NetworkID: &networkID,
  124. EstablishTunnelTimeoutSeconds: &timeout,
  125. },
  126. paramsDelta: nil,
  127. noticeReceiver: nil,
  128. },
  129. wantTunnel: true,
  130. expectedErr: nil,
  131. },
  132. {
  133. name: "Success: disable SOCKS proxy",
  134. args: args{
  135. ctxTimeout: 0,
  136. configJSON: configJSON,
  137. embeddedServerEntryList: "",
  138. params: Parameters{
  139. DataRootDirectory: &testDataDirName,
  140. ClientPlatform: &clientPlatform,
  141. NetworkID: &networkID,
  142. EstablishTunnelTimeoutSeconds: &timeout,
  143. DisableLocalSocksProxy: &trueVal,
  144. },
  145. paramsDelta: nil,
  146. noticeReceiver: nil,
  147. },
  148. wantTunnel: true,
  149. expectedErr: nil,
  150. },
  151. {
  152. name: "Success: disable HTTP proxy",
  153. args: args{
  154. ctxTimeout: 0,
  155. configJSON: configJSON,
  156. embeddedServerEntryList: "",
  157. params: Parameters{
  158. DataRootDirectory: &testDataDirName,
  159. ClientPlatform: &clientPlatform,
  160. NetworkID: &networkID,
  161. EstablishTunnelTimeoutSeconds: &timeout,
  162. DisableLocalHTTPProxy: &trueVal,
  163. },
  164. paramsDelta: nil,
  165. noticeReceiver: nil,
  166. },
  167. wantTunnel: true,
  168. expectedErr: nil,
  169. },
  170. {
  171. name: "Success: disable SOCKS and HTTP proxies",
  172. args: args{
  173. ctxTimeout: 0,
  174. configJSON: configJSON,
  175. embeddedServerEntryList: "",
  176. params: Parameters{
  177. DataRootDirectory: &testDataDirName,
  178. ClientPlatform: &clientPlatform,
  179. NetworkID: &networkID,
  180. EstablishTunnelTimeoutSeconds: &timeout,
  181. DisableLocalSocksProxy: &trueVal,
  182. DisableLocalHTTPProxy: &trueVal,
  183. },
  184. paramsDelta: nil,
  185. noticeReceiver: nil,
  186. },
  187. wantTunnel: true,
  188. expectedErr: nil,
  189. },
  190. }
  191. for _, tt := range tests {
  192. t.Run(tt.name, func(t *testing.T) {
  193. ctx := context.Background()
  194. var cancelFunc context.CancelFunc
  195. if tt.args.ctxTimeout > 0 {
  196. ctx, cancelFunc = context.WithTimeout(ctx, tt.args.ctxTimeout)
  197. }
  198. tunnel, err := StartTunnel(
  199. ctx,
  200. tt.args.configJSON,
  201. tt.args.embeddedServerEntryList,
  202. tt.args.params,
  203. tt.args.paramsDelta,
  204. tt.args.noticeReceiver)
  205. gotTunnel := (tunnel != nil)
  206. if cancelFunc != nil {
  207. cancelFunc()
  208. }
  209. if tunnel != nil {
  210. tunnel.Stop()
  211. }
  212. if gotTunnel != tt.wantTunnel {
  213. t.Errorf("StartTunnel() gotTunnel = %v, wantTunnel %v", err, tt.wantTunnel)
  214. }
  215. if err != tt.expectedErr {
  216. t.Fatalf("StartTunnel() error = %v, expectedErr %v", err, tt.expectedErr)
  217. return
  218. }
  219. if tunnel == nil {
  220. return
  221. }
  222. if tt.args.params.DisableLocalSocksProxy != nil && *tt.args.params.DisableLocalSocksProxy {
  223. if tunnel.SOCKSProxyPort != 0 {
  224. t.Fatalf("should not have started SOCKS proxy")
  225. }
  226. } else {
  227. if tunnel.SOCKSProxyPort == 0 {
  228. t.Fatalf("failed to start SOCKS proxy")
  229. }
  230. }
  231. if tt.args.params.DisableLocalHTTPProxy != nil && *tt.args.params.DisableLocalHTTPProxy {
  232. if tunnel.HTTPProxyPort != 0 {
  233. t.Fatalf("should not have started HTTP proxy")
  234. }
  235. } else {
  236. if tunnel.HTTPProxyPort == 0 {
  237. t.Fatalf("failed to start HTTP proxy")
  238. }
  239. }
  240. })
  241. }
  242. }
  243. func TestMultipleStartTunnel(t *testing.T) {
  244. configJSON, err := os.ReadFile("../../psiphon/controller_test.config")
  245. if err != nil {
  246. // What to do if config file is not present?
  247. t.Skipf("error loading configuration file: %s", err)
  248. }
  249. var config map[string]interface{}
  250. err = json.Unmarshal(configJSON, &config)
  251. if err != nil {
  252. t.Fatalf("json.Unmarshal failed: %v", err)
  253. }
  254. // Use the legacy encoding to both exercise that case, and facilitate a
  255. // gradual network upgrade to new encoding support.
  256. config["TargetAPIEncoding"] = protocol.PSIPHON_API_ENCODING_JSON
  257. configJSON, err = json.Marshal(config)
  258. if err != nil {
  259. t.Fatalf("json.Marshal failed: %v", err)
  260. }
  261. testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
  262. if err != nil {
  263. t.Fatalf("ioutil.TempDir failed: %v", err)
  264. }
  265. defer os.RemoveAll(testDataDirName)
  266. ctx := context.Background()
  267. tunnel1, err := StartTunnel(
  268. ctx,
  269. configJSON,
  270. "",
  271. Parameters{DataRootDirectory: &testDataDirName},
  272. nil,
  273. nil)
  274. if err != nil {
  275. t.Fatalf("first StartTunnel() error = %v", err)
  276. }
  277. // We have not stopped the tunnel, so a second StartTunnel() should fail
  278. _, err = StartTunnel(
  279. ctx,
  280. configJSON,
  281. "",
  282. Parameters{DataRootDirectory: &testDataDirName},
  283. nil,
  284. nil)
  285. if err != errMultipleStart {
  286. t.Fatalf("second StartTunnel() should have failed with errMultipleStart; got %v", err)
  287. }
  288. // Stop the tunnel and try again
  289. tunnel1.Stop()
  290. tunnel3, err := StartTunnel(
  291. ctx,
  292. configJSON,
  293. "",
  294. Parameters{DataRootDirectory: &testDataDirName},
  295. nil,
  296. nil)
  297. if err != nil {
  298. t.Fatalf("third StartTunnel() error = %v", err)
  299. }
  300. // Stop the tunnel so it doesn't interfere with other tests
  301. tunnel3.Stop()
  302. }
  303. func TestPsiphonTunnel_Dial(t *testing.T) {
  304. trueVal := true
  305. configJSON, err := os.ReadFile("../../psiphon/controller_test.config")
  306. if err != nil {
  307. // Skip, don't fail, if config file is not present
  308. t.Skipf("error loading configuration file: %s", err)
  309. }
  310. var config map[string]interface{}
  311. err = json.Unmarshal(configJSON, &config)
  312. if err != nil {
  313. t.Fatalf("json.Unmarshal failed: %v", err)
  314. }
  315. // Use the legacy encoding to both exercise that case, and facilitate a
  316. // gradual network upgrade to new encoding support.
  317. config["TargetAPIEncoding"] = protocol.PSIPHON_API_ENCODING_JSON
  318. configJSON, err = json.Marshal(config)
  319. if err != nil {
  320. t.Fatalf("json.Marshal failed: %v", err)
  321. }
  322. testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
  323. if err != nil {
  324. t.Fatalf("ioutil.TempDir failed: %v", err)
  325. }
  326. defer os.RemoveAll(testDataDirName)
  327. type args struct {
  328. remoteAddr string
  329. }
  330. tests := []struct {
  331. name string
  332. args args
  333. wantErr bool
  334. }{
  335. {
  336. name: "Success: example.com",
  337. args: args{remoteAddr: "example.com:443"},
  338. wantErr: false,
  339. },
  340. {
  341. name: "Failure: invalid address",
  342. args: args{remoteAddr: "example.com:99999"},
  343. wantErr: true,
  344. },
  345. }
  346. for _, tt := range tests {
  347. t.Run(tt.name, func(t *testing.T) {
  348. tunnel, err := StartTunnel(
  349. context.Background(),
  350. configJSON,
  351. "",
  352. Parameters{
  353. DataRootDirectory: &testDataDirName,
  354. // Don't need local proxies for dial tests
  355. // (and this is likely the configuration that will be used by consumers of the library who utilitize Dial).
  356. DisableLocalSocksProxy: &trueVal,
  357. DisableLocalHTTPProxy: &trueVal,
  358. },
  359. nil,
  360. nil)
  361. if err != nil {
  362. t.Fatalf("StartTunnel() error = %v", err)
  363. }
  364. defer tunnel.Stop()
  365. conn, err := tunnel.Dial(tt.args.remoteAddr)
  366. if (err != nil) != tt.wantErr {
  367. t.Fatalf("PsiphonTunnel.Dial() error = %v, wantErr %v", err, tt.wantErr)
  368. return
  369. }
  370. if tt.wantErr != (conn == nil) {
  371. t.Fatalf("PsiphonTunnel.Dial() conn = %v, wantConn %v", conn, !tt.wantErr)
  372. }
  373. })
  374. }
  375. }