While looking into the custom socket logic from #2234, I came across a pretty significant loophole.
Right now, a client can open a WebSocket and never join a channel, completely bypassing the max_concurrent_users limit.
UsersCounter.add and Tracker.track only happen inside RealtimeChannel.join, so these bare connections are basically invisible to the system. They don't count toward the tenant limit, and the 10-minute Tracker cleanup doesn't catch them either.
That means a buggy client (or someone abusing it) could keep thousands of WebSocket connections open with regular heartbeats and consume server resources without ever hitting the tenant limit.
Easy way to reproduce:
Set a tenant's max_concurrent_users to 1.
Open multiple raw WebSocket connections to that tenant without sending phx_join.
All of them connect successfully, the user count stays unchanged, and the connections stay open indefinitely.
Also, as an effect, this socket-level counting fixes the channel-leaving issue from #2233 too.
While looking into the custom socket logic from #2234, I came across a pretty significant loophole.
Right now, a client can open a WebSocket and never join a channel, completely bypassing the max_concurrent_users limit.
UsersCounter.add and Tracker.track only happen inside RealtimeChannel.join, so these bare connections are basically invisible to the system. They don't count toward the tenant limit, and the 10-minute Tracker cleanup doesn't catch them either.
That means a buggy client (or someone abusing it) could keep thousands of WebSocket connections open with regular heartbeats and consume server resources without ever hitting the tenant limit.
Easy way to reproduce:
Set a tenant's max_concurrent_users to 1.
Open multiple raw WebSocket connections to that tenant without sending phx_join.
All of them connect successfully, the user count stays unchanged, and the connections stay open indefinitely.
Also, as an effect, this socket-level counting fixes the channel-leaving issue from #2233 too.