From 2d2c1e287e5f3dde873ae5c8ffbbd0fb5f7107f8 Mon Sep 17 00:00:00 2001 From: Saeed Date: Fri, 1 May 2026 11:05:09 +0330 Subject: [PATCH] add speed limiter for download and upload speed in megabytes --- database/model/model.go | 34 ++++++++++---------- go.mod | 2 ++ web/assets/js/model/inbound.js | 24 ++++++++++++++ web/html/form/client.html | 26 +++++++++++++++ web/service/inbound.go | 48 ++++++++++++++++------------ web/service/xray.go | 11 +++++-- web/translation/translate.en_US.toml | 3 ++ web/translation/translate.fa_IR.toml | 5 ++- xray/api.go | 41 +++++++++++++++++++++--- 9 files changed, 150 insertions(+), 44 deletions(-) diff --git a/database/model/model.go b/database/model/model.go index 01654d22..4b588af6 100644 --- a/database/model/model.go +++ b/database/model/model.go @@ -131,20 +131,22 @@ type CustomGeoResource struct { // Client represents a client configuration for Xray inbounds with traffic limits and settings. type Client struct { - ID string `json:"id,omitempty"` // Unique client identifier - Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm") - Password string `json:"password,omitempty"` // Client password - Flow string `json:"flow,omitempty"` // Flow control (XTLS) - Auth string `json:"auth,omitempty"` // Auth password (Hysteria) - Email string `json:"email"` // Client email identifier - LimitIP int `json:"limitIp"` // IP limit for this client - TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB - ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp - Enable bool `json:"enable" form:"enable"` // Whether the client is enabled - TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications - SubID string `json:"subId" form:"subId"` // Subscription identifier - Comment string `json:"comment" form:"comment"` // Client comment - Reset int `json:"reset" form:"reset"` // Reset period in days - CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp - UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp + ID string `json:"id,omitempty"` // Unique client identifier + Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm") + Password string `json:"password,omitempty"` // Client password + Flow string `json:"flow,omitempty"` // Flow control (XTLS) + Auth string `json:"auth,omitempty"` // Auth password (Hysteria) + Email string `json:"email"` // Client email identifier + LimitIP int `json:"limitIp"` // IP limit for this client + UploadSpeedLimit int64 `json:"uploadSpeedLimit" form:"uploadSpeedLimit"` // Upload speed limit in bytes/sec + DownloadSpeedLimit int64 `json:"downloadSpeedLimit" form:"downloadSpeedLimit"` // Download speed limit in bytes/sec + TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB + ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp + Enable bool `json:"enable" form:"enable"` // Whether the client is enabled + TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications + SubID string `json:"subId" form:"subId"` // Subscription identifier + Comment string `json:"comment" form:"comment"` // Client comment + Reset int `json:"reset" form:"reset"` // Reset period in days + CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp + UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp } diff --git a/go.mod b/go.mod index 406a86a8..311156e4 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/mhsanaei/3x-ui/v2 go 1.26.2 +replace github.com/xtls/xray-core => ../core + require ( github.com/gin-contrib/gzip v1.2.6 github.com/gin-contrib/sessions v1.1.0 diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index f695d251..a4845554 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -2387,6 +2387,8 @@ Inbound.ClientBase = class extends XrayCommonClass { constructor( email = RandomUtil.randomLowerAndNum(8), limitIp = 0, + uploadSpeedLimit = 0, + downloadSpeedLimit = 0, totalGB = 0, expiryTime = 0, enable = true, @@ -2400,6 +2402,8 @@ Inbound.ClientBase = class extends XrayCommonClass { super(); this.email = email; this.limitIp = limitIp; + this.uploadSpeedLimit = uploadSpeedLimit; + this.downloadSpeedLimit = downloadSpeedLimit; this.totalGB = totalGB; this.expiryTime = expiryTime; this.enable = enable; @@ -2415,6 +2419,8 @@ Inbound.ClientBase = class extends XrayCommonClass { return [ json.email, json.limitIp, + json.uploadSpeedLimit, + json.downloadSpeedLimit, json.totalGB, json.expiryTime, json.enable, @@ -2431,6 +2437,8 @@ Inbound.ClientBase = class extends XrayCommonClass { return { email: this.email, limitIp: this.limitIp, + uploadSpeedLimit: this.uploadSpeedLimit, + downloadSpeedLimit: this.downloadSpeedLimit, totalGB: this.totalGB, expiryTime: this.expiryTime, enable: this.enable, @@ -2468,6 +2476,22 @@ Inbound.ClientBase = class extends XrayCommonClass { set _totalGB(gb) { this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0); } + + get _uploadSpeedLimitMB() { + return NumberFormatter.toFixed(this.uploadSpeedLimit / SizeFormatter.ONE_MB, 2); + } + + set _uploadSpeedLimitMB(mb) { + this.uploadSpeedLimit = NumberFormatter.toFixed((mb || 0) * SizeFormatter.ONE_MB, 0); + } + + get _downloadSpeedLimitMB() { + return NumberFormatter.toFixed(this.downloadSpeedLimit / SizeFormatter.ONE_MB, 2); + } + + set _downloadSpeedLimitMB(mb) { + this.downloadSpeedLimit = NumberFormatter.toFixed((mb || 0) * SizeFormatter.ONE_MB, 0); + } }; Inbound.VmessSettings = class extends Inbound.Settings { diff --git a/web/html/form/client.html b/web/html/form/client.html index 9c6a7f8a..876e825b 100644 --- a/web/html/form/client.html +++ b/web/html/form/client.html @@ -139,6 +139,32 @@ + + + + MB/s + + + + + MB/s + diff --git a/web/service/inbound.go b/web/service/inbound.go index 8ab5e6a8..0618400f 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -733,13 +733,15 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) { cipher = oldSettings["method"].(string) } err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{ - "email": client.Email, - "id": client.ID, - "auth": client.Auth, - "security": client.Security, - "flow": client.Flow, - "password": client.Password, - "cipher": cipher, + "email": client.Email, + "id": client.ID, + "auth": client.Auth, + "security": client.Security, + "flow": client.Flow, + "password": client.Password, + "cipher": cipher, + "uploadSpeedLimit": client.UploadSpeedLimit, + "downloadSpeedLimit": client.DownloadSpeedLimit, }) if err1 == nil { logger.Debug("Client added by api:", client.Email) @@ -1199,13 +1201,15 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin cipher = oldSettings["method"].(string) } err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{ - "email": clients[0].Email, - "id": clients[0].ID, - "security": clients[0].Security, - "flow": clients[0].Flow, - "auth": clients[0].Auth, - "password": clients[0].Password, - "cipher": cipher, + "email": clients[0].Email, + "id": clients[0].ID, + "security": clients[0].Security, + "flow": clients[0].Flow, + "auth": clients[0].Auth, + "password": clients[0].Password, + "cipher": cipher, + "uploadSpeedLimit": clients[0].UploadSpeedLimit, + "downloadSpeedLimit": clients[0].DownloadSpeedLimit, }) if err1 == nil { logger.Debug("Client edited by api:", clients[0].Email) @@ -2094,13 +2098,15 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, e cipher = oldSettings["method"].(string) } err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]any{ - "email": client.Email, - "id": client.ID, - "auth": client.Auth, - "security": client.Security, - "flow": client.Flow, - "password": client.Password, - "cipher": cipher, + "email": client.Email, + "id": client.ID, + "auth": client.Auth, + "security": client.Security, + "flow": client.Flow, + "password": client.Password, + "cipher": cipher, + "uploadSpeedLimit": client.UploadSpeedLimit, + "downloadSpeedLimit": client.DownloadSpeedLimit, }) if err1 == nil { logger.Debug("Client enabled due to reset traffic:", clientEmail) diff --git a/web/service/xray.go b/web/service/xray.go index 958d36d2..5f0d4710 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -146,9 +146,16 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { continue } - // clear client config for additional parameters + // Keep only fields that are needed by Xray runtime config generation. for key := range c { - if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" && key != "auth" { + if key != "email" && + key != "id" && + key != "password" && + key != "flow" && + key != "method" && + key != "auth" && + key != "uploadSpeedLimit" && + key != "downloadSpeedLimit" { delete(c, key) } if flow, ok := c["flow"].(string); ok && flow == "xtls-rprx-vision-udp443" { diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 49c9f952..5ed31417 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -256,6 +256,7 @@ "targetAddress" = "Target Address" "monitorDesc" = "Leave blank to listen on all IPs" "meansNoLimit" = "= Unlimited. (unit: GB)" +"meansNoLimitMB" = "= Unlimited. (unit: MB)" "totalFlow" = "Total Flow" "leaveBlankToNeverExpire" = "Leave blank to never expire" "noRecommendKeepDefault" = "It is recommended to keep the default" @@ -289,6 +290,8 @@ "IPLimitlog" = "IP Log" "IPLimitlogDesc" = "The IPs history log. (to enable inbound after disabling, clear the log)" "IPLimitlogclear" = "Clear The Log" +"uploadSpeedLimit" = "Upload Speed" +"downloadSpeedLimit" = "Download Speed" "setDefaultCert" = "Set Cert from Panel" "telegramDesc" = "Please provide Telegram Chat ID. (use '/id' command in the bot) or (@userinfobot)" "subscriptionDesc" = "To find your subscription URL, navigate to the 'Details'. Additionally, you can use the same name for several clients." diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index e1f49b80..604f8ab7 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -255,7 +255,8 @@ "destinationPort" = "پورت مقصد" "targetAddress" = "آدرس مقصد" "monitorDesc" = "به‌طور پیش‌فرض خالی‌بگذارید" -"meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)" +"meansNoLimit" = "= نامحدود. (واحد: گیگابایت)" +"meansNoLimitMB" = "= نامحدود. (واحد: مگابایت)" "totalFlow" = "ترافیک کل" "leaveBlankToNeverExpire" = "برای منقضی‌نشدن خالی‌بگذارید" "noRecommendKeepDefault" = "توصیه‌می‌شود به‌طور پیش‌فرض حفظ‌شود" @@ -289,6 +290,8 @@ "IPLimitlog" = "گزارش‌ها" "IPLimitlogDesc" = "گزارش تاریخچه آی‌پی. برای فعال کردن ورودی پس از غیرفعال شدن، گزارش را پاک کنید" "IPLimitlogclear" = "پاک کردن گزارش‌ها" +"uploadSpeedLimit" = "سرعت آپلود" +"downloadSpeedLimit" = "سرعت دانلود" "setDefaultCert" = "استفاده از گواهی پنل" "telegramDesc" = "لطفا شناسه گفتگوی تلگرام را وارد کنید. (از دستور '/id' در ربات استفاده کنید) یا (@userinfobot)" "subscriptionDesc" = "شما می‌توانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین می‌توانید از همین نام برای چندین کاربر استفاده‌کنید" diff --git a/xray/api.go b/xray/api.go index bfb64665..7c17a481 100644 --- a/xray/api.go +++ b/xray/api.go @@ -65,6 +65,28 @@ func getOptionalUserString(user map[string]any, key string) (string, error) { return strValue, nil } +func getOptionalUserInt64(user map[string]any, key string) (int64, error) { + value, ok := user[key] + if !ok || value == nil { + return 0, nil + } + + switch v := value.(type) { + case int: + return int64(v), nil + case int32: + return int64(v), nil + case int64: + return v, nil + case float64: + return int64(v), nil + case json.Number: + return v.Int64() + default: + return 0, fmt.Errorf("invalid type for user field %q: %T", key, value) + } +} + // Init connects to the Xray API server and initializes handler and stats service clients. func (x *XrayAPI) Init(apiPort int) error { if apiPort <= 0 || apiPort > math.MaxUint16 { @@ -136,6 +158,14 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an if err != nil { return err } + uploadSpeedLimit, err := getOptionalUserInt64(user, "uploadSpeedLimit") + if err != nil { + return err + } + downloadSpeedLimit, err := getOptionalUserInt64(user, "downloadSpeedLimit") + if err != nil { + return err + } var account *serial.TypedMessage switch Protocol { @@ -241,14 +271,17 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an } client := *x.HandlerServiceClient + protocolUser := &protocol.User{ + Email: userEmail, + Account: account, + UplinkSpeedLimit: uint64(uploadSpeedLimit), + DownlinkSpeedLimit: uint64(downloadSpeedLimit), + } _, err = client.AlterInbound(context.Background(), &command.AlterInboundRequest{ Tag: inboundTag, Operation: serial.ToTypedMessage(&command.AddUserOperation{ - User: &protocol.User{ - Email: userEmail, - Account: account, - }, + User: protocolUser, }), }) return err