diff --git a/database/model/model.go b/database/model/model.go
index 047780e59d..3ebfb1cf31 100644
--- a/database/model/model.go
+++ b/database/model/model.go
@@ -135,21 +135,22 @@ type ClientReverse 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)
- Reverse *ClientReverse `json:"reverse,omitempty"` // VLESS simple reverse proxy settings
- 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 0a967ebf29..880608387d 100644
--- a/go.mod
+++ b/go.mod
@@ -2,6 +2,8 @@ module github.com/mhsanaei/3x-ui/v2
go 1.26.3
+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 a3c1d017b1..fd4996f77a 100644
--- a/web/assets/js/model/inbound.js
+++ b/web/assets/js/model/inbound.js
@@ -2458,6 +2458,8 @@ Inbound.ClientBase = class extends XrayCommonClass {
constructor(
email = RandomUtil.randomLowerAndNum(8),
limitIp = 0,
+ uploadSpeedLimit = 0,
+ downloadSpeedLimit = 0,
totalGB = 0,
expiryTime = 0,
enable = true,
@@ -2471,6 +2473,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;
@@ -2486,6 +2490,8 @@ Inbound.ClientBase = class extends XrayCommonClass {
return [
json.email,
json.limitIp,
+ json.uploadSpeedLimit,
+ json.downloadSpeedLimit,
json.totalGB,
json.expiryTime,
json.enable,
@@ -2502,6 +2508,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,
@@ -2539,6 +2547,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 737c102e95..352e18de5f 100644
--- a/web/html/form/client.html
+++ b/web/html/form/client.html
@@ -99,6 +99,35 @@
+
+
+
+
+ 0 {{ i18n "pages.inbounds.meansNoLimitMB" }}
+
+ {{ i18n "pages.inbounds.uploadSpeedLimit" }}
+
+
+
+
+ MB/s
+
+
+
+
+
+ 0 {{ i18n "pages.inbounds.meansNoLimitMB" }}
+
+ {{ i18n "pages.inbounds.downloadSpeedLimit" }}
+
+
+
+
+ MB/s
+
+
diff --git a/web/service/inbound.go b/web/service/inbound.go
index c99843b3f6..6b0cbac9c5 100644
--- a/web/service/inbound.go
+++ b/web/service/inbound.go
@@ -803,13 +803,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)
@@ -1297,13 +1299,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)
@@ -2313,13 +2317,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 b2d396462f..ef723add98 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" && key != "reverse" {
+ 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 c313ba1c90..77e70973c3 100644
--- a/web/translation/translate.en_US.toml
+++ b/web/translation/translate.en_US.toml
@@ -257,6 +257,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"
@@ -290,6 +291,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 4d27cc4364..ec94d48b62 100644
--- a/web/translation/translate.fa_IR.toml
+++ b/web/translation/translate.fa_IR.toml
@@ -256,7 +256,8 @@
"destinationPort" = "پورت مقصد"
"targetAddress" = "آدرس مقصد"
"monitorDesc" = "بهطور پیشفرض خالیبگذارید"
-"meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)"
+"meansNoLimit" = "= نامحدود. (واحد: گیگابایت)"
+"meansNoLimitMB" = "= نامحدود. (واحد: مگابایت)"
"totalFlow" = "ترافیک کل"
"leaveBlankToNeverExpire" = "برای منقضینشدن خالیبگذارید"
"noRecommendKeepDefault" = "توصیهمیشود بهطور پیشفرض حفظشود"
@@ -290,6 +291,8 @@
"IPLimitlog" = "گزارشها"
"IPLimitlogDesc" = "گزارش تاریخچه آیپی. برای فعال کردن ورودی پس از غیرفعال شدن، گزارش را پاک کنید"
"IPLimitlogclear" = "پاک کردن گزارشها"
+"uploadSpeedLimit" = "سرعت آپلود"
+"downloadSpeedLimit" = "سرعت دانلود"
"setDefaultCert" = "استفاده از گواهی پنل"
"telegramDesc" = "لطفا شناسه گفتگوی تلگرام را وارد کنید. (از دستور '/id' در ربات استفاده کنید) یا (@userinfobot)"
"subscriptionDesc" = "شما میتوانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین میتوانید از همین نام برای چندین کاربر استفادهکنید"
diff --git a/xray/api.go b/xray/api.go
index bfb646650b..7c17a48180 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