Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions core/protocol/anytls/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
// tearing the listener down (see core/inbound_users.go).
//
// UPGRADING sing-box: re-copy this file from the new tag and re-apply the
// change below. Nothing here will fail to compile if you forget, it will just
// changes below. Nothing here will fail to compile if you forget, it will just
// silently keep running the old implementation.
//
// Local change vs sing-box: none, other than the package clause.
// Local change vs sing-box: two lines, both for the user-session registry.
// NewInbound wraps the router with it, and NewConnection -- which owns an
// anytls session for as long as that session lives -- is redirected through
// newSessionConnection in users.go, so the session can be registered there and
// closed later. Without it, removing a user only stops new sessions while the
// one the client already holds keeps being served. See core/usersession.
package anytls

import (
Expand Down Expand Up @@ -54,6 +59,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
router: uot.NewRouter(router, logger),
logger: logger,
}
inbound.router = withUserSessions(inbound.router)

if options.TLS != nil && options.TLS.Enabled {
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
Expand Down Expand Up @@ -117,7 +123,7 @@ func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata ada
}
conn = tlsConn
}
err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
err := h.newSessionConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source))
Expand Down
52 changes: 52 additions & 0 deletions core/protocol/anytls/users.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,67 @@
package anytls

import (
"context"
"net"

"github.com/shenaba/2s-ui/core/usersession"

"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"

anytls "github.com/anytls/sing-anytls"
)

// UpdateUsers swaps the user table of a running inbound, and cuts the sessions
// of everyone who just left it. Both halves are needed: the table alone only
// decides who may open a *new* session, while an anytls client that
// authenticated before the change keeps opening streams on the one it has.
func (h *Inbound) UpdateUsers(users []option.AnyTLSUser) error {
h.service.UpdateUsers(common.Map(users, func(it option.AnyTLSUser) anytls.User {
return (anytls.User)(it)
}))
// Unclosable is deliberately not checked: newSessionConnection registers
// the transport of every anytls session, so a removed user's session is
// always cut outright and never costs the inbound a rebuild. The only thing
// that can be counted here is a straggler stream that reached the router
// after its own session had already ended -- not a session, and not worth
// disconnecting everyone else for. If anytls ever grows a session this
// inbound does not hold the transport of, that stops being true.
h.sessions().CloseUsers(usersession.KeepSet(common.Map(users, func(it option.AnyTLSUser) string {
return it.Name
})))
return nil
}

// withUserSessions installs the session registry. anytls only needs the router
// to record who is connected -- the session itself is reached in
// newSessionConnection below, which is where it can be closed.
func withUserSessions(router adapter.ConnectionRouterEx) adapter.ConnectionRouterEx {
return usersession.WrapRouterEx(router, usersession.BindOnly)
}

// sessions reaches the registry installed by withUserSessions. NewInbound wraps
// the router right after building the struct and nothing replaces the field
// afterwards, so this holds. It is a bare assertion on purpose: someone
// reordering that would otherwise turn every session hook here into a silent
// no-op, and a panic on the save path is the lesser outcome.
func (h *Inbound) sessions() *usersession.Registry {
return h.router.(*usersession.RouterEx).Registry()
}

// newSessionConnection stands in for h.service.NewConnection at the one call
// site that owns an anytls session for its whole life. Registering the session
// here is what makes it closable; the streams it later opens reach the router
// individually and cannot be used to find it.
//
// The call blocks until the session ends, so the deferred Untrack is what keeps
// the registry to sessions that actually exist.
func (h *Inbound) newSessionConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error {
key := source.String()
h.sessions().Track(key, conn)
defer h.sessions().Untrack(key)
return h.service.NewConnection(ctx, conn, source, onClose)
}
12 changes: 11 additions & 1 deletion core/protocol/hysteria/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// tearing the listener down (see core/inbound_users.go).
//
// UPGRADING sing-box: re-copy this file from the new tag and re-apply the
// change below. Nothing here will fail to compile if you forget, it will just
// changes below. Nothing here will fail to compile if you forget, it will just
// silently keep running the old implementation.
//
// Local change vs sing-box: the service is keyed by user name
Expand All @@ -14,6 +14,15 @@
// UpdateUsers does rewrite it, under live sessions, and a position is not.
// Deleting a user shifted every later one, which mis-attributed traffic and
// could index past the end of the name slice outright (upstream #1231).
//
// Second local change: one line installs the user-session registry
//
// inbound.router = withUserSessions(inbound.router)
//
// which is what lets an already authenticated session be reached when its user
// is removed from the inbound -- swapping the user table alone only decides who
// may start a new one. That line is the whole of it here; everything it reaches
// lives in users.go, which the copy check skips. See core/usersession.
package hysteria

import (
Expand Down Expand Up @@ -68,6 +77,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
}),
tlsConfig: tlsConfig,
}
inbound.router = withUserSessions(inbound.router)
var sendBps, receiveBps uint64
if options.Up.Value() > 0 {
sendBps = options.Up.Value()
Expand Down
19 changes: 18 additions & 1 deletion core/protocol/hysteria/users.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package hysteria

import (
"github.com/shenaba/2s-ui/core/usersession"

"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
)

// UpdateUsers swaps the user table of a running inbound and disconnects
// everyone who just left it -- see the note in tuic/users.go, hysteria reuses
// an authenticated session the same way, and a removed user who is still
// connected costs the inbound a rebuild for the same reason.
func (h *Inbound) UpdateUsers(users []option.HysteriaUser) error {
userList := make([]string, 0, len(users))
userPasswordList := make([]string, 0, len(users))
Expand All @@ -18,5 +25,15 @@ func (h *Inbound) UpdateUsers(users []option.HysteriaUser) error {
userPasswordList = append(userPasswordList, password)
}
h.service.UpdateUsers(userList, userPasswordList)
return nil
return h.sessions().CloseUsers(usersession.KeepSet(userList)).RestartRequired()
}

// withUserSessions installs the session registry in front of the router. This
// inbound holds the full adapter.Router, so it takes the Router shim.
func withUserSessions(router adapter.Router) adapter.Router {
return usersession.WrapRouter(router, usersession.BindOnly)
}

func (h *Inbound) sessions() *usersession.Registry {
return h.router.(*usersession.Router).Registry()
}
12 changes: 11 additions & 1 deletion core/protocol/hysteria2/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// tearing the listener down (see core/inbound_users.go).
//
// UPGRADING sing-box: re-copy this file from the new tag and re-apply the
// change below. Nothing here will fail to compile if you forget, it will just
// changes below. Nothing here will fail to compile if you forget, it will just
// silently keep running the old implementation.
//
// Local change vs sing-box: the service is keyed by user name
Expand All @@ -14,6 +14,15 @@
// UpdateUsers does rewrite it, under live sessions, and a position is not.
// Deleting a user shifted every later one, which mis-attributed traffic and
// could index past the end of the name slice outright (upstream #1231).
//
// Second local change: one line installs the user-session registry
//
// inbound.router = withUserSessions(inbound.router)
//
// which is what lets an already authenticated session be reached when its user
// is removed from the inbound -- swapping the user table alone only decides who
// may start a new one. That line is the whole of it here; everything it reaches
// lives in users.go, which the copy check skips. See core/usersession.
package hysteria2

import (
Expand Down Expand Up @@ -139,6 +148,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
}),
tlsConfig: tlsConfig,
}
inbound.router = withUserSessions(inbound.router)
var udpTimeout time.Duration
if options.UDPTimeout != 0 {
udpTimeout = time.Duration(options.UDPTimeout)
Expand Down
19 changes: 18 additions & 1 deletion core/protocol/hysteria2/users.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package hysteria2

import (
"github.com/shenaba/2s-ui/core/usersession"

"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
)

// UpdateUsers swaps the user table of a running inbound and disconnects
// everyone who just left it -- see the note in tuic/users.go, hysteria2 reuses
// an authenticated session the same way, and a removed user who is still
// connected costs the inbound a rebuild for the same reason.
func (h *Inbound) UpdateUsers(users []option.Hysteria2User) error {
userList := make([]string, 0, len(users))
userPasswordList := make([]string, 0, len(users))
Expand All @@ -12,5 +19,15 @@ func (h *Inbound) UpdateUsers(users []option.Hysteria2User) error {
userPasswordList = append(userPasswordList, user.Password)
}
h.service.UpdateUsers(userList, userPasswordList)
return nil
return h.sessions().CloseUsers(usersession.KeepSet(userList)).RestartRequired()
}

// withUserSessions installs the session registry in front of the router. This
// inbound holds the full adapter.Router, so it takes the Router shim.
func withUserSessions(router adapter.Router) adapter.Router {
return usersession.WrapRouter(router, usersession.BindOnly)
}

func (h *Inbound) sessions() *usersession.Registry {
return h.router.(*usersession.Router).Registry()
}
12 changes: 11 additions & 1 deletion core/protocol/trojan/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// tearing the listener down (see core/inbound_users.go).
//
// UPGRADING sing-box: re-copy this file from the new tag and re-apply the
// change below. Nothing here will fail to compile if you forget, it will just
// changes below. Nothing here will fail to compile if you forget, it will just
// silently keep running the old implementation.
//
// Local change vs sing-box: the service is keyed by user name
Expand All @@ -14,6 +14,15 @@
// UpdateUsers does rewrite it, under live sessions, and a position is not.
// Deleting a user shifted every later one, which mis-attributed traffic and
// could index past the end of the name slice outright (upstream #1231).
//
// Second local change: one line installs the user-session registry
//
// inbound.router = withUserSessions(inbound.router)
//
// which is what lets an already authenticated session be reached when its user
// is removed from the inbound -- swapping the user table alone only decides who
// may start a new one. That line is the whole of it here; everything it reaches
// lives in users.go, which the copy check skips. See core/usersession.
package trojan

import (
Expand Down Expand Up @@ -118,6 +127,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if err != nil {
return nil, err
}
inbound.router = withUserSessions(inbound.router)
inbound.service = service
inbound.listener = listener.New(listener.Options{
Context: ctx,
Expand Down
31 changes: 30 additions & 1 deletion core/protocol/trojan/users.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
package trojan

import (
"github.com/shenaba/2s-ui/core/usersession"

"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
)

// UpdateUsers swaps the user table of a running inbound and cuts the multiplex
// sessions of everyone who just left it -- see the note in vless/users.go.
// Sessions are only cut once the table actually changed: trojan rejects a
// duplicate name and fails the whole update, and that must leave the inbound
// exactly as it was.
func (h *Inbound) UpdateUsers(users []option.TrojanUser) error {
return h.service.UpdateUsers(common.Map(users, func(it option.TrojanUser) string {
err := h.service.UpdateUsers(common.Map(users, func(it option.TrojanUser) string {
return it.Name
}), common.Map(users, func(it option.TrojanUser) string {
return it.Password
}))
if err != nil {
return err
}
// Unclosable is not checked here; see the note in vless/users.go.
h.sessions().CloseUsers(usersession.KeepSet(common.Map(users, func(it option.TrojanUser) string {
return it.Name
})))
return nil
}

// withUserSessions installs the session registry in front of the router. Only
// the multiplex carrier is tracked: every other connection authenticates on its
// own and is already covered by ConnTracker. The fallback path routes
// unauthenticated visitors through this same router, which is one more reason
// nothing here may refuse a connection.
func withUserSessions(router adapter.ConnectionRouterEx) adapter.ConnectionRouterEx {
return usersession.WrapRouterEx(router, usersession.TrackMuxCarrier)
}

func (h *Inbound) sessions() *usersession.Registry {
return h.router.(*usersession.RouterEx).Registry()
}
12 changes: 11 additions & 1 deletion core/protocol/tuic/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// tearing the listener down (see core/inbound_users.go).
//
// UPGRADING sing-box: re-copy this file from the new tag and re-apply the
// change below. Nothing here will fail to compile if you forget, it will just
// changes below. Nothing here will fail to compile if you forget, it will just
// silently keep running the old implementation.
//
// Local change vs sing-box: the service is keyed by user name
Expand All @@ -14,6 +14,15 @@
// UpdateUsers does rewrite it, under live sessions, and a position is not.
// Deleting a user shifted every later one, which mis-attributed traffic and
// could index past the end of the name slice outright (upstream #1231).
//
// Second local change: one line installs the user-session registry
//
// inbound.router = withUserSessions(inbound.router)
//
// which is what lets an already authenticated session be reached when its user
// is removed from the inbound -- swapping the user table alone only decides who
// may start a new one. That line is the whole of it here; everything it reaches
// lives in users.go, which the copy check skips. See core/usersession.
package tuic

import (
Expand Down Expand Up @@ -73,6 +82,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
}),
tlsConfig: tlsConfig,
}
inbound.router = withUserSessions(inbound.router)
var udpTimeout time.Duration
if options.UDPTimeout != 0 {
udpTimeout = time.Duration(options.UDPTimeout)
Expand Down
32 changes: 31 additions & 1 deletion core/protocol/tuic/users.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
package tuic

import (
"github.com/shenaba/2s-ui/core/usersession"

"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"

"github.com/gofrs/uuid/v5"
)

// UpdateUsers swaps the user table of a running inbound and disconnects
// everyone who just left it. Both halves are needed: the table alone only
// decides who may open a new session, while a client that authenticated before
// the change keeps opening streams on the one it already has.
//
// The second half is not something this layer can do to a QUIC session, so when
// a removed user is still connected it reports ErrRestartRequired and the
// caller rebuilds the inbound instead. That disconnects everyone on it once,
// which is why it is reported rather than done unconditionally: an update that
// removes nobody, or removes nobody who is connected, still costs nothing.
func (h *Inbound) UpdateUsers(users []option.TUICUser) error {
userList := make([]string, 0, len(users))
userUUIDList := make([][16]byte, 0, len(users))
Expand All @@ -24,5 +37,22 @@ func (h *Inbound) UpdateUsers(users []option.TUICUser) error {
userPasswordList = append(userPasswordList, user.Password)
}
h.server.UpdateUsers(userList, userUUIDList, userPasswordList)
return nil
return h.sessions().CloseUsers(usersession.KeepSet(userList)).RestartRequired()
}

// withUserSessions installs the session registry in front of the router. TUIC
// authenticates once per QUIC session and every stream after that rides it, so
// the registry is here to answer one question at removal time: is the user
// being removed actually connected? It cannot do more than that -- sing-quic
// hands out neither a handle on the session nor a stable name for it. See
// core/usersession.
func withUserSessions(router adapter.ConnectionRouterEx) adapter.ConnectionRouterEx {
return usersession.WrapRouterEx(router, usersession.BindOnly)
}

// sessions reaches the registry withUserSessions installed. The assertion is
// bare on purpose: reordering NewInbound so that it no longer holds would
// otherwise turn every hook here into a silent no-op.
func (h *Inbound) sessions() *usersession.Registry {
return h.router.(*usersession.RouterEx).Registry()
}
Loading
Loading