mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
add speed limiter for download and upload speed in megabytes
This commit is contained in:
parent
51e2fb6dbf
commit
2d2c1e287e
9 changed files with 150 additions and 44 deletions
|
|
@ -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
2
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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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" = "شما میتوانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین میتوانید از همین نام برای چندین کاربر استفادهکنید"
|
||||
|
|
|
|||
41
xray/api.go
41
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue