-
Notifications
You must be signed in to change notification settings - Fork 47
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): make config support PacketListeners and make dependencies explicit and decoupled #304
base: main
Are you sure you want to change the base?
Conversation
BTW, I believe it's possible to reduce some of the duplication with Generics. I'll explore that in parallel, but I wanted to advance this PR regardless of that. |
BTW, I am not changing the config format yet, but I introduced a The flow is: |
FYI, example QUIC support using this change: #305 |
Changed the code to use Generics and save coding. |
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.
Note how I'm separating this module file.
I envision the config.go living in the root SDK, since it's generic and simple, with minimal dependencies, and this config module being in the x folder, since it pulls all sorts of dependencies to implement all protocols.
Once the config lives in the main module, we can also consider adding config packages to each transport in the future too, to make it easy to register them.
ba74e88
to
866f9c1
Compare
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.
Thanks for the refactoring, it is great!
x/configurl/config.go
Outdated
_ TypeRegistry[any] = (*ExtensibleProvider[any])(nil) | ||
) | ||
|
||
func (p *ExtensibleProvider[ObjectType]) buildersMap() map[string]BuildFunc[ObjectType] { |
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.
Maybe call it ensureBuildersMap
since it has side effect to the object?
func (p *ExtensibleProvider[ObjectType]) buildersMap() map[string]BuildFunc[ObjectType] { | |
func (p *ExtensibleProvider[ObjectType]) ensureBuildersMap() map[string]BuildFunc[ObjectType] { |
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 p | ||
// ExtensibleProvider creates instances of ObjectType in a way that can be extended via its [TypeRegistry] interface. | ||
type ExtensibleProvider[ObjectType comparable] struct { |
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.
Do we really need the comparable
constraint here?
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.
Yes, because we compare it against the zero instance in NewInstance
.
x/configurl/config.go
Outdated
func (p *ExtensibleProvider[ObjectType]) RegisterType(subtype string, newInstance BuildFunc[ObjectType]) error { | ||
builders := p.buildersMap() | ||
if _, found := builders[subtype]; found { | ||
return fmt.Errorf("type %v registered twice", subtype) |
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.
Maybe we can replace the old entry? This might be useful in some "override" scenarios.
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.
Agreed. Changed accordingly.
p.RegisterStreamDialerType("ws", wrapStreamDialerWithWebSocket) | ||
p.RegisterPacketDialerType("ws", wrapPacketDialerWithWebSocket) | ||
// TypeRegistry registers config types. | ||
type TypeRegistry[ObjectType any] interface { |
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.
T
would be good enough, since ObjectType
doesn't add anything more meaningful than T
?
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 find ObjectType
clearer than simply T
, and it reads better in the comment as well.
x/configurl/config.go
Outdated
} | ||
|
||
if _, found := p.pdBuilders[subtype]; found { | ||
return fmt.Errorf("config parser %v for StreamDialer added twice", subtype) | ||
newDialer, ok := p.buildersMap()[config.URL.Scheme] |
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.
To be consistent with the name in RegisterType
.
newDialer, ok := p.buildersMap()[config.URL.Scheme] | |
newInstance, ok := p.buildersMap()[config.URL.Scheme] |
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.
Oops, leftover from the refactoring. Renamed.
x/configurl/tls_test.go
Outdated
func TestTLS_SNI(t *testing.T) { | ||
tlsURL, err := url.Parse("tls:sni=www.google.com") | ||
tlsURL, err := ParseConfig("tls:sni=www.google.com") |
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.
tlsURL, err := ParseConfig("tls:sni=www.google.com") | |
config, err := ParseConfig("tls:sni=www.google.com") |
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/configurl/tlsfrag.go
Outdated
@@ -0,0 +1,39 @@ | |||
// Copyright 2023 The Outline Authors |
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.
// Copyright 2023 The Outline Authors | |
// Copyright 2024 The Outline Authors |
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.
Updated
x/configurl/dns.go
Outdated
return nil, err | ||
} | ||
if closer, ok := sd.(io.Closer); ok { | ||
defer closer.Close() |
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 we closing the sd
here? It will be passed to dns.NewStreamDialer
below, will it be invalid after closing?
I think we may just need to close it when we return a non-nil
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.
Removed.
I was playing with the concept of cleaning up. We will need to add some mechanism for cleanup, or else we might leak resources. But I'll leave that to a future PR.
x/configurl/dns.go
Outdated
return nil, err | ||
} | ||
if closer, ok := pd.(io.Closer); ok { | ||
defer closer.Close() |
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.
Same as above.
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.
Same
// TODO: accept an inner dialer from the caller and pass it to UDPEndpoint | ||
ep := &transport.UDPEndpoint{Address: config.serverAddress} | ||
return shadowsocks.NewPacketListener(ep, config.cryptoKey) | ||
func registerShadowsocksPacketListener(r TypeRegistry[transport.PacketListener], typeID string, newPD BuildFunc[transport.PacketDialer]) { |
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.
Not for this PR, but I think we can replace all PacketListener
with the PacketDialer
for Shadowsocks in the future. Are we doing anything special in the PacketListener
?
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.
Maaaaybe. That's something to be investigated, and it may increase the server capacity. But it may also break NAT traversal, since it will tie the UDP socket to a single destination, preventing the local address from receiving packets from other addresses not yet contacted. This can in turn break some communication tools.
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.
Another look?
p.RegisterStreamDialerType("ws", wrapStreamDialerWithWebSocket) | ||
p.RegisterPacketDialerType("ws", wrapPacketDialerWithWebSocket) | ||
// TypeRegistry registers config types. | ||
type TypeRegistry[ObjectType any] interface { |
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 find ObjectType
clearer than simply T
, and it reads better in the comment as well.
|
||
return p | ||
// ExtensibleProvider creates instances of ObjectType in a way that can be extended via its [TypeRegistry] interface. | ||
type ExtensibleProvider[ObjectType comparable] struct { |
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.
Yes, because we compare it against the zero instance in NewInstance
.
x/configurl/config.go
Outdated
func (p *ExtensibleProvider[ObjectType]) RegisterType(subtype string, newInstance BuildFunc[ObjectType]) error { | ||
builders := p.buildersMap() | ||
if _, found := builders[subtype]; found { | ||
return fmt.Errorf("type %v registered twice", subtype) |
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.
Agreed. Changed accordingly.
x/configurl/config.go
Outdated
} | ||
|
||
if _, found := p.pdBuilders[subtype]; found { | ||
return fmt.Errorf("config parser %v for StreamDialer added twice", subtype) | ||
newDialer, ok := p.buildersMap()[config.URL.Scheme] |
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.
Oops, leftover from the refactoring. Renamed.
x/configurl/doc.go
Outdated
Configurable transports simplifies the way you create and manage transports. | ||
With the config package, you can use [NewPacketDialer] and [NewStreamDialer] to create dialers using a simple text string. | ||
Configurable strategies simplifies the way you create and manage strategies. | ||
With the configurl package, you can use [NewPacketDialer], [NewStreamDialer] and [NewPacketListener] to create objects using a simple text string. |
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.
Updated it to refer to the ConfigModule
functions.
x/configurl/tlsfrag.go
Outdated
@@ -0,0 +1,39 @@ | |||
// Copyright 2023 The Outline Authors |
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.
Updated
x/configurl/module.go
Outdated
|
||
// ConfigModule enables the creation of network objects based on a config. The config is | ||
// extensible by registering providers for config subtypes. | ||
type ConfigModule struct { |
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.
It's not really "Transports". It could be anything. I may need to include dns.Resolver
s there, Stream/Packet Endpoints, or http.Client
s. So it's really generic.
We can't use both Factory and Provider. They effectively mean the same thing. We could call it something Provider
, but it's not clear what "something" should be, and Provider
collides with the basic Provider
type we already have.
I'm renaming it to ProviderContainer
. What do you think?
I also created some auxiliary functions to make it more flexible.
x/configurl/config.go
Outdated
_ TypeRegistry[any] = (*ExtensibleProvider[any])(nil) | ||
) | ||
|
||
func (p *ExtensibleProvider[ObjectType]) buildersMap() map[string]BuildFunc[ObjectType] { |
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/configurl/dns.go
Outdated
return nil, err | ||
} | ||
if closer, ok := sd.(io.Closer); ok { | ||
defer closer.Close() |
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.
Removed.
I was playing with the concept of cleaning up. We will need to add some mechanism for cleanup, or else we might leak resources. But I'll leave that to a future PR.
x/configurl/dns.go
Outdated
return nil, err | ||
} | ||
if closer, ok := pd.(io.Closer); ok { | ||
defer closer.Close() |
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.
Same
Properly Support PacketListeners in the Config
As I design the advanced config for Outline, I ran into the issue that we need to support PacketListeners for both the Outline Client and to use QUIC. This PR allows PacketListeners to have a nested config like we have for dialers. I also added support to register types.
Explicit and decouples dependencies
The factory functions used to take as input factory functions for stream and packet dialers, regardless whether they needed them or not. This made dependencies unclear and not enforceable at compile time.
It also doesn't scale well, as more advanced strategies may need dependencies of different types. With the introduction of PacketListeners, I already ran into that issue. However, instead of adding a PacketListener factory to the factory function, I simplified the registration function to only take the context and config. The extra dependencies are passed explicitly then you create the factory function. This ensures compile checks, and allows us to inject any dependency we want, without needing to change the API.
Include Context
I now pass a Context to factory functions. This will allow us to inject deadlines or cancel creation for dialers that take long (e.g. Smart Dialer, Psiphon). It may also allow us to inject a structure for "named dialers" in the future.