mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-11-29 02:42:51 +00:00
feat: add per-client speed limit support for inbound connections
This commit is contained in:
parent
3675a720da
commit
31d090fc7e
8 changed files with 325 additions and 27 deletions
|
|
@ -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;
|
||||||
|
|
@ -1810,12 +1826,14 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
||||||
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,19 +1896,19 @@ 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),
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1950,12 +1969,14 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||||
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(
|
||||||
json.id,
|
json.id,
|
||||||
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;
|
||||||
|
|
@ -2270,6 +2297,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = 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,
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 đa〔tải lên/tải xuống〕cho 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"
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,8 @@
|
||||||
"IPLimitlog" = "IP 日志"
|
"IPLimitlog" = "IP 日志"
|
||||||
"IPLimitlogDesc" = "IP 历史日志(要启用被禁用的入站流量,请清除日志)"
|
"IPLimitlogDesc" = "IP 历史日志(要启用被禁用的入站流量,请清除日志)"
|
||||||
"IPLimitlogclear" = "清除日志"
|
"IPLimitlogclear" = "清除日志"
|
||||||
|
"speedLimit"="独立限速"
|
||||||
|
"speedLimitDesc"="设置该用户的最大〔上传/下载速度〕,\r\n单位 KB/s,0 表示不限速"
|
||||||
"setDefaultCert" = "从面板设置证书"
|
"setDefaultCert" = "从面板设置证书"
|
||||||
"telegramDesc" = "请提供Telegram聊天ID。(在机器人中使用'/id'命令)或(@userinfobot"
|
"telegramDesc" = "请提供Telegram聊天ID。(在机器人中使用'/id'命令)或(@userinfobot"
|
||||||
"subscriptionDesc" = "要找到你的订阅 URL,请导航到“详细信息”。此外,你可以为多个客户端使用相同的名称。"
|
"subscriptionDesc" = "要找到你的订阅 URL,请导航到“详细信息”。此外,你可以为多个客户端使用相同的名称。"
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,8 @@
|
||||||
"IPLimitlog" = "IP 日誌"
|
"IPLimitlog" = "IP 日誌"
|
||||||
"IPLimitlogDesc" = "IP 歷史日誌(要啟用被禁用的入站流量,請清除日誌)"
|
"IPLimitlogDesc" = "IP 歷史日誌(要啟用被禁用的入站流量,請清除日誌)"
|
||||||
"IPLimitlogclear" = "清除日誌"
|
"IPLimitlogclear" = "清除日誌"
|
||||||
|
"speedLimit"="獨立限速"
|
||||||
|
"speedLimitDesc"="設定該使用者的最大〔上傳/下載速度〕,\r\n單位 KB/s,0 表示不限速"
|
||||||
"setDefaultCert" = "從面板設定證書"
|
"setDefaultCert" = "從面板設定證書"
|
||||||
"telegramDesc" = "請提供Telegram聊天ID。(在機器人中使用'/id'命令)或(@userinfobot"
|
"telegramDesc" = "請提供Telegram聊天ID。(在機器人中使用'/id'命令)或(@userinfobot"
|
||||||
"subscriptionDesc" = "要找到你的訂閱 URL,請導航到“詳細資訊”。此外,你可以為多個客戶端使用相同的名稱。"
|
"subscriptionDesc" = "要找到你的訂閱 URL,請導航到“詳細資訊”。此外,你可以為多個客戶端使用相同的名稱。"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue