feat: add per-client speed limit support for inbound connections

This commit is contained in:
Harry NG 2025-09-18 20:10:08 +07:00
parent 3675a720da
commit 31d090fc7e
8 changed files with 325 additions and 27 deletions

View file

@ -4,7 +4,7 @@ const Protocols = {
TROJAN: 'trojan', TROJAN: 'trojan',
SHADOWSOCKS: 'shadowsocks', SHADOWSOCKS: 'shadowsocks',
TUNNEL: 'tunnel', TUNNEL: 'tunnel',
MIXED: 'mixed', SOCKS: 'socks',
HTTP: 'http', HTTP: 'http',
WIREGUARD: 'wireguard', WIREGUARD: 'wireguard',
}; };
@ -140,8 +140,22 @@ class XrayCommonClass {
return new XrayCommonClass(); return new XrayCommonClass();
} }
/**
* 最佳实践中文注释这是一个智能的通用的序列化方法
* 1. 使用 ...this 创建一个当前对象所有属性的浅拷贝
* 2. 遍历拷贝后的对象删除所有以下划线 "_" 开头的属性
* 这些带下划线的属性被约定为仅供前端 UI 逻辑使用 _expiryTime不应提交给后端
* 3. 返回一个干净的只包含持久化数据的对象
* 这个方法将被所有子类继承无需在每个子类中重复实现保证了代码的健壮性和可维护性
*/
toJson() { toJson() {
return this; const obj = { ...this };
for (const key in obj) {
if (key.startsWith('_')) {
delete obj[key];
}
}
return obj;
} }
toString(format = true) { toString(format = true) {
@ -729,8 +743,8 @@ class RealityStreamSettings extends XrayCommonClass {
constructor( constructor(
show = false, show = false,
xver = 0, xver = 0,
target = 'google.com:443', target = 'tesla.com:443',
serverNames = 'google.com,www.google.com', serverNames = 'tesla.com,www.tesla.com',
privateKey = '', privateKey = '',
minClientVer = '', minClientVer = '',
maxClientVer = '', maxClientVer = '',
@ -1713,7 +1727,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol); case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol); case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol); case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
case Protocols.MIXED: return new Inbound.MixedSettings(protocol); case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol); case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol); case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
default: return null; default: return null;
@ -1727,7 +1741,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json); case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json); case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json); case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json); case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json); case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
default: return null; default: return null;
@ -1784,6 +1798,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
security = USERS_SECURITY.AUTO, security = USERS_SECURITY.AUTO,
email = RandomUtil.randomLowerAndNum(8), email = RandomUtil.randomLowerAndNum(8),
limitIp = 0, limitIp = 0,
speedLimit = 0, //
totalGB = 0, totalGB = 0,
expiryTime = 0, expiryTime = 0,
enable = true, enable = true,
@ -1799,6 +1814,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
this.security = security; this.security = security;
this.email = email; this.email = email;
this.limitIp = limitIp; this.limitIp = limitIp;
this.speedLimit = speedLimit;
this.totalGB = totalGB; this.totalGB = totalGB;
this.expiryTime = expiryTime; this.expiryTime = expiryTime;
this.enable = enable; this.enable = enable;
@ -1809,13 +1825,15 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
this.created_at = created_at; this.created_at = created_at;
this.updated_at = updated_at; this.updated_at = updated_at;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.VmessSettings.VMESS( return new Inbound.VmessSettings.VMESS(
json.id, json.id,
json.security, json.security,
json.email, json.email,
json.limitIp, json.limitIp,
json.speedLimit ?? 0,
json.totalGB, json.totalGB,
json.expiryTime, json.expiryTime,
json.enable, json.enable,
@ -1859,16 +1877,15 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
protocol, protocol,
vlesses = [new Inbound.VLESSSettings.VLESS()], vlesses = [new Inbound.VLESSSettings.VLESS()],
decryption = "none", decryption = "none",
encryption = "none", encryption = "",
fallbacks = [], fallbacks = [],
selectedAuth = undefined,
) { ) {
super(protocol); super(protocol);
this.vlesses = vlesses; this.vlesses = vlesses;
this.decryption = decryption; this.decryption = decryption;
this.encryption = encryption; this.encryption = encryption;
this.fallbacks = fallbacks; this.fallbacks = fallbacks;
this.selectedAuth = selectedAuth; this.selectedAuth = "X25519, not Post-Quantum";
} }
addFallback() { addFallback() {
@ -1879,24 +1896,24 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
this.fallbacks.splice(index, 1); this.fallbacks.splice(index, 1);
} }
// decryption should be set to static value
static fromJson(json = {}) { static fromJson(json = {}) {
const obj = new Inbound.VLESSSettings( const obj = new Inbound.VLESSSettings(
Protocols.VLESS, Protocols.VLESS,
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)), (json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption, json.decryption,
json.encryption, json.encryption,
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []), Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || [])
json.selectedAuth
); );
obj.selectedAuth = json.selectedAuth || "X25519, not Post-Quantum";
return obj; return obj;
} }
toJson() { toJson() {
const json = { const json = {
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses), clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
}; };
if (this.decryption) { if (this.decryption) {
json.decryption = this.decryption; json.decryption = this.decryption;
} }
@ -1914,8 +1931,8 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
return json; return json;
} }
}; };
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
@ -1924,6 +1941,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
flow = '', flow = '',
email = RandomUtil.randomLowerAndNum(8), email = RandomUtil.randomLowerAndNum(8),
limitIp = 0, limitIp = 0,
speedLimit = 0,
totalGB = 0, totalGB = 0,
expiryTime = 0, expiryTime = 0,
enable = true, enable = true,
@ -1939,6 +1957,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
this.flow = flow; this.flow = flow;
this.email = email; this.email = email;
this.limitIp = limitIp; this.limitIp = limitIp;
this.speedLimit = speedLimit;
this.totalGB = totalGB; this.totalGB = totalGB;
this.expiryTime = expiryTime; this.expiryTime = expiryTime;
this.enable = enable; this.enable = enable;
@ -1949,6 +1968,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
this.created_at = created_at; this.created_at = created_at;
this.updated_at = updated_at; this.updated_at = updated_at;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.VLESSSettings.VLESS( return new Inbound.VLESSSettings.VLESS(
@ -1956,6 +1976,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.flow, json.flow,
json.email, json.email,
json.limitIp, json.limitIp,
json.speedLimit ?? 0,
json.totalGB, json.totalGB,
json.expiryTime, json.expiryTime,
json.enable, json.enable,
@ -2069,6 +2090,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
password = RandomUtil.randomSeq(10), password = RandomUtil.randomSeq(10),
email = RandomUtil.randomLowerAndNum(8), email = RandomUtil.randomLowerAndNum(8),
limitIp = 0, limitIp = 0,
speedLimit = 0,
totalGB = 0, totalGB = 0,
expiryTime = 0, expiryTime = 0,
enable = true, enable = true,
@ -2083,6 +2105,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
this.password = password; this.password = password;
this.email = email; this.email = email;
this.limitIp = limitIp; this.limitIp = limitIp;
this.speedLimit = speedLimit;
this.totalGB = totalGB; this.totalGB = totalGB;
this.expiryTime = expiryTime; this.expiryTime = expiryTime;
this.enable = enable; this.enable = enable;
@ -2099,6 +2122,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
password: this.password, password: this.password,
email: this.email, email: this.email,
limitIp: this.limitIp, limitIp: this.limitIp,
speedLimit: this.speedLimit,
totalGB: this.totalGB, totalGB: this.totalGB,
expiryTime: this.expiryTime, expiryTime: this.expiryTime,
enable: this.enable, enable: this.enable,
@ -2116,6 +2140,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
json.password, json.password,
json.email, json.email,
json.limitIp, json.limitIp,
json.speedLimit ?? 0,
json.totalGB, json.totalGB,
json.expiryTime, json.expiryTime,
json.enable, json.enable,
@ -2238,6 +2263,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
password = RandomUtil.randomShadowsocksPassword(), password = RandomUtil.randomShadowsocksPassword(),
email = RandomUtil.randomLowerAndNum(8), email = RandomUtil.randomLowerAndNum(8),
limitIp = 0, limitIp = 0,
speedLimit = 0,
totalGB = 0, totalGB = 0,
expiryTime = 0, expiryTime = 0,
enable = true, enable = true,
@ -2253,6 +2279,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
this.password = password; this.password = password;
this.email = email; this.email = email;
this.limitIp = limitIp; this.limitIp = limitIp;
this.speedLimit = speedLimit;
this.totalGB = totalGB; this.totalGB = totalGB;
this.expiryTime = expiryTime; this.expiryTime = expiryTime;
this.enable = enable; this.enable = enable;
@ -2263,13 +2290,14 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
this.created_at = created_at; this.created_at = created_at;
this.updated_at = updated_at; this.updated_at = updated_at;
} }
toJson() { toJson() {
return { return {
method: this.method, method: this.method,
password: this.password, password: this.password,
email: this.email, email: this.email,
limitIp: this.limitIp, limitIp: this.limitIp,
speedLimit: this.speedLimit,
totalGB: this.totalGB, totalGB: this.totalGB,
expiryTime: this.expiryTime, expiryTime: this.expiryTime,
enable: this.enable, enable: this.enable,
@ -2288,6 +2316,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
json.password, json.password,
json.email, json.email,
json.limitIp, json.limitIp,
json.speedLimit ?? 0,
json.totalGB, json.totalGB,
json.expiryTime, json.expiryTime,
json.enable, json.enable,
@ -2366,8 +2395,8 @@ Inbound.TunnelSettings = class extends Inbound.Settings {
} }
}; };
Inbound.MixedSettings = class extends Inbound.Settings { Inbound.SocksSettings = class extends Inbound.Settings {
constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') { constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
super(protocol); super(protocol);
this.auth = auth; this.auth = auth;
this.accounts = accounts; this.accounts = accounts;
@ -2387,11 +2416,11 @@ Inbound.MixedSettings = class extends Inbound.Settings {
let accounts; let accounts;
if (json.auth === 'password') { if (json.auth === 'password') {
accounts = json.accounts.map( accounts = json.accounts.map(
account => Inbound.MixedSettings.SocksAccount.fromJson(account) account => Inbound.SocksSettings.SocksAccount.fromJson(account)
) )
} }
return new Inbound.MixedSettings( return new Inbound.SocksSettings(
Protocols.MIXED, Protocols.SOCKS,
json.auth, json.auth,
accounts, accounts,
json.udp, json.udp,
@ -2408,7 +2437,7 @@ Inbound.MixedSettings = class extends Inbound.Settings {
}; };
} }
}; };
Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass { Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) { constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
super(); super();
this.user = user; this.user = user;
@ -2416,7 +2445,7 @@ Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.MixedSettings.SocksAccount(json.user, json.pass); return new Inbound.SocksSettings.SocksAccount(json.user, json.pass);
} }
}; };

View file

@ -68,6 +68,27 @@
</template> </template>
<a-input-number :style="{ width: '50%' }" v-model.number="client.tgId" min="0"></a-input-number> <a-input-number :style="{ width: '50%' }" v-model.number="client.tgId" min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.speedLimitDesc" }}</span>
</template>
<span>
{{ i18n "pages.inbounds.speedLimit" }}
<a-icon type="question-circle"></a-icon>
</span>
</a-tooltip>
</template>
<a-input-number
v-model.number="client.speedLimit"
:min="0"
style="width: 100%">
<template slot="addonAfter">
KB/s
</template>
</a-input-number>
</a-form-item>
<a-form-item v-if="client.email" label='{{ i18n "comment" }}'> <a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
<a-input v-model.trim="client.comment"></a-input> <a-input v-model.trim="client.comment"></a-input>
</a-form-item> </a-form-item>

View file

@ -512,6 +512,10 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
cm["created_at"] = nowTs cm["created_at"] = nowTs
} }
cm["updated_at"] = nowTs cm["updated_at"] = nowTs
cm["speedLimit"] = clients[i].SpeedLimit
interfaceClients[i] = cm interfaceClients[i] = cm
} }
} }
@ -592,6 +596,9 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
"flow": client.Flow, "flow": client.Flow,
"password": client.Password, "password": client.Password,
"cipher": cipher, "cipher": cipher,
"level": client.SpeedLimit,
}) })
if err1 == nil { if err1 == nil {
logger.Debug("Client added by api:", client.Email) logger.Debug("Client added by api:", client.Email)
@ -781,6 +788,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
} }
newMap["created_at"] = preservedCreated newMap["created_at"] = preservedCreated
newMap["updated_at"] = time.Now().Unix() * 1000 newMap["updated_at"] = time.Now().Unix() * 1000
newMap["speedLimit"] = clients[0].SpeedLimit
interfaceClients[0] = newMap interfaceClients[0] = newMap
} }
} }
@ -855,6 +863,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
"flow": clients[0].Flow, "flow": clients[0].Flow,
"password": clients[0].Password, "password": clients[0].Password,
"cipher": cipher, "cipher": cipher,
"level": clients[0].SpeedLimit,
}) })
if err1 == nil { if err1 == nil {
logger.Debug("Client edited by api:", clients[0].Email) logger.Debug("Client edited by api:", clients[0].Email)
@ -2112,6 +2121,10 @@ func (s *InboundService) MigrationRequirements() {
c["created_at"] = time.Now().Unix() * 1000 c["created_at"] = time.Now().Unix() * 1000
} }
c["updated_at"] = time.Now().Unix() * 1000 c["updated_at"] = time.Now().Unix() * 1000
if _, ok := c["speedLimit"]; !ok {
c["speedLimit"] = 0
}
newClients = append(newClients, any(c)) newClients = append(newClients, any(c))
} }
settings["clients"] = newClients settings["clients"] = newClients

View file

@ -5,9 +5,11 @@ import (
"errors" "errors"
"runtime" "runtime"
"sync" "sync"
"strconv"
"x-ui/logger" "x-ui/logger"
"x-ui/xray" "x-ui/xray"
json_util "x-ui/util/json_util"
"go.uber.org/atomic" "go.uber.org/atomic"
) )
@ -30,6 +32,13 @@ func (s *XrayService) IsXrayRunning() bool {
return p != nil && p.IsRunning() return p != nil && p.IsRunning()
} }
func (s *XrayService) GetApiPort() int {
if p == nil {
return 0
}
return p.GetAPIPort()
}
func (s *XrayService) GetXrayErr() error { func (s *XrayService) GetXrayErr() error {
if p == nil { if p == nil {
return nil return nil
@ -91,16 +100,234 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
return nil, err return nil, err
} }
s.inboundService.AddTraffic(nil, nil)
inbounds, err := s.inboundService.GetAllInbounds() inbounds, err := s.inboundService.GetAllInbounds()
if err != nil { if err != nil {
return nil, err return nil, err
} }
uniqueSpeeds := make(map[int]bool)
for _, inbound := range inbounds { for _, inbound := range inbounds {
if !inbound.Enable { if !inbound.Enable {
continue continue
} }
dbClients, _ := s.inboundService.GetClients(inbound)
for _, dbClient := range dbClients {
if dbClient.SpeedLimit > 0 {
uniqueSpeeds[dbClient.SpeedLimit] = true
}
}
}
var finalPolicy map[string]interface{}
if xrayConfig.Policy != nil {
if err := json.Unmarshal(xrayConfig.Policy, &finalPolicy); err != nil {
logger.Warningf("Failed to parse the policy in the template: %v", err)
finalPolicy = make(map[string]interface{})
}
} else {
finalPolicy = make(map[string]interface{})
}
var policyLevels map[string]interface{}
if levels, ok := finalPolicy["levels"].(map[string]interface{}); ok {
policyLevels = levels
} else {
policyLevels = make(map[string]interface{})
}
// 3. [Important modification]: Ensure the integrity of the level 0 policy, which is key to enabling device restrictions and default user statistics.
var level0 map[string]interface{}
if l0, ok := policyLevels["0"].(map[string]interface{}); ok {
// If level 0 already exists in the template, use it as the base for modifications.
level0 = l0
} else {
// If it does not exist in the template, create a brand new map.
level0 = make(map[string]interface{})
}
// [Chinese comment]: Regardless of whether level 0 exists, supplement or override the following key parameters for it.
// handshake and connIdle are prerequisites to activate Xray connection statistics,
// uplinkOnly and downlinkOnly set to 0 mean no speed limit, which is the default behavior for level 0 users.
// statsUserUplink and statsUserDownlink ensure that user traffic can be counted.
level0["handshake"] = 4
level0["connIdle"] = 300
level0["uplinkOnly"] = 0
level0["downlinkOnly"] = 0
level0["statsUserUplink"] = true
level0["statsUserDownlink"] = true
// [Added]: Add this key option to enable Xray-core's online IP statistics feature.
// This is a prerequisite for the proper functioning of the "device restriction" feature.
level0["statsUserOnline"] = true
// Write the fully configured level 0 back to policyLevels to ensure the final generated config.json is correct.
policyLevels["0"] = level0
// 4. Iterate through all collected speed limits and create a corresponding level for each unique speed limit
for speed := range uniqueSpeeds {
// Create a level for each speed, where the level's name is the string representation of the speed
// For example, the speed 1024 KB/s corresponds to the level "1024"
policyLevels[strconv.Itoa(speed)] = map[string]interface{}{
"downlinkOnly": speed,
"uplinkOnly": speed,
"handshake": 4,
"connIdle": 300,
"statsUserUplink": true,
"statsUserDownlink": true,
"statsUserOnline": true,
}
}
// 5. Write the modified levels back to the policy object, serialize it back to xrayConfig.Policy, and apply the generated policy to the Xray configuration
finalPolicy["levels"] = policyLevels
policyJSON, err := json.Marshal(finalPolicy)
if err != nil {
return nil, err
}
xrayConfig.Policy = json_util.RawMessage(policyJSON)
// =================================================================
// Add logs here to print the final generated speed limit policy
// =================================================================
if len(uniqueSpeeds) > 0 {
finalPolicyLog, _ := json.Marshal(policyLevels)
logger.Infof("已为Xray动态生成限速策略: %s", string(finalPolicyLog))
}
s.inboundService.AddTraffic(nil, nil)
for _, inbound := range inbounds {
if !inbound.Enable {
continue
}
inboundConfig := inbound.GenXrayInboundConfig()
speedByEmail := make(map[string]int)
speedById := make(map[string]int)
dbClients, _ := s.inboundService.GetClients(inbound)
for _, dbc := range dbClients {
if dbc.Email != "" {
speedByEmail[dbc.Email] = dbc.SpeedLimit
}
if dbc.ID != "" {
speedById[dbc.ID] = dbc.SpeedLimit
}
}
var settings map[string]interface{}
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
logger.Warningf("Failed to parse inbound.Settings (inbound %d): %v, skipping this inbound", inbound.Id, err)
continue
}
originalClients, ok := settings["clients"].([]interface{})
if ok {
clientStats := inbound.ClientStats
var xrayClients []interface{}
for _, clientRaw := range originalClients {
c, ok := clientRaw.(map[string]interface{})
if !ok {
continue
}
if en, ok := c["enable"].(bool); ok && !en {
if em, _ := c["email"].(string); em != "" {
logger.Infof("User marked as disabled in settings removed from Xray config: %s", em)
}
continue
}
email, _ := c["email"].(string)
idStr, _ := c["id"].(string)
disabledByStat := false
for _, stat := range clientStats {
if stat.Email == email && !stat.Enable {
disabledByStat = true
break
}
}
if disabledByStat {
logger.Infof("User disabled and removed from Xray config: %s", email)
continue
}
xrayClient := make(map[string]interface{})
if id, ok := c["id"]; ok { xrayClient["id"] = id }
if email != "" { xrayClient["email"] = email }
if flow, ok := c["flow"]; ok {
if fs, ok2 := flow.(string); ok2 && fs == "xtls-rprx-vision-udp443" {
xrayClient["flow"] = "xtls-rprx-vision"
} else {
xrayClient["flow"] = flow
}
}
if password, ok := c["password"]; ok { xrayClient["password"] = password }
if method, ok := c["method"]; ok { xrayClient["method"] = method }
level := 0
if email != "" {
if v, ok := speedByEmail[email]; ok && v > 0 {
level = v
}
}
if level == 0 && idStr != "" {
if v, ok := speedById[idStr]; ok && v > 0 {
level = v
}
}
if level == 0 {
if sl, ok := c["speedLimit"]; ok {
switch vv := sl.(type) {
case float64:
level = int(vv)
case int:
level = vv
case int64:
level = int(vv)
case string:
if n, err := strconv.Atoi(vv); err == nil {
level = n
}
}
}
}
if level > 0 && email != "" {
logger.Infof("Applied independent speed limit for user %s: %d KB/s", email, level)
}
// =================================================================
xrayClient["level"] = level
xrayClients = append(xrayClients, xrayClient)
}
settings["clients"] = xrayClients
finalSettingsForXray, err := json.Marshal(settings)
if err != nil {
logger.Warningf("Failed to serialize inbound settings for Xray in GetXrayConfig for inbound %d: %v, skipping this inbound", inbound.Id, err)
continue
}
inboundConfig.Settings = json_util.RawMessage(finalSettingsForXray)
}
// get settings clients // get settings clients
settings := map[string]any{} settings := map[string]any{}
json.Unmarshal([]byte(inbound.Settings), &settings) json.Unmarshal([]byte(inbound.Settings), &settings)
@ -176,7 +403,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
inbound.StreamSettings = string(newStream) inbound.StreamSettings = string(newStream)
} }
inboundConfig := inbound.GenXrayInboundConfig()
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig) xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
} }
return xrayConfig, nil return xrayConfig, nil
@ -251,4 +478,4 @@ func (s *XrayService) IsNeedRestartAndSetFalse() bool {
// Check if Xray is not running and wasn't stopped manually, i.e. crashed // Check if Xray is not running and wasn't stopped manually, i.e. crashed
func (s *XrayService) DidXrayCrash() bool { func (s *XrayService) DidXrayCrash() bool {
return !s.IsXrayRunning() && !isManuallyStopped.Load() return !s.IsXrayRunning() && !isManuallyStopped.Load()
} }

View file

@ -239,6 +239,8 @@
"telegramDesc" = "Please provide Telegram Chat ID. (use '/id' command in the bot) or (@userinfobot)" "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." "subscriptionDesc" = "To find your subscription URL, navigate to the 'Details'. Additionally, you can use the same name for several clients."
"info" = "Info" "info" = "Info"
"speedLimit"="Independent speedLimit"
"speedLimitDesc"="Set the maximum upload/download speed for this user in KB/s. 0 means unlimited speed."
"same" = "Same" "same" = "Same"
"inboundData" = "Inbound's Data" "inboundData" = "Inbound's Data"
"exportInbound" = "Export Inbound" "exportInbound" = "Export Inbound"

View file

@ -235,6 +235,8 @@
"IPLimitlog" = "Lịch sử IP" "IPLimitlog" = "Lịch sử IP"
"IPLimitlogDesc" = "Lịch sử đăng nhập IP (trước khi kích hoạt điểm vào sau khi bị vô hiệu hóa bởi giới hạn IP, bạn nên xóa lịch sử)." "IPLimitlogDesc" = "Lịch sử đăng nhập IP (trước khi kích hoạt điểm vào sau khi bị vô hiệu hóa bởi giới hạn IP, bạn nên xóa lịch sử)."
"IPLimitlogclear" = "Xóa Lịch sử" "IPLimitlogclear" = "Xóa Lịch sử"
"speedLimit" = "Giới hạn tốc độ riêng"
"speedLimitDesc" = "Thiết lập tốc độ tối đatải lên/tải xuốngcho người dùng này\r\nđơn vị KB/s, 0 có nghĩa là không giới hạn"
"setDefaultCert" = "Đặt chứng chỉ từ bảng điều khiển" "setDefaultCert" = "Đặt chứng chỉ từ bảng điều khiển"
"telegramDesc" = "Vui lòng cung cấp ID Trò chuyện Telegram. (sử dụng lệnh '/id' trong bot) hoặc (@userinfobot)" "telegramDesc" = "Vui lòng cung cấp ID Trò chuyện Telegram. (sử dụng lệnh '/id' trong bot) hoặc (@userinfobot)"
"subscriptionDesc" = "Bạn có thể tìm liên kết gói đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau" "subscriptionDesc" = "Bạn có thể tìm liên kết gói đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau"

View file

@ -235,6 +235,8 @@
"IPLimitlog" = "IP 日志" "IPLimitlog" = "IP 日志"
"IPLimitlogDesc" = "IP 历史日志(要启用被禁用的入站流量,请清除日志)" "IPLimitlogDesc" = "IP 历史日志(要启用被禁用的入站流量,请清除日志)"
"IPLimitlogclear" = "清除日志" "IPLimitlogclear" = "清除日志"
"speedLimit"="独立限速"
"speedLimitDesc"="设置该用户的最大〔上传/下载速度〕,\r\n单位 KB/s0 表示不限速"
"setDefaultCert" = "从面板设置证书" "setDefaultCert" = "从面板设置证书"
"telegramDesc" = "请提供Telegram聊天ID。在机器人中使用'/id'命令)或(@userinfobot" "telegramDesc" = "请提供Telegram聊天ID。在机器人中使用'/id'命令)或(@userinfobot"
"subscriptionDesc" = "要找到你的订阅 URL请导航到“详细信息”。此外你可以为多个客户端使用相同的名称。" "subscriptionDesc" = "要找到你的订阅 URL请导航到“详细信息”。此外你可以为多个客户端使用相同的名称。"

View file

@ -235,6 +235,8 @@
"IPLimitlog" = "IP 日誌" "IPLimitlog" = "IP 日誌"
"IPLimitlogDesc" = "IP 歷史日誌(要啟用被禁用的入站流量,請清除日誌)" "IPLimitlogDesc" = "IP 歷史日誌(要啟用被禁用的入站流量,請清除日誌)"
"IPLimitlogclear" = "清除日誌" "IPLimitlogclear" = "清除日誌"
"speedLimit"="獨立限速"
"speedLimitDesc"="設定該使用者的最大〔上傳/下載速度〕,\r\n單位 KB/s0 表示不限速"
"setDefaultCert" = "從面板設定證書" "setDefaultCert" = "從面板設定證書"
"telegramDesc" = "請提供Telegram聊天ID。在機器人中使用'/id'命令)或(@userinfobot" "telegramDesc" = "請提供Telegram聊天ID。在機器人中使用'/id'命令)或(@userinfobot"
"subscriptionDesc" = "要找到你的訂閱 URL請導航到“詳細資訊”。此外你可以為多個客戶端使用相同的名稱。" "subscriptionDesc" = "要找到你的訂閱 URL請導航到“詳細資訊”。此外你可以為多個客戶端使用相同的名稱。"