Skip to content
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(tcpreuse): add options for sharing TCP listeners amongst TCP, WS and WSS transports #2984

Open
wants to merge 32 commits into
base: master
Choose a base branch
from

Conversation

aschmahmann
Copy link
Collaborator

closes #2684

@MarcoPolo @sukunrt I took a stab at this as a result of looking at ipfs/kubo#10521. I also pulled from @Jorropo's work on #2737.

Initial testing seems to indicate things work ok, but this definitely needs more tests, API cleanup + eyes before merging.

Side note: Given that we're sharing the underlying TCP listener now I wonder if we basically have to tackle #1435 as well.

p2p/transport/tcp/tcp.go Outdated Show resolved Hide resolved
Comment on lines 28 to 32
const (
Unknown DemultiplexedConnType = iota
MultistreamSelect
HTTP
TLS
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setup is hardcoded and doesn't allow others to add new protocols to demultiplex from the same listener without making a PR. This seems ok given we have to be careful about making sure we don't get magic byte collisions between different protocols, but if we want to set this up as a registry object instead that seems fine too.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. Thought needs to put into extending this. Compared to that, adding a new enum here is easy.

p2p/transport/tcpreuse/demultiplex.go Outdated Show resolved Hide resolved
p2p/transport/tcpreuse/demultiplex.go Outdated Show resolved Hide resolved

func IsTLS(s Sample) bool {
switch string(s[:]) {
case "\x16\x03\x01", "\x16\x03\x02", "\x16\x03\x03", "\x16\x03\x04":
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure from reading https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2 it seems like \x16\x03\x04 is never expected in the client hello and so should be here. (fyi @Jorropo since I took this from magiselect)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean so should not be here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya, thanks

p2p/transport/tcpreuse/demultiplex.go Outdated Show resolved Hide resolved
p2p/transport/tcpreuse/listener.go Outdated Show resolved Hide resolved
@@ -122,6 +133,9 @@ type TcpTransport struct {
disableReuseport bool // Explicitly disable reuseport.
enableMetrics bool

// share and demultiplex TCP listeners across multiple transports
sharedTcp *tcpreuse.ConnMgr
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestions on what to call this feature/option? I grabbed the ConnMgr name because it's used in the quicreuse package, but it doesn't seem quite right and tcpreuse seems likely to be confused with reuseport.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. I also don't like the connmgr name in quicreuse. But I don't have a better name that isn't also confusing. At least this is consistently confusing (?)

}

func (m *multiplexedListener) Run() error {
const numWorkers = 16
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have preset workers, options, etc. but I wonder if the resource manager might also help with the job here. My understanding is that it's used starting the multistream level because it won't play nicely with just a net.Conn, maybe this means we can move the resource management lower in the stack.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was refactored to use a channel semaphore with N=64. That should be good enough for most (all?) use cases.

This thing should be very fast we are reading the first 3 bytes of the first packet.

Comment on lines +14 to +15
// EnvReuseportVal stores the value of envReuseport. defaults to true.
var EnvReuseportVal = true
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't feel good to export as it's only used for tests and is the result of me moving things to avoid dependency cycles (and separate concerns).

Any suggestions on where this reuseport logic lives? Could potentially move the tests or have enough options to thread the testing logic through

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a refactor to be done here, but maybe we do that in a separate PR? This one is already quite big

@sukunrt
Copy link
Member

sukunrt commented Oct 1, 2024

Overall it looks quite good.

The only thing major thing that we must fix before merging is this:

// TODO: if/how do we want to handle stalled connections and stop them from clogging up the pipeline?
// Drop connection because the buffer is full

We need a way to limit the number of connections being upgraded per IP/subnet. Otherwise, a single node can make the target node unresponsive.

We don't have the endpoint multiaddr at this point. This makes it difficult to use the resource manager as is. It's not too bad though, The only useful part of endpoint is (IP,PORT,TCP/UDP) tuple.

	OpenConnection(dir Direction, usefd bool, endpoint multiaddr.Multiaddr) (ConnManagementScope, error)
  1. We can just use the rcmgr as is. We have the IP and Port of the remote, that's all that's needed by the resource manager any way.
  2. We can implement the ConnLimiter separately for this tcp reuse block.

I think 1 is fine. AllowList anyway works with a IP / Subnet. We can make this clear in the documentation for Resource Manager.

Comment on lines +152 to 155
func NewTCPTransport(upgrader transport.Upgrader, rcmgr network.ResourceManager, sharedTCP *tcpreuse.ConnMgr, opts ...Option) (*TcpTransport, error) {
if rcmgr == nil {
Copy link
Member

@sukunrt sukunrt Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change.
It simplifies the top level basic host api a lot. You can do:

libp2p.New(Transport(tcp.NewTransport), Transport(websocket.New), ShareTCPListener())

And not need to specify the rcmgr, or the connection gater.

I'm confused how to provide this API, enabling sharing by just specifying ShareTCPListener, and pass in the sharedTCP objects as one of the optional arguments TCP Transport. If we can pass in the sharedTCP object as an optional argument that will prevent breaking this.

The simplest alternative implementation wise, without any breaking change, is an API like:

tcmgr := tcpreuse.NewConnMgr(false, gater, rcmgr)
libp2p.New(
    Transport(tcp.NewTransport, tcp.WithSharedTCP(tcmgr)),
    Transport(websocket.New, websocket.WithSharedTCP(tcmgr)),
)

p2p/transport/tcpreuse/listener.go Show resolved Hide resolved
Comment on lines +185 to +193
// Gate and resource limit the connection here.
// If done after sampling the connection, we'll be vulnerable to DOS attacks by a single peer
// which clogs up our entire connection queue.
// This duplicates the responsibility of gating and resource limiting between here and the upgrader. The
// alternative without duplication requires moving the process of upgrading the connection here, which forces
// us to establish the websocket connection here. That is more duplication, or a significant breaking change.
//
// Bugs around multiple calls to OpenConnection or InterceptAccept are prevented by the transport
// integration tests.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the smallest change I could think of which provided this DoS by a single node prevention.
It's also nicely guarded by transport integration tests, so this feels okay to me.

p2p/transport/tcpreuse/listener_test.go Show resolved Hide resolved
Comment on lines -130 to +153
mnc, err := manet.WrapNetConn(c)
if err != nil {
c.Close()
return nil, err
}
return mnc, nil
return c, nil
Copy link
Member

@sukunrt sukunrt Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping it by manet shadows the Scope method which we need for the upgrader. Any regression here is prevented by transport integration tests.

options.go Outdated
Comment on lines 639 to 653
// ShareTCPListener shares the same listen address between TCP and Websocket transports.
func ShareTCPListener() Option {
return func(cfg *Config) error {
cfg.ShareTCPListener = true
return nil
}
}
Copy link
Member

@sukunrt sukunrt Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API has the nice property of keeping all current code same and enabling sharing by just specifying

libp2p.New(
...
ShareTCPListener()
)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As libp2p.New user I will wonder why I need to explicitly pass this Option to enable sharing for TCP ports, but don't have to do anything extra for UDP ones.

No chance of making this implicit? Or at least explain in godoc comment why this needs to be explicit?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason is that I want to get some confidence here before defaulting this behavior to on. In a future release it'll be the default and you wouldn't need this.

Aside: UDP transports don't currently share the port if you use port 0 across them (this does). That's also something that I'd like to change in a future release.

I'll update the comment.

@sukunrt sukunrt requested a review from MarcoPolo October 7, 2024 11:03
@sukunrt sukunrt marked this pull request as ready for review October 7, 2024 11:04
@MarcoPolo MarcoPolo mentioned this pull request Oct 9, 2024
30 tasks
@MarcoPolo MarcoPolo mentioned this pull request Oct 28, 2024
20 tasks
@MarcoPolo MarcoPolo self-assigned this Oct 29, 2024
Copy link
Collaborator

@MarcoPolo MarcoPolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this mostly looks good. Just a couple things I'll clean up in the next pass

p2p/transport/tcp/tcp.go Outdated Show resolved Hide resolved
p2p/transport/websocket/websocket.go Outdated Show resolved Hide resolved
p2p/transport/tcpreuse/listener.go Outdated Show resolved Hide resolved
Comment on lines 28 to 32
const (
Unknown DemultiplexedConnType = iota
MultistreamSelect
HTTP
TLS
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. Thought needs to put into extending this. Compared to that, adding a new enum here is easy.

p2p/transport/tcpreuse/demultiplex.go Outdated Show resolved Hide resolved
libp2p_test.go Outdated Show resolved Hide resolved
p2p/net/upgrader/listener.go Outdated Show resolved Hide resolved
p2p/transport/tcp/metrics.go Outdated Show resolved Hide resolved
p2p/transport/tcpreuse/demultiplex.go Outdated Show resolved Hide resolved
p2p/transport/websocket/conn.go Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use the same port for TCP and WebSocket
4 participants