Skip to content

Commit 5f90dcf

Browse files
committed
fix: StartTunnel re-entry, early stderr notices, clearing notice writer
1 parent 3b01244 commit 5f90dcf

File tree

2 files changed

+187
-67
lines changed

2 files changed

+187
-67
lines changed

ClientLibrary/clientlib/clientlib.go

Lines changed: 72 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -140,63 +140,9 @@ func StartTunnel(
140140
if !started.CompareAndSwap(false, true) {
141141
return nil, errMultipleStart
142142
}
143-
144-
config, err := psiphon.LoadConfig(configJSON)
145-
if err != nil {
146-
return nil, errors.TraceMsg(err, "failed to load config file")
147-
}
148-
149-
// Use params.DataRootDirectory to set related config values.
150-
if params.DataRootDirectory != nil {
151-
config.DataRootDirectory = *params.DataRootDirectory
152-
153-
// Migrate old fields
154-
config.MigrateDataStoreDirectory = *params.DataRootDirectory
155-
config.MigrateObfuscatedServerListDownloadDirectory = *params.DataRootDirectory
156-
config.MigrateRemoteServerListDownloadFilename = filepath.Join(*params.DataRootDirectory, "server_list_compressed")
157-
}
158-
159-
if params.NetworkID != nil {
160-
config.NetworkID = *params.NetworkID
161-
}
162-
163-
if params.ClientPlatform != nil {
164-
config.ClientPlatform = *params.ClientPlatform
165-
} // else use the value in config
166-
167-
if params.EstablishTunnelTimeoutSeconds != nil {
168-
config.EstablishTunnelTimeoutSeconds = params.EstablishTunnelTimeoutSeconds
169-
} // else use the value in config
170-
171-
if config.UseNoticeFiles == nil && config.EmitDiagnosticNotices && params.EmitDiagnosticNoticesToFiles {
172-
config.UseNoticeFiles = &psiphon.UseNoticeFiles{
173-
RotatingFileSize: 0,
174-
RotatingSyncFrequency: 0,
175-
}
176-
} // else use the value in the config
177-
178-
if params.DisableLocalSocksProxy != nil {
179-
config.DisableLocalSocksProxy = *params.DisableLocalSocksProxy
180-
} // else use the value in the config
181-
182-
if params.DisableLocalHTTPProxy != nil {
183-
config.DisableLocalHTTPProxy = *params.DisableLocalHTTPProxy
184-
} // else use the value in the config
185-
186-
// config.Commit must be called before calling config.SetParameters
187-
// or attempting to connect.
188-
err = config.Commit(true)
189-
if err != nil {
190-
return nil, errors.TraceMsg(err, "config.Commit failed")
191-
}
192-
193-
// If supplied, apply the parameters delta
194-
if len(paramsDelta) > 0 {
195-
err = config.SetParameters("", false, paramsDelta)
196-
if err != nil {
197-
return nil, errors.TraceMsg(err, fmt.Sprintf("SetParameters failed for delta: %v", paramsDelta))
198-
}
199-
}
143+
// There _must_ not be an early return between here and where tunnel.stop is deferred,
144+
// otherwise `started` will not get set back to false and we will be unable to call
145+
// StartTunnel again.
200146

201147
// Will be closed when the tunnel has successfully connected
202148
connectedSignal := make(chan struct{})
@@ -206,7 +152,8 @@ func StartTunnel(
206152
// Create the tunnel object
207153
tunnel := new(PsiphonTunnel)
208154

209-
// Set up notice handling
155+
// Set up notice handling. It is important to do this before config operations, as
156+
// otherwise they will write notices to stderr.
210157
psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver(
211158
func(notice []byte) {
212159
var event NoticeEvent
@@ -247,11 +194,6 @@ func StartTunnel(
247194
}
248195
}))
249196

250-
err = psiphon.OpenDataStore(config)
251-
if err != nil {
252-
return nil, errors.TraceMsg(err, "failed to open data store")
253-
}
254-
255197
// Create a cancelable context that will be used for stopping the tunnel
256198
tunnelCtx, cancelTunnelCtx := context.WithCancel(ctx)
257199

@@ -265,15 +207,82 @@ func StartTunnel(
265207
cancelTunnelCtx()
266208
tunnel.embeddedServerListWaitGroup.Wait()
267209
tunnel.controllerWaitGroup.Wait()
210+
// This is safe to call even if the data store hasn't been opened
268211
psiphon.CloseDataStore()
269212
started.Store(false)
213+
// Clear our notice receiver, as it is no longer needed and we should let it be
214+
// garbage-collected.
215+
psiphon.SetNoticeWriter(io.Discard)
270216
}
271217

272218
defer func() {
273219
if retErr != nil {
274220
tunnel.stop()
275221
}
276222
}()
223+
// We have now set up our on-error cleanup and it is safe to have early error returns.
224+
225+
config, err := psiphon.LoadConfig(configJSON)
226+
if err != nil {
227+
return nil, errors.TraceMsg(err, "failed to load config file")
228+
}
229+
230+
// Use params.DataRootDirectory to set related config values.
231+
if params.DataRootDirectory != nil {
232+
config.DataRootDirectory = *params.DataRootDirectory
233+
234+
// Migrate old fields
235+
config.MigrateDataStoreDirectory = *params.DataRootDirectory
236+
config.MigrateObfuscatedServerListDownloadDirectory = *params.DataRootDirectory
237+
config.MigrateRemoteServerListDownloadFilename = filepath.Join(*params.DataRootDirectory, "server_list_compressed")
238+
}
239+
240+
if params.NetworkID != nil {
241+
config.NetworkID = *params.NetworkID
242+
}
243+
244+
if params.ClientPlatform != nil {
245+
config.ClientPlatform = *params.ClientPlatform
246+
} // else use the value in config
247+
248+
if params.EstablishTunnelTimeoutSeconds != nil {
249+
config.EstablishTunnelTimeoutSeconds = params.EstablishTunnelTimeoutSeconds
250+
} // else use the value in config
251+
252+
if config.UseNoticeFiles == nil && config.EmitDiagnosticNotices && params.EmitDiagnosticNoticesToFiles {
253+
config.UseNoticeFiles = &psiphon.UseNoticeFiles{
254+
RotatingFileSize: 0,
255+
RotatingSyncFrequency: 0,
256+
}
257+
} // else use the value in the config
258+
259+
if params.DisableLocalSocksProxy != nil {
260+
config.DisableLocalSocksProxy = *params.DisableLocalSocksProxy
261+
} // else use the value in the config
262+
263+
if params.DisableLocalHTTPProxy != nil {
264+
config.DisableLocalHTTPProxy = *params.DisableLocalHTTPProxy
265+
} // else use the value in the config
266+
267+
// config.Commit must be called before calling config.SetParameters
268+
// or attempting to connect.
269+
err = config.Commit(true)
270+
if err != nil {
271+
return nil, errors.TraceMsg(err, "config.Commit failed")
272+
}
273+
274+
// If supplied, apply the parameters delta
275+
if len(paramsDelta) > 0 {
276+
err = config.SetParameters("", false, paramsDelta)
277+
if err != nil {
278+
return nil, errors.TraceMsg(err, fmt.Sprintf("SetParameters failed for delta: %v", paramsDelta))
279+
}
280+
}
281+
282+
err = psiphon.OpenDataStore(config)
283+
if err != nil {
284+
return nil, errors.TraceMsg(err, "failed to open data store")
285+
}
277286

278287
// If specified, the embedded server list is loaded and stored. When there
279288
// are no server candidates at all, we wait for this import to complete
@@ -374,10 +383,6 @@ func (tunnel *PsiphonTunnel) Stop() {
374383
tunnel.stop()
375384
tunnel.stop = nil
376385
tunnel.controllerDial = nil
377-
378-
// Clear our notice receiver, as it is no longer needed and we should let it be
379-
// garbage-collected.
380-
psiphon.SetNoticeWriter(io.Discard)
381386
}
382387

383388
// Dial connects to the specified address through the Psiphon tunnel.

ClientLibrary/clientlib/clientlib_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"testing"
2727
"time"
2828

29+
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon"
2930
"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/protocol"
3031
)
3132

@@ -418,3 +419,117 @@ func TestPsiphonTunnel_Dial(t *testing.T) {
418419
})
419420
}
420421
}
422+
423+
// We had a problem where config-related notices were being printed to stderr before we
424+
// set the NoticeWriter. We want to make sure that no longer happens.
425+
func TestStartTunnelNoOutput(t *testing.T) {
426+
configJSON, err := os.ReadFile("../../psiphon/controller_test.config")
427+
if err != nil {
428+
// What to do if config file is not present?
429+
t.Skipf("error loading configuration file: %s", err)
430+
}
431+
432+
var config map[string]interface{}
433+
err = json.Unmarshal(configJSON, &config)
434+
if err != nil {
435+
t.Fatalf("json.Unmarshal failed: %v", err)
436+
}
437+
438+
// Before starting the tunnel, set up a notice receiver. If it receives anything at
439+
// all, that means that it would have been printed to stderr.
440+
psiphon.SetNoticeWriter(psiphon.NewNoticeReceiver(
441+
func(notice []byte) {
442+
t.Fatalf("Received notice: %v", string(notice))
443+
}))
444+
445+
// Use the legacy encoding to both exercise that case, and facilitate a
446+
// gradual network upgrade to new encoding support.
447+
config["TargetAPIEncoding"] = protocol.PSIPHON_API_ENCODING_JSON
448+
449+
configJSON, err = json.Marshal(config)
450+
if err != nil {
451+
t.Fatalf("json.Marshal failed: %v", err)
452+
}
453+
454+
testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
455+
if err != nil {
456+
t.Fatalf("ioutil.TempDir failed: %v", err)
457+
}
458+
defer os.RemoveAll(testDataDirName)
459+
460+
ctx := context.Background()
461+
462+
tunnel, err := StartTunnel(
463+
ctx,
464+
configJSON,
465+
"",
466+
Parameters{DataRootDirectory: &testDataDirName},
467+
nil,
468+
nil)
469+
470+
if err != nil {
471+
t.Fatalf("StartTunnel() error = %v", err)
472+
}
473+
tunnel.Stop()
474+
}
475+
476+
// We had a problem where a very early error could result in `started` being set to true
477+
// and not be set back to false, preventing StartTunnel from being re-callable.
478+
func TestStartTunnelReentry(t *testing.T) {
479+
testDataDirName, err := os.MkdirTemp("", "psiphon-clientlib-test")
480+
if err != nil {
481+
t.Fatalf("ioutil.TempDir failed: %v", err)
482+
}
483+
defer os.RemoveAll(testDataDirName)
484+
485+
configJSON := []byte("BAD CONFIG JSON")
486+
487+
ctx := context.Background()
488+
489+
_, err = StartTunnel(
490+
ctx,
491+
configJSON,
492+
"",
493+
Parameters{DataRootDirectory: &testDataDirName},
494+
nil,
495+
nil)
496+
497+
if err == nil {
498+
t.Fatalf("expected config error")
499+
}
500+
501+
// Call again with a good config. Should work.
502+
configJSON, err = os.ReadFile("../../psiphon/controller_test.config")
503+
if err != nil {
504+
// What to do if config file is not present?
505+
t.Skipf("error loading configuration file: %s", err)
506+
}
507+
508+
var config map[string]interface{}
509+
err = json.Unmarshal(configJSON, &config)
510+
if err != nil {
511+
t.Fatalf("json.Unmarshal failed: %v", err)
512+
}
513+
514+
// Use the legacy encoding to both exercise that case, and facilitate a
515+
// gradual network upgrade to new encoding support.
516+
config["TargetAPIEncoding"] = protocol.PSIPHON_API_ENCODING_JSON
517+
518+
configJSON, err = json.Marshal(config)
519+
if err != nil {
520+
t.Fatalf("json.Marshal failed: %v", err)
521+
}
522+
523+
tunnel, err := StartTunnel(
524+
ctx,
525+
configJSON,
526+
"",
527+
Parameters{DataRootDirectory: &testDataDirName},
528+
nil,
529+
nil)
530+
531+
if err != nil {
532+
t.Fatalf("StartTunnel() error = %v", err)
533+
}
534+
tunnel.Stop()
535+
}

0 commit comments

Comments
 (0)