@@ -27,6 +27,8 @@ import (
27
27
"time"
28
28
29
29
"github.com/goccy/go-yaml"
30
+ "github.com/goccy/go-yaml/ast"
31
+ "github.com/goccy/go-yaml/parser"
30
32
"github.com/Jigsaw-Code/outline-sdk/dns"
31
33
"github.com/Jigsaw-Code/outline-sdk/transport"
32
34
"github.com/Jigsaw-Code/outline-sdk/x/configurl"
@@ -95,12 +97,13 @@ type dnsEntryConfig struct {
95
97
TCP * tcpEntryConfig `yaml:"tcp,omitempty"`
96
98
}
97
99
98
- type psiphonEntryConfig struct {
99
- // Don't verify the psiphon config format here, just pass it forward
100
- Psiphon map [ string ] interface {} `yaml:" psiphon,omitempty"`
100
+ type fallbackEntryStructConfig struct {
101
+ Psiphon any `yaml:" psiphon,omitempty"`
102
+ // As we allow more fallback types beyond psiphon they will be added here
101
103
}
102
104
103
- // can be either a configurl string or a psiphon config
105
+ // This contains either a configURL string or a fallbackEntryStructConfig
106
+ // It is parsed into the correct type later
104
107
type fallbackEntryConfig any
105
108
106
109
type configConfig struct {
@@ -331,22 +334,23 @@ func (f *StrategyFinder) findFallback(ctx context.Context, testDomains []string,
331
334
fallback , err := raceTests (ctx , 250 * time .Millisecond , fallbackConfigs , func (fallbackConfig fallbackEntryConfig ) (* SearchResult , error ) {
332
335
switch v := fallbackConfig .(type ) {
333
336
case string :
334
- fallbackUrl := v
335
- dialer , err := configModule .NewStreamDialer (ctx , fallbackUrl )
337
+ configUrl := v
338
+ dialer , err := configModule .NewStreamDialer (ctx , configUrl )
336
339
if err != nil {
337
340
return nil , fmt .Errorf ("getStreamDialer failed: %w" , err )
338
341
}
339
342
340
- err = f .testDialer (ctx , dialer , testDomains , fallbackUrl )
343
+ err = f .testDialer (ctx , dialer , testDomains , configUrl )
341
344
if err != nil {
342
345
return nil , err
343
346
}
344
347
345
348
return & SearchResult {dialer , fallbackConfig }, nil
346
- case map [string ]interface {}:
347
- psiphonJSON , err := json .Marshal (v )
349
+ case fallbackEntryStructConfig :
350
+ psiphonCfg := v .Psiphon
351
+ psiphonJSON , err := json .Marshal (psiphonCfg )
348
352
if err != nil {
349
- f .logCtx (ctx , "Error marshaling to JSON: %v, %v" , v , err )
353
+ f .logCtx (ctx , "Error marshaling to JSON: %v, %v" , psiphonCfg , err )
350
354
}
351
355
352
356
// TODO(laplante): pass this forward into psiphon.go, which takes raw json
@@ -355,10 +359,8 @@ func (f *StrategyFinder) findFallback(ctx context.Context, testDomains []string,
355
359
default :
356
360
return nil , fmt .Errorf ("unknown fallback type: %v" , v )
357
361
}
358
-
359
- // If neither, it's an unknown type
360
- return nil , fmt .Errorf ("unknown fallback type: %v" , fallbackConfig )
361
362
})
363
+
362
364
if err != nil {
363
365
return nil , fmt .Errorf ("could not find a working fallback: %w" , err )
364
366
}
@@ -392,14 +394,58 @@ func (f *StrategyFinder) newProxylessDialer(ctx context.Context, testDomains []s
392
394
return f .findTLS (ctx , testDomains , dnsDialer , config .TLS )
393
395
}
394
396
397
+ func (f * StrategyFinder ) parseConfig (configBytes []byte ) (configConfig , error ) {
398
+ parsedBytes , err := parser .ParseBytes (configBytes , 0 )
399
+ if err != nil {
400
+ return configConfig {}, fmt .Errorf ("failed to parse config: %v" , err )
401
+ }
402
+
403
+ var parsedConfig configConfig
404
+ err = yaml .NodeToValue (parsedBytes .Docs [0 ].Body , & parsedConfig )
405
+ if err != nil {
406
+ return configConfig {}, fmt .Errorf ("failed to parse config: %v" , err )
407
+ }
408
+
409
+ // Parse fallback list
410
+ mapping , ok := parsedBytes .Docs [0 ].Body .(* ast.MappingNode )
411
+ if ! ok {
412
+ return configConfig {}, fmt .Errorf ("failed to parse config: root is not a mapping" )
413
+ }
414
+ for _ , value := range mapping .Values {
415
+ if key , ok := value .Key .(* ast.StringNode ); ok && key .Value == "fallback" {
416
+ sequence , ok := value .Value .(* ast.SequenceNode )
417
+ if ! ok {
418
+ return configConfig {}, fmt .Errorf ("failed to parse config: fallback is not a sequence" )
419
+ }
420
+ for i , fallback := range sequence .Values {
421
+ switch v := fallback .(type ) {
422
+ case * ast.StringNode :
423
+ parsedConfig .Fallback [i ] = v .Value
424
+ case * ast.MappingNode :
425
+ var fallbackEntry fallbackEntryStructConfig
426
+ err := yaml .NodeToValue (v , & fallbackEntry )
427
+ if err != nil {
428
+ return configConfig {}, fmt .Errorf ("failed to parse config as fallbackEntryStructConfig: %v" , err )
429
+ }
430
+ parsedConfig .Fallback [i ] = fallbackEntry
431
+ default :
432
+ return configConfig {}, fmt .Errorf ("unknown fallback type: %v" , v )
433
+ }
434
+ }
435
+ }
436
+ }
437
+
438
+ return parsedConfig , nil
439
+ }
440
+
395
441
// NewDialer uses the config in configBytes to search for a strategy that unblocks DNS and TLS for all of the testDomains, returning a dialer with the found strategy.
396
442
// It returns an error if no strategy was found that unblocks the testDomains.
397
443
// The testDomains must be domains with a TLS service running on port 443.
398
444
func (f * StrategyFinder ) NewDialer (ctx context.Context , testDomains []string , configBytes []byte ) (transport.StreamDialer , error ) {
399
445
var parsedConfig configConfig
400
- err := yaml . Unmarshal (configBytes , & parsedConfig )
446
+ parsedConfig , err := f . parseConfig (configBytes )
401
447
if err != nil {
402
- return nil , fmt . Errorf ( "failed to parse config: %v" , err )
448
+ return nil , err
403
449
}
404
450
405
451
// Make domain fully-qualified to prevent confusing domain search.
0 commit comments