Skip to content

Close sessions of removed users on UpdateUsers - #19

Open
shenaba wants to merge 1 commit into
SagerNet:mainfrom
shenaba:close-removed-user-sessions
Open

shenaba wants to merge 1 commit into
SagerNet:mainfrom
shenaba:close-removed-user-sessions

Conversation

@shenaba

@shenaba shenaba commented Sep 11, 2026 •

Copy link
Copy Markdown

Service.UpdateUsers replaces the user table, but a tuic / hysteria / hysteria2 session authenticates once and then multiplexes every later stream over the same QUIC connection without consulting that table again:

  • tuic — handleUniStream sets authUser and closes authDone; handleStream only waits on authDone.
  • hysteria — handleConnection sets authUser after the client hello; loopStreams never re-checks.
  • hysteria2 — ServeHTTP sets authenticated and short-circuits on every later auth request; dispatchStream only tests that flag.

So dropping a user from the table does not revoke the access that user already has, and a caller that needs to cut someone off — quota exhausted, subscription expired, credentials revoked — has no way to do it. Closing the individual relayed streams achieves nothing: the client immediately opens new ones on the same authenticated session. There is no exported handle on the session or on its *quic.Conn, so the only remaining option is tearing down the whole listener, which disconnects every other user on it.

How long the revoked session then survives is entirely up to the client. For tuic it need not end at all on its own: loopHeartbeats keeps sending ack-eliciting datagrams, so the idle timeout never fires.

(Found from a sing-box-based panel that calls UpdateUsers to disable a user whose traffic quota ran out. The user stays connected and keeps consuming; with shadowsocks or plain vless the same operation cuts them off at once, because there the transport and the relayed connection are the same thing.)

Change

Each Service now keeps the set of its authenticated sessions, and UpdateUsers closes the ones whose user is gone from the new table. Sessions are registered only after authentication — before that there is no user to match a removed one against — and unregistered when they end. Victims are collected under the session lock and closed after it is released, so the close path never runs with that lock held.

No API change. UpdateUsers is the only entry point, and sing-box calls it from NewInbound before Start, when no session exists yet, so nothing changes for a caller that never removes a user.

Test

tuic/service_test.go brings a service and a client up over loopback UDP, relays a stream, removes the user, and requires the already-established connection to fail. It fails on main with session of a removed user stayed alive and passes with this change; also clean under -race -count=3.

It is the first test in the repo, so make test compiled and ran nothing before this. The scaffolding it needs (a self-signed certificate and an aTLS.ServerConfig over crypto/tls) is kept inside the one file rather than made into a shared helper, since hysteria and hysteria2 are separate packages and would each need their own copy; happy to add those two as well, or to drop the test entirely if you would rather keep the repo test-free.

A tuic, hysteria or hysteria2 session authenticates once and then
multiplexes every later stream over the same QUIC connection without
consulting the user table again. UpdateUsers only replaced that table, so
a user removed from it kept full access over the connection it already
had: closing the individual relayed streams achieves nothing, the client
simply opens new ones on the same authenticated session. With tuic's
server-side heartbeat refreshing the idle timer, that can last as long as
the client cares to keep the connection.

Track authenticated sessions per service and close the ones whose user is
gone from the new table.
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.

1 participant