add speed limiter for download and upload speed in megabytes

This commit is contained in:
Saeed 2026-05-01 11:05:09 +03:30
parent 51e2fb6dbf
commit 2d2c1e287e
9 changed files with 150 additions and 44 deletions

View file

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

2
go.mod
View file

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

View file

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

View file

@ -139,6 +139,32 @@
</template>
<a-input-number v-model.number="client.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="client.email">
<template slot="label">
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimitMB" }}</span>
</template>
{{ i18n "pages.inbounds.uploadSpeedLimit" }}
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="client._uploadSpeedLimitMB" :min="0"></a-input-number>
<span style="margin-left: 8px;">MB/s</span>
</a-form-item>
<a-form-item v-if="client.email">
<template slot="label">
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimitMB" }}</span>
</template>
{{ i18n "pages.inbounds.downloadSpeedLimit" }}
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="client._downloadSpeedLimitMB" :min="0"></a-input-number>
<span style="margin-left: 8px;">MB/s</span>
</a-form-item>
<a-form-item
v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit"
>

View file

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

View file

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

View file

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

View file

@ -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" = "شما می‌توانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین می‌توانید از همین نام برای چندین کاربر استفاده‌کنید"

View file

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