From bb4baebb13a8d7033896aa12cad8bd4acecaa2bb Mon Sep 17 00:00:00 2001 From: LiGaAmG Date: Mon, 31 Aug 2026 14:57:48 +0300 Subject: [PATCH] fix(hysteria2): preserve user identity across hot updates (#1231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(hysteria2): use stable user identity on hot updates Replace Service[int] (position-based) with Service[string] (name-based) so that existing sessions retain a correct stable identity when the user list changes via UpdateUsers. Remove the now-redundant userNameList field. Fixes panic: runtime error: index out of range after user deletion, and wrong username attribution when list positions shift. * fix(hysteria2): gofmt struct field alignment after userNameList removal * fix: extend stable user identity fix to hysteria, tuic, trojan, vless, vmess The same Service[int] → Service[string] bug fixed for hysteria2 in the previous commit affects all five remaining inbound protocols. When users are deleted or reordered, integer position-based identity causes panics (index out of range) and wrong username attribution in logs and traffic stats. Apply the same fix across hysteria v1, tuic, trojan, vless, and vmess: - Service[int] → Service[string] in all five inbound structs - Pass user.Name as the stable identity instead of the list index - Remove the now-unnecessary cached `users []option.*User` fields from trojan, vless, and vmess structs (they existed solely to map indexes back to names) - UpdateUsers helpers rebuilt to pass names, keeping auth/password fields unchanged Co-Authored-By: Claude Sonnet 4.6 * ci: add go vet + gofmt check for patched protocols * fix: gofmt struct field alignment in hysteria and tuic inbound * fix: address review feedback on stable-identity PR Three fixes from maintainer review: 1. Logging regression (trojan/vless/vmess): when a client has no name, the old code fell back to the index as a log label; the new code was emitting `[] inbound connection`. Align with the hysteria2 pattern: omit the `[name]` prefix entirely when name is empty. 2. Duplicate/empty client names: trojan's Service.UpdateUsers rejects a repeated identity, so two clients with the same name on the same inbound would make UpdateUsers fail. vmess silently overwrites the earlier entry. vless misattributes flow. Add server-side validation in ClientService.Save (new, edit, addbulk) that rejects empty names and names already in use on any shared inbound within the same tx. 3. Remove .github/workflows/check.yml: the workflow only triggered on the feature branch, so it would be permanently dead after merge. * fix: make client name validation global and trim whitespace * ci: temporarily re-add check workflow for CI verification * ci: remove temporary check workflow * validate client names on bulk save paths too --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Alireza Ahmadi --- core/protocol/hysteria/inbound.go | 28 ++++++++----------- core/protocol/hysteria/users.go | 9 ++---- core/protocol/hysteria2/inbound.go | 28 ++++++++----------- core/protocol/hysteria2/users.go | 9 ++---- core/protocol/trojan/inbound.go | 33 ++++++++++------------ core/protocol/trojan/users.go | 9 ++---- core/protocol/tuic/inbound.go | 26 +++++++---------- core/protocol/tuic/users.go | 7 ++--- core/protocol/vless/inbound.go | 45 +++++++++++++++--------------- core/protocol/vless/users.go | 5 ++-- core/protocol/vmess/inbound.go | 41 ++++++++++++++------------- core/protocol/vmess/users.go | 9 ++---- service/client.go | 43 ++++++++++++++++++++++++++++ 13 files changed, 147 insertions(+), 145 deletions(-) diff --git a/core/protocol/hysteria/inbound.go b/core/protocol/hysteria/inbound.go index 98d7cb8..4b35c19 100644 --- a/core/protocol/hysteria/inbound.go +++ b/core/protocol/hysteria/inbound.go @@ -25,12 +25,11 @@ func RegisterInbound(registry *inbound.Registry) { type Inbound struct { inbound.Adapter - router adapter.Router - logger log.ContextLogger - listener *listener.Listener - tlsConfig tls.ServerConfig - service *hysteria.Service[int] - userNameList []string + router adapter.Router + logger log.ContextLogger + listener *listener.Listener + tlsConfig tls.ServerConfig + service *hysteria.Service[string] } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) { @@ -70,7 +69,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } else { udpTimeout = C.UDPTimeout } - service, err := hysteria.NewService[int](hysteria.ServiceOptions{ + service, err := hysteria.NewService[string](hysteria.ServiceOptions{ Context: ctx, Logger: logger, SendBPS: sendBps, @@ -90,12 +89,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if err != nil { return nil, err } - userList := make([]int, 0, len(options.Users)) - userNameList := make([]string, 0, len(options.Users)) + userList := make([]string, 0, len(options.Users)) userPasswordList := make([]string, 0, len(options.Users)) - for index, user := range options.Users { - userList = append(userList, index) - userNameList = append(userNameList, user.Name) + for _, user := range options.Users { + userList = append(userList, user.Name) var password string if user.AuthString != "" { password = user.AuthString @@ -106,7 +103,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } service.UpdateUsers(userList, userPasswordList) inbound.service = service - inbound.userNameList = userNameList return inbound, nil } @@ -122,8 +118,7 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.S metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - userID, _ := auth.UserFromContext[int](ctx) - if userName := h.userNameList[userID]; userName != "" { + if userName, _ := auth.UserFromContext[string](ctx); userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) } else { @@ -144,8 +139,7 @@ func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) - userID, _ := auth.UserFromContext[int](ctx) - if userName := h.userNameList[userID]; userName != "" { + if userName, _ := auth.UserFromContext[string](ctx); userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", metadata.Destination) } else { diff --git a/core/protocol/hysteria/users.go b/core/protocol/hysteria/users.go index 27e740f..b36b471 100644 --- a/core/protocol/hysteria/users.go +++ b/core/protocol/hysteria/users.go @@ -5,12 +5,10 @@ import ( ) func (h *Inbound) UpdateUsers(users []option.HysteriaUser) error { - userList := make([]int, 0, len(users)) - userNameList := make([]string, 0, len(users)) + userList := make([]string, 0, len(users)) userPasswordList := make([]string, 0, len(users)) - for index, user := range users { - userList = append(userList, index) - userNameList = append(userNameList, user.Name) + for _, user := range users { + userList = append(userList, user.Name) var password string if user.AuthString != "" { password = user.AuthString @@ -20,6 +18,5 @@ func (h *Inbound) UpdateUsers(users []option.HysteriaUser) error { userPasswordList = append(userPasswordList, password) } h.service.UpdateUsers(userList, userPasswordList) - h.userNameList = userNameList return nil } diff --git a/core/protocol/hysteria2/inbound.go b/core/protocol/hysteria2/inbound.go index bb59807..7896585 100644 --- a/core/protocol/hysteria2/inbound.go +++ b/core/protocol/hysteria2/inbound.go @@ -30,12 +30,11 @@ func RegisterInbound(registry *inbound.Registry) { type Inbound struct { inbound.Adapter - router adapter.Router - logger log.ContextLogger - listener *listener.Listener - tlsConfig tls.ServerConfig - service *hysteria2.Service[int] - userNameList []string + router adapter.Router + logger log.ContextLogger + listener *listener.Listener + tlsConfig tls.ServerConfig + service *hysteria2.Service[string] } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) { @@ -113,7 +112,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } else { udpTimeout = C.UDPTimeout } - service, err := hysteria2.NewService[int](hysteria2.ServiceOptions{ + service, err := hysteria2.NewService[string](hysteria2.ServiceOptions{ Context: ctx, Logger: logger, BrutalDebug: options.BrutalDebug, @@ -129,17 +128,14 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if err != nil { return nil, err } - userList := make([]int, 0, len(options.Users)) - userNameList := make([]string, 0, len(options.Users)) + userList := make([]string, 0, len(options.Users)) userPasswordList := make([]string, 0, len(options.Users)) - for index, user := range options.Users { - userList = append(userList, index) - userNameList = append(userNameList, user.Name) + for _, user := range options.Users { + userList = append(userList, user.Name) userPasswordList = append(userPasswordList, user.Password) } service.UpdateUsers(userList, userPasswordList) inbound.service = service - inbound.userNameList = userNameList return inbound, nil } @@ -155,8 +151,7 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.S metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - userID, _ := auth.UserFromContext[int](ctx) - if userName := h.userNameList[userID]; userName != "" { + if userName, _ := auth.UserFromContext[string](ctx); userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) } else { @@ -177,8 +172,7 @@ func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) - userID, _ := auth.UserFromContext[int](ctx) - if userName := h.userNameList[userID]; userName != "" { + if userName, _ := auth.UserFromContext[string](ctx); userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", metadata.Destination) } else { diff --git a/core/protocol/hysteria2/users.go b/core/protocol/hysteria2/users.go index ce1dd3c..2038dc8 100644 --- a/core/protocol/hysteria2/users.go +++ b/core/protocol/hysteria2/users.go @@ -5,15 +5,12 @@ import ( ) func (h *Inbound) UpdateUsers(users []option.Hysteria2User) error { - userList := make([]int, 0, len(users)) - userNameList := make([]string, 0, len(users)) + userList := make([]string, 0, len(users)) userPasswordList := make([]string, 0, len(users)) - for index, user := range users { - userList = append(userList, index) - userNameList = append(userNameList, user.Name) + for _, user := range users { + userList = append(userList, user.Name) userPasswordList = append(userPasswordList, user.Password) } h.service.UpdateUsers(userList, userPasswordList) - h.userNameList = userNameList return nil } diff --git a/core/protocol/trojan/inbound.go b/core/protocol/trojan/inbound.go index 6e11c08..4e90bad 100644 --- a/core/protocol/trojan/inbound.go +++ b/core/protocol/trojan/inbound.go @@ -18,7 +18,6 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -34,8 +33,7 @@ type Inbound struct { router adapter.ConnectionRouterEx logger log.ContextLogger listener *listener.Listener - service *trojan.Service[int] - users []option.TrojanUser + service *trojan.Service[string] tlsConfig tls.ServerConfig fallbackAddr M.Socksaddr fallbackAddrTLSNextProto map[string]M.Socksaddr @@ -47,7 +45,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo Adapter: inbound.NewAdapter(C.TypeTrojan, tag), router: router, logger: logger, - users: options.Users, } if options.TLS != nil { tlsConfig, err := tls.NewServerWithOptions(tls.ServerOptions{ @@ -86,9 +83,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } fallbackHandler = adapter.NewUpstreamContextHandlerEx(inbound.fallbackConnection, nil) } - service := trojan.NewService[int](adapter.NewUpstreamContextHandlerEx(inbound.newConnection, inbound.newPacketConnection), fallbackHandler, logger) - err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int { - return index + service := trojan.NewService[string](adapter.NewUpstreamContextHandlerEx(inbound.newConnection, inbound.newPacketConnection), fallbackHandler, logger) + err := service.UpdateUsers(common.Map(options.Users, func(it option.TrojanUser) string { + return it.Name }), common.Map(options.Users, func(it option.TrojanUser) string { return it.Password })) @@ -184,36 +181,34 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - userIndex, loaded := auth.UserFromContext[int](ctx) + user, loaded := auth.UserFromContext[string](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } - user := h.users[userIndex].Name - if user == "" { - user = F.ToString(userIndex) - } else { + if user != "" { metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } - h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - userIndex, loaded := auth.UserFromContext[int](ctx) + user, loaded := auth.UserFromContext[string](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } - user := h.users[userIndex].Name - if user == "" { - user = F.ToString(userIndex) - } else { + if user != "" { metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) } - h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } diff --git a/core/protocol/trojan/users.go b/core/protocol/trojan/users.go index 41db1c4..a90e187 100644 --- a/core/protocol/trojan/users.go +++ b/core/protocol/trojan/users.go @@ -6,14 +6,9 @@ import ( ) func (h *Inbound) UpdateUsers(users []option.TrojanUser) error { - err := h.service.UpdateUsers(common.MapIndexed(users, func(index int, _ option.TrojanUser) int { - return index + return 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 - } - h.users = users - return nil } diff --git a/core/protocol/tuic/inbound.go b/core/protocol/tuic/inbound.go index 600c7f9..3264052 100644 --- a/core/protocol/tuic/inbound.go +++ b/core/protocol/tuic/inbound.go @@ -29,12 +29,11 @@ func RegisterInbound(registry *inbound.Registry) { type Inbound struct { inbound.Adapter - router adapter.ConnectionRouterEx - logger log.ContextLogger - listener *listener.Listener - tlsConfig tls.ServerConfig - server *tuic.Service[int] - userNameList []string + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener + tlsConfig tls.ServerConfig + server *tuic.Service[string] } func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) { @@ -63,7 +62,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } else { udpTimeout = C.UDPTimeout } - service, err := tuic.NewService[int](tuic.ServiceOptions{ + service, err := tuic.NewService[string](tuic.ServiceOptions{ Context: ctx, Logger: logger, TLSConfig: tlsConfig, @@ -77,8 +76,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if err != nil { return nil, err } - var userList []int - var userNameList []string + var userList []string var userUUIDList [][16]byte var userPasswordList []string for index, user := range options.Users { @@ -89,14 +87,12 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if err != nil { return nil, E.Cause(err, "invalid uuid for user ", index) } - userList = append(userList, index) - userNameList = append(userNameList, user.Name) + userList = append(userList, user.Name) userUUIDList = append(userUUIDList, userUUID) userPasswordList = append(userPasswordList, user.Password) } service.UpdateUsers(userList, userUUIDList, userPasswordList) inbound.server = service - inbound.userNameList = userNameList return inbound, nil } @@ -112,8 +108,7 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.S metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - userID, _ := auth.UserFromContext[int](ctx) - if userName := h.userNameList[userID]; userName != "" { + if userName, _ := auth.UserFromContext[string](ctx); userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) } else { @@ -134,8 +129,7 @@ func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) - userID, _ := auth.UserFromContext[int](ctx) - if userName := h.userNameList[userID]; userName != "" { + if userName, _ := auth.UserFromContext[string](ctx); userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound packet connection to ", metadata.Destination) } else { diff --git a/core/protocol/tuic/users.go b/core/protocol/tuic/users.go index fd21188..8c4624f 100644 --- a/core/protocol/tuic/users.go +++ b/core/protocol/tuic/users.go @@ -8,8 +8,7 @@ import ( ) func (h *Inbound) UpdateUsers(users []option.TUICUser) error { - userList := make([]int, 0, len(users)) - userNameList := make([]string, 0, len(users)) + userList := make([]string, 0, len(users)) userUUIDList := make([][16]byte, 0, len(users)) userPasswordList := make([]string, 0, len(users)) for index, user := range users { @@ -20,12 +19,10 @@ func (h *Inbound) UpdateUsers(users []option.TUICUser) error { if err != nil { return E.Cause(err, "invalid uuid for user ", index) } - userList = append(userList, index) - userNameList = append(userNameList, user.Name) + userList = append(userList, user.Name) userUUIDList = append(userUUIDList, userUUID) userPasswordList = append(userPasswordList, user.Password) } h.server.UpdateUsers(userList, userUUIDList, userPasswordList) - h.userNameList = userNameList return nil } diff --git a/core/protocol/vless/inbound.go b/core/protocol/vless/inbound.go index 75cd412..0f4f5dd 100644 --- a/core/protocol/vless/inbound.go +++ b/core/protocol/vless/inbound.go @@ -21,7 +21,6 @@ import ( "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -39,8 +38,7 @@ type Inbound struct { router adapter.ConnectionRouterEx logger logger.ContextLogger listener *listener.Listener - users []option.VLESSUser - service *vless.Service[int] + service *vless.Service[string] tlsConfig tls.ServerConfig transport adapter.V2RayServerTransport } @@ -51,19 +49,18 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo ctx: ctx, router: uot.NewRouter(router, logger), logger: logger, - users: options.Users, } var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { return nil, err } - service := vless.NewService[int](logger, adapter.NewUpstreamContextHandlerEx(inbound.newConnectionEx, inbound.newPacketConnectionEx)) - service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int { - return index - }), common.Map(inbound.users, func(it option.VLESSUser) string { + service := vless.NewService[string](logger, adapter.NewUpstreamContextHandlerEx(inbound.newConnectionEx, inbound.newPacketConnectionEx)) + service.UpdateUsers(common.Map(options.Users, func(it option.VLESSUser) string { + return it.Name + }), common.Map(options.Users, func(it option.VLESSUser) string { return it.UUID - }), common.Map(inbound.users, func(it option.VLESSUser) string { + }), common.Map(options.Users, func(it option.VLESSUser) string { return it.Flow })) inbound.service = service @@ -167,41 +164,45 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - userIndex, loaded := auth.UserFromContext[int](ctx) + user, loaded := auth.UserFromContext[string](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } - user := h.users[userIndex].Name - if user == "" { - user = F.ToString(userIndex) - } else { + if user != "" { metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } - h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - userIndex, loaded := auth.UserFromContext[int](ctx) + user, loaded := auth.UserFromContext[string](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } - user := h.users[userIndex].Name - if user == "" { - user = F.ToString(userIndex) - } else { + if user != "" { metadata.User = user } if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress { metadata.Destination = M.Socksaddr{} conn = packetaddr.NewConn(bufio.NewNetPacketConn(conn), metadata.Destination) - h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection") + if user != "" { + h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection") + } else { + h.logger.InfoContext(ctx, "inbound packet addr connection") + } } else { - h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + if user != "" { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } diff --git a/core/protocol/vless/users.go b/core/protocol/vless/users.go index 80d9209..c436ee4 100644 --- a/core/protocol/vless/users.go +++ b/core/protocol/vless/users.go @@ -6,13 +6,12 @@ import ( ) func (h *Inbound) UpdateUsers(users []option.VLESSUser) error { - h.service.UpdateUsers(common.MapIndexed(users, func(index int, _ option.VLESSUser) int { - return index + h.service.UpdateUsers(common.Map(users, func(it option.VLESSUser) string { + return it.Name }), common.Map(users, func(it option.VLESSUser) string { return it.UUID }), common.Map(users, func(it option.VLESSUser) string { return it.Flow })) - h.users = users return nil } diff --git a/core/protocol/vmess/inbound.go b/core/protocol/vmess/inbound.go index 3a1630f..9dbc203 100644 --- a/core/protocol/vmess/inbound.go +++ b/core/protocol/vmess/inbound.go @@ -21,7 +21,6 @@ import ( "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -40,8 +39,7 @@ type Inbound struct { router adapter.ConnectionRouterEx logger logger.ContextLogger listener *listener.Listener - service *vmess.Service[int] - users []option.VMessUser + service *vmess.Service[string] tlsConfig tls.ServerConfig transport adapter.V2RayServerTransport } @@ -52,7 +50,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo ctx: ctx, router: uot.NewRouter(router, logger), logger: logger, - users: options.Users, } var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) @@ -66,10 +63,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if options.Transport != nil && options.Transport.Type != "" { serviceOptions = append(serviceOptions, vmess.ServiceWithDisableHeaderProtection()) } - service := vmess.NewService[int](adapter.NewUpstreamContextHandlerEx(inbound.newConnectionEx, inbound.newPacketConnectionEx), serviceOptions...) + service := vmess.NewService[string](adapter.NewUpstreamContextHandlerEx(inbound.newConnectionEx, inbound.newPacketConnectionEx), serviceOptions...) inbound.service = service - err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int { - return index + err = service.UpdateUsers(common.Map(options.Users, func(it option.VMessUser) string { + return it.Name }), common.Map(options.Users, func(it option.VMessUser) string { return it.UUID }), common.Map(options.Users, func(it option.VMessUser) int { @@ -173,41 +170,45 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - userIndex, loaded := auth.UserFromContext[int](ctx) + user, loaded := auth.UserFromContext[string](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } - user := h.users[userIndex].Name - if user == "" { - user = F.ToString(userIndex) - } else { + if user != "" { metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } - h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - userIndex, loaded := auth.UserFromContext[int](ctx) + user, loaded := auth.UserFromContext[string](ctx) if !loaded { N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) return } - user := h.users[userIndex].Name - if user == "" { - user = F.ToString(userIndex) - } else { + if user != "" { metadata.User = user } if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress { metadata.Destination = M.Socksaddr{} conn = packetaddr.NewConn(bufio.NewNetPacketConn(conn), metadata.Destination) - h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection") + if user != "" { + h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection") + } else { + h.logger.InfoContext(ctx, "inbound packet addr connection") + } } else { - h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + if user != "" { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + } else { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } diff --git a/core/protocol/vmess/users.go b/core/protocol/vmess/users.go index fd04c1d..b98d310 100644 --- a/core/protocol/vmess/users.go +++ b/core/protocol/vmess/users.go @@ -6,16 +6,11 @@ import ( ) func (h *Inbound) UpdateUsers(users []option.VMessUser) error { - err := h.service.UpdateUsers(common.MapIndexed(users, func(index int, _ option.VMessUser) int { - return index + return h.service.UpdateUsers(common.Map(users, func(it option.VMessUser) string { + return it.Name }), common.Map(users, func(it option.VMessUser) string { return it.UUID }), common.Map(users, func(it option.VMessUser) int { return it.AlterId })) - if err != nil { - return err - } - h.users = users - return nil } diff --git a/service/client.go b/service/client.go index 8d950ba..5a114d9 100644 --- a/service/client.go +++ b/service/client.go @@ -47,6 +47,28 @@ func (s *ClientService) GetAll() (*[]model.Client, error) { return &clients, nil } +// validateClientName rejects empty names and globally duplicate names, +// then stores the trimmed name back on the client. +func (s *ClientService) validateClientName(tx *gorm.DB, client *model.Client) error { + name := strings.TrimSpace(client.Name) + if name == "" { + return common.NewError("client name must not be empty") + } + query := tx.Model(model.Client{}).Where("name = ?", name) + if client.Id != 0 { + query = query.Where("id != ?", client.Id) + } + var count int64 + if err := query.Count(&count).Error; err != nil { + return err + } + if count > 0 { + return common.NewErrorf("client name %q is already in use", name) + } + client.Name = name + return nil +} + func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, hostname string) ([]uint, error) { var err error var inboundIds []uint @@ -58,6 +80,9 @@ func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, host if err != nil { return nil, err } + if err = s.validateClientName(tx, &client); err != nil { + return nil, err + } if err = setConfigIdentity(&client); err != nil { return nil, err } @@ -91,7 +116,17 @@ func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, host return nil, err } now := time.Now().Unix() + // Every client is validated before any of them is written, so the + // batch has to be checked against itself as well as against the table. + seen := make(map[string]bool, len(clients)) for _, client := range clients { + if err = s.validateClientName(tx, client); err != nil { + return nil, err + } + if seen[client.Name] { + return nil, common.NewErrorf("duplicate client name %q in request", client.Name) + } + seen[client.Name] = true if err = setConfigIdentity(client); err != nil { return nil, err } @@ -116,7 +151,15 @@ func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, host if err != nil { return nil, err } + seen := make(map[string]bool, len(clients)) for _, client := range clients { + if err = s.validateClientName(tx, client); err != nil { + return nil, err + } + if seen[client.Name] { + return nil, common.NewErrorf("duplicate client name %q in request", client.Name) + } + seen[client.Name] = true changedInboundIds, err := s.findInboundsChanges(tx, client, true) if err != nil { return nil, err