From 3fe02a658a08a09615a30d26d305ee0811589ac6 Mon Sep 17 00:00:00 2001 From: nobody <59990325+vrnobody@users.noreply.github.com> Date: Sat, 26 Jul 2025 16:40:04 +0800 Subject: [PATCH] Commands: Add adu/rmu inbound user management to API (#4943) Co-authored-by: nobody --- main/commands/all/api/api.go | 2 + main/commands/all/api/inbound_user_add.go | 144 +++++++++++++++++++ main/commands/all/api/inbound_user_remove.go | 62 ++++++++ 3 files changed, 208 insertions(+) create mode 100644 main/commands/all/api/inbound_user_add.go create mode 100644 main/commands/all/api/inbound_user_remove.go diff --git a/main/commands/all/api/api.go b/main/commands/all/api/api.go index 14a7c564..968213b6 100644 --- a/main/commands/all/api/api.go +++ b/main/commands/all/api/api.go @@ -23,6 +23,8 @@ var CmdAPI = &base.Command{ cmdRemoveOutbounds, cmdListInbounds, cmdListOutbounds, + cmdAddInboundUsers, + cmdRemoveInboundUsers, cmdInboundUser, cmdInboundUserCount, cmdAddRules, diff --git a/main/commands/all/api/inbound_user_add.go b/main/commands/all/api/inbound_user_add.go new file mode 100644 index 00000000..9f9acf7e --- /dev/null +++ b/main/commands/all/api/inbound_user_add.go @@ -0,0 +1,144 @@ +package api + +import ( + "context" + "fmt" + + "github.com/xtls/xray-core/common/protocol" + + handlerService "github.com/xtls/xray-core/app/proxyman/command" + cserial "github.com/xtls/xray-core/common/serial" + + "github.com/xtls/xray-core/core" + "github.com/xtls/xray-core/infra/conf" + "github.com/xtls/xray-core/infra/conf/serial" + "github.com/xtls/xray-core/proxy/shadowsocks" + "github.com/xtls/xray-core/proxy/shadowsocks_2022" + "github.com/xtls/xray-core/proxy/trojan" + vlessin "github.com/xtls/xray-core/proxy/vless/inbound" + vmessin "github.com/xtls/xray-core/proxy/vmess/inbound" + + "github.com/xtls/xray-core/main/commands/base" +) + +var cmdAddInboundUsers = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api adu [--server=127.0.0.1:8080] [c2.json]...", + Short: "Add users to inbounds", + Long: ` +Add users to inbounds. +Arguments: + -s, -server + The API server address. Default 127.0.0.1:8080 + -t, -timeout + Timeout seconds to call API. Default 3 +Example: + {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json +`, + Run: executeAddInboundUsers, +} + +func executeAddInboundUsers(cmd *base.Command, args []string) { + setSharedFlags(cmd) + cmd.Flag.Parse(args) + unnamedArgs := cmd.Flag.Args() + inbs := extractInboundsConfig(unnamedArgs) + + conn, ctx, close := dialAPIServer() + defer close() + client := handlerService.NewHandlerServiceClient(conn) + + success := 0 + for _, inb := range inbs { + success += executeInboundUserAction(ctx, client, inb, addInboundUserAction) + } + fmt.Println("Added", success, "user(s) in total.") +} + +func addInboundUserAction(ctx context.Context, client handlerService.HandlerServiceClient, tag string, user *protocol.User) error { + fmt.Println("add user:", user.Email) + _, err := client.AlterInbound(ctx, &handlerService.AlterInboundRequest{ + Tag: tag, + Operation: cserial.ToTypedMessage( + &handlerService.AddUserOperation{ + User: user, + }), + }) + return err +} + +func extractInboundUsers(inb *core.InboundHandlerConfig) []*protocol.User { + if inb == nil { + return nil + } + inst, err := inb.ProxySettings.GetInstance() + if err != nil || inst == nil { + fmt.Println("failed to get inbound instance:", err) + return nil + } + switch ty := inst.(type) { + case *vmessin.Config: + return ty.User + case *vlessin.Config: + return ty.Clients + case *trojan.ServerConfig: + return ty.Users + case *shadowsocks.ServerConfig: + return ty.Users + case *shadowsocks_2022.MultiUserServerConfig: + return ty.Users + default: + fmt.Println("unsupported inbound type") + } + return nil +} + +func extractInboundsConfig(unnamedArgs []string) []conf.InboundDetourConfig { + ins := make([]conf.InboundDetourConfig, 0) + for _, arg := range unnamedArgs { + r, err := loadArg(arg) + if err != nil { + base.Fatalf("failed to load %s: %s", arg, err) + } + conf, err := serial.DecodeJSONConfig(r) + if err != nil { + base.Fatalf("failed to decode %s: %s", arg, err) + } + ins = append(ins, conf.InboundConfigs...) + } + return ins +} + +func executeInboundUserAction(ctx context.Context, client handlerService.HandlerServiceClient, inb conf.InboundDetourConfig, action func(ctx context.Context, client handlerService.HandlerServiceClient, tag string, user *protocol.User) error) int { + success := 0 + + tag := inb.Tag + if len(tag) < 1 { + return success + } + + fmt.Println("processing inbound:", tag) + built, err := inb.Build() + if err != nil { + fmt.Println("failed to build config:", err) + return success + } + + users := extractInboundUsers(built) + if users == nil { + return success + } + + for _, user := range users { + if len(user.Email) < 1 { + continue + } + if err := action(ctx, client, inb.Tag, user); err == nil { + fmt.Println("result: ok") + success += 1 + } else { + fmt.Println(err) + } + } + return success +} diff --git a/main/commands/all/api/inbound_user_remove.go b/main/commands/all/api/inbound_user_remove.go new file mode 100644 index 00000000..d1261700 --- /dev/null +++ b/main/commands/all/api/inbound_user_remove.go @@ -0,0 +1,62 @@ +package api + +import ( + "fmt" + + handlerService "github.com/xtls/xray-core/app/proxyman/command" + cserial "github.com/xtls/xray-core/common/serial" + + "github.com/xtls/xray-core/main/commands/base" +) + +var cmdRemoveInboundUsers = &base.Command{ + CustomFlags: true, + UsageLine: "{{.Exec}} api rmu [--server=127.0.0.1:8080] -tag=tag [email2]...", + Short: "Remove users from inbounds", + Long: ` +Remove users from inbounds. +Arguments: + -s, -server + The API server address. Default 127.0.0.1:8080 + -t, -timeout + Timeout seconds to call API. Default 3 + -tag + Inbound tag +Example: + {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -tag="vless-in" "xray@love.com" ... +`, + Run: executeRemoveUsers, +} + +func executeRemoveUsers(cmd *base.Command, args []string) { + setSharedFlags(cmd) + var tag string + cmd.Flag.StringVar(&tag, "tag", "", "") + cmd.Flag.Parse(args) + emails := cmd.Flag.Args() + if len(tag) < 1 { + base.Fatalf("inbound tag not specified") + } + + conn, ctx, close := dialAPIServer() + defer close() + client := handlerService.NewHandlerServiceClient(conn) + + success := 0 + for _, email := range emails { + fmt.Println("remove user:", email) + _, err := client.AlterInbound(ctx, &handlerService.AlterInboundRequest{ + Tag: tag, + Operation: cserial.ToTypedMessage( + &handlerService.RemoveUserOperation{ + Email: email, + }), + }) + if err == nil { + success += 1 + } else { + fmt.Println(err) + } + } + fmt.Println("Removed", success, "user(s) in total.") +}