-
Notifications
You must be signed in to change notification settings - Fork 64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(x): Parse psiphon config format as part of smart-proxy #394
base: main
Are you sure you want to change the base?
Conversation
4394b98
to
7a8888e
Compare
Alright, I made an attempt at changing for format. But I don't particularly like it. It seems like it introduces a lot of complication in parsing the config.
All for the dubious pleasure of mixing strings and objects in a list. You're sure it wouldn't be cleaner just to switch to a keyed map while there's no one relying on this? I do really like just copying the json from the psiphon config directly. Seems much nicer. |
x/smart/stream_dialer.go
Outdated
|
||
return &SearchResult{dialer, fallbackConfig}, nil | ||
case map[string]interface{}: | ||
psiphonJSON, err := json.Marshal(v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need a switch here based on what type field was set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a check for the string, but I'm not sure I'm understanding what you mean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to do the same we do for DNS, like:
if cfg := entry.Psiphon; cfg != nil {
// Process Psiphon config
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doing it like
type psiphonEntryConfig struct {
Psiphon any `yaml:"psiphon,omitempty"`
}
...
psiphonCfg, ok := fallbackConfig.(psiphonEntryConfig)
doesn't succeed. ok = false and it fails to coerce the type.
case psiphonEntryConfig:
also fails
The point of the map[string]interface{}
type was to get enough of a toehold on the type that I'm able to extract the child object.
Attempting to call fallbackConfig.Psiphon
fails to build because it's not certain the field exists.
Because there's no union type yaml.Unmarshal can't do all the work for us of mapping the yaml into structs while we're mixing structs and strings.
We could force the conversion here with something like https://pkg.go.dev/reflect, but that seems heavyweight when we're just trying to check and will be passing on to the psiphon lib in a moment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That type should be called something else, since it should allow other types. Example:
type fallbackEntryConfig struct {
Psiphon any
FooBar SomeType
}
To parse, you can change the list entry to be ast.Node (from github.com/goccy/go-yaml).
Then you can use NodeToValue.
Or do what we do in the client with mapToAny.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
broke out a parseConfig
function that parses strings and objects directly from the ast. I would love ideas on how to make this part shorter though.
c6d9a3a
to
4ac6bd3
Compare
x/smart/stream_dialer.go
Outdated
|
||
return &SearchResult{dialer, fallbackConfig}, nil | ||
case map[string]interface{}: | ||
psiphonJSON, err := json.Marshal(v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to extract the config from the psiphon
field, like we do for DNS.
Perhaps define a fallbackEntryConfig with the Psiphon: any
field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
x/smart/stream_dialer.go
Outdated
|
||
return &SearchResult{dialer, fallbackConfig}, nil | ||
case map[string]interface{}: | ||
psiphonJSON, err := json.Marshal(v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to do the same we do for DNS, like:
if cfg := entry.Psiphon; cfg != nil {
// Process Psiphon config
}
x/smart/stream_dialer.go
Outdated
|
||
return &SearchResult{dialer, fallbackConfig}, nil | ||
case map[string]interface{}: | ||
psiphonJSON, err := json.Marshal(v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That type should be called something else, since it should allow other types. Example:
type fallbackEntryConfig struct {
Psiphon any
FooBar SomeType
}
To parse, you can change the list entry to be ast.Node (from github.com/goccy/go-yaml).
Then you can use NodeToValue.
Or do what we do in the client with mapToAny.
4ac6bd3
to
1b11975
Compare
1b11975
to
5cc6097
Compare
5cc6097
to
c0a72b2
Compare
x/smart/stream_dialer.go
Outdated
return &SearchResult{dialer, fallbackUrl}, nil | ||
return &SearchResult{dialer, fallbackConfig}, nil | ||
case fallbackEntryStructConfig: | ||
psiphonCfg := v.Psiphon |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please test if Psiphon is not nil. I want the Psiphon code to be in it's own block. Think about how it would look with more types.
Also, you shouldn't call Marshall on nil.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
return nil, fmt.Errorf("getStreamDialer failed: %w", err) | ||
} | ||
fallback, err := raceTests(ctx, 250*time.Millisecond, fallbackConfigs, func(fallbackConfig fallbackEntryConfig) (*SearchResult, error) { | ||
switch v := fallbackConfig.(type) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This parsing logic probably deserves a unit test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think this specifically and the selection logic in general would benefit from more unit testing. I'm also a bit nervous about the correctness of where we're returning errors vs logging error output.
To do it as a unit test will require some refactoring though, since currently the network calls are intertwined. Can I do it in a follow up?
I did add a few tests, but just for ParseConfig, since it's currently well isolated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing that may help for testing is replacing the base dialers (though we can't do that for Psiphon). But that can be done some other time.
@@ -365,14 +394,58 @@ func (f *StrategyFinder) newProxylessDialer(ctx context.Context, testDomains []s | |||
return f.findTLS(ctx, testDomains, dnsDialer, config.TLS) | |||
} | |||
|
|||
func (f *StrategyFinder) parseConfig(configBytes []byte) (configConfig, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you doing this manual parsing instead of unmarshalling into the output directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got rid of the manual parsing and switched this to use mapToAny
+ Unmarshal
.
I've just copied mapToAny
in wholesale from outline-apps. I figure we'll want it anyway eventually. But let me know if you'd like me to put it somewhere other than this file in anticipation of eventually moving over more code.
I've also added a bit of unit testing, partially just to enumerate the allowed config fields in a single readable place besides x/examples/smart_proxy
.
4cd69e3
to
58822c9
Compare
58822c9
to
b9c7805
Compare
return nil, fmt.Errorf("getStreamDialer failed: %w", err) | ||
} | ||
fallback, err := raceTests(ctx, 250*time.Millisecond, fallbackConfigs, func(fallbackConfig fallbackEntryConfig) (*SearchResult, error) { | ||
switch v := fallbackConfig.(type) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing that may help for testing is replacing the base dialers (though we can't do that for Psiphon). But that can be done some other time.
b9c7805
to
9eefc82
Compare
9eefc82
to
49d77cb
Compare
This is not yet setting up the psiphon connection, but I figured it would be important to hash out exactly what format we want to use in
config.yaml
for the fallbacks since this is modifying the format slightly from #384.I've broken the parsing of the config out into
parseConfig
to deal with the string and object parsing.Don't worry too much about the json parsing/logging around :331. It will go away in the next PR. I just wanted to make sure this parsing approach would work for getting from YAML->rawJson without actually putting the format validation here.
Tested
Also tested a failing psiphon config followed by a successful outline server.