fix(hysteria2): preserve user identity across hot updates (#1231)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>
Co-authored-by: Alireza Ahmadi <alireza7@gmail.com>
This commit is contained in:
LiGaAmG 2026-08-31 14:57:48 +03:00 • committed by GitHub
parent 88bde26fcf
commit bb4baebb13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 147 additions and 145 deletions

View file

@ -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 {

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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