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',
|
||||
SHADOWSOCKS: 'shadowsocks',
|
||||
TUNNEL: 'tunnel',
|
||||
MIXED: 'mixed',
|
||||
SOCKS: 'socks',
|
||||
HTTP: 'http',
|
||||
WIREGUARD: 'wireguard',
|
||||
};
|
||||
|
|
@ -140,8 +140,22 @@ class XrayCommonClass {
|
|||
return new XrayCommonClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* 【最佳实践】中文注释:这是一个智能的、通用的序列化方法。
|
||||
* 1. 使用 ...this 创建一个当前对象所有属性的浅拷贝。
|
||||
* 2. 遍历拷贝后的对象,删除所有以下划线 "_" 开头的属性。
|
||||
* 这些带下划线的属性被约定为仅供前端 UI 逻辑使用(如 _expiryTime),不应提交给后端。
|
||||
* 3. 返回一个干净的、只包含持久化数据的对象。
|
||||
* 这个方法将被所有子类继承,无需在每个子类中重复实现,保证了代码的健壮性和可维护性。
|
||||
*/
|
||||
toJson() {
|
||||
return this;
|
||||
const obj = { ...this };
|
||||
for (const key in obj) {
|
||||
if (key.startsWith('_')) {
|
||||
delete obj[key];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
toString(format = true) {
|
||||
|
|
@ -729,8 +743,8 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||
constructor(
|
||||
show = false,
|
||||
xver = 0,
|
||||
target = 'google.com:443',
|
||||
serverNames = 'google.com,www.google.com',
|
||||
target = 'tesla.com:443',
|
||||
serverNames = 'tesla.com,www.tesla.com',
|
||||
privateKey = '',
|
||||
minClientVer = '',
|
||||
maxClientVer = '',
|
||||
|
|
@ -1713,7 +1727,7 @@ Inbound.Settings = class extends XrayCommonClass {
|
|||
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
|
||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(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.WIREGUARD: return new Inbound.WireguardSettings(protocol);
|
||||
default: return null;
|
||||
|
|
@ -1727,7 +1741,7 @@ Inbound.Settings = class extends XrayCommonClass {
|
|||
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
|
||||
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.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.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
|
||||
default: return null;
|
||||
|
|
@ -1784,6 +1798,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
|||
security = USERS_SECURITY.AUTO,
|
||||
email = RandomUtil.randomLowerAndNum(8),
|
||||
limitIp = 0,
|
||||
speedLimit = 0, //
|
||||
totalGB = 0,
|
||||
expiryTime = 0,
|
||||
enable = true,
|
||||
|
|
@ -1799,6 +1814,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
|||
this.security = security;
|
||||
this.email = email;
|
||||
this.limitIp = limitIp;
|
||||
this.speedLimit = speedLimit;
|
||||
this.totalGB = totalGB;
|
||||
this.expiryTime = expiryTime;
|
||||
this.enable = enable;
|
||||
|
|
@ -1810,12 +1826,14 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
|||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.VmessSettings.VMESS(
|
||||
json.id,
|
||||
json.security,
|
||||
json.email,
|
||||
json.limitIp,
|
||||
json.speedLimit ?? 0,
|
||||
json.totalGB,
|
||||
json.expiryTime,
|
||||
json.enable,
|
||||
|
|
@ -1859,16 +1877,15 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||
protocol,
|
||||
vlesses = [new Inbound.VLESSSettings.VLESS()],
|
||||
decryption = "none",
|
||||
encryption = "none",
|
||||
encryption = "",
|
||||
fallbacks = [],
|
||||
selectedAuth = undefined,
|
||||
) {
|
||||
super(protocol);
|
||||
this.vlesses = vlesses;
|
||||
this.decryption = decryption;
|
||||
this.encryption = encryption;
|
||||
this.fallbacks = fallbacks;
|
||||
this.selectedAuth = selectedAuth;
|
||||
this.selectedAuth = "X25519, not Post-Quantum";
|
||||
}
|
||||
|
||||
addFallback() {
|
||||
|
|
@ -1879,19 +1896,19 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||
this.fallbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
// decryption should be set to static value
|
||||
static fromJson(json = {}) {
|
||||
const obj = new Inbound.VLESSSettings(
|
||||
Protocols.VLESS,
|
||||
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
json.decryption,
|
||||
json.encryption,
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
|
||||
json.selectedAuth
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || [])
|
||||
);
|
||||
obj.selectedAuth = json.selectedAuth || "X25519, not Post-Quantum";
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
toJson() {
|
||||
const json = {
|
||||
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
|
||||
|
|
@ -1924,6 +1941,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||
flow = '',
|
||||
email = RandomUtil.randomLowerAndNum(8),
|
||||
limitIp = 0,
|
||||
speedLimit = 0,
|
||||
totalGB = 0,
|
||||
expiryTime = 0,
|
||||
enable = true,
|
||||
|
|
@ -1939,6 +1957,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||
this.flow = flow;
|
||||
this.email = email;
|
||||
this.limitIp = limitIp;
|
||||
this.speedLimit = speedLimit;
|
||||
this.totalGB = totalGB;
|
||||
this.expiryTime = expiryTime;
|
||||
this.enable = enable;
|
||||
|
|
@ -1950,12 +1969,14 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
|||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.VLESSSettings.VLESS(
|
||||
json.id,
|
||||
json.flow,
|
||||
json.email,
|
||||
json.limitIp,
|
||||
json.speedLimit ?? 0,
|
||||
json.totalGB,
|
||||
json.expiryTime,
|
||||
json.enable,
|
||||
|
|
@ -2069,6 +2090,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
password = RandomUtil.randomSeq(10),
|
||||
email = RandomUtil.randomLowerAndNum(8),
|
||||
limitIp = 0,
|
||||
speedLimit = 0,
|
||||
totalGB = 0,
|
||||
expiryTime = 0,
|
||||
enable = true,
|
||||
|
|
@ -2083,6 +2105,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
this.password = password;
|
||||
this.email = email;
|
||||
this.limitIp = limitIp;
|
||||
this.speedLimit = speedLimit;
|
||||
this.totalGB = totalGB;
|
||||
this.expiryTime = expiryTime;
|
||||
this.enable = enable;
|
||||
|
|
@ -2099,6 +2122,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
password: this.password,
|
||||
email: this.email,
|
||||
limitIp: this.limitIp,
|
||||
speedLimit: this.speedLimit,
|
||||
totalGB: this.totalGB,
|
||||
expiryTime: this.expiryTime,
|
||||
enable: this.enable,
|
||||
|
|
@ -2116,6 +2140,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
|||
json.password,
|
||||
json.email,
|
||||
json.limitIp,
|
||||
json.speedLimit ?? 0,
|
||||
json.totalGB,
|
||||
json.expiryTime,
|
||||
json.enable,
|
||||
|
|
@ -2238,6 +2263,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
password = RandomUtil.randomShadowsocksPassword(),
|
||||
email = RandomUtil.randomLowerAndNum(8),
|
||||
limitIp = 0,
|
||||
speedLimit = 0,
|
||||
totalGB = 0,
|
||||
expiryTime = 0,
|
||||
enable = true,
|
||||
|
|
@ -2253,6 +2279,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
this.password = password;
|
||||
this.email = email;
|
||||
this.limitIp = limitIp;
|
||||
this.speedLimit = speedLimit;
|
||||
this.totalGB = totalGB;
|
||||
this.expiryTime = expiryTime;
|
||||
this.enable = enable;
|
||||
|
|
@ -2270,6 +2297,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
password: this.password,
|
||||
email: this.email,
|
||||
limitIp: this.limitIp,
|
||||
speedLimit: this.speedLimit,
|
||||
totalGB: this.totalGB,
|
||||
expiryTime: this.expiryTime,
|
||||
enable: this.enable,
|
||||
|
|
@ -2288,6 +2316,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||
json.password,
|
||||
json.email,
|
||||
json.limitIp,
|
||||
json.speedLimit ?? 0,
|
||||
json.totalGB,
|
||||
json.expiryTime,
|
||||
json.enable,
|
||||
|
|
@ -2366,8 +2395,8 @@ Inbound.TunnelSettings = class extends Inbound.Settings {
|
|||
}
|
||||
};
|
||||
|
||||
Inbound.MixedSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
|
||||
Inbound.SocksSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
|
||||
super(protocol);
|
||||
this.auth = auth;
|
||||
this.accounts = accounts;
|
||||
|
|
@ -2387,11 +2416,11 @@ Inbound.MixedSettings = class extends Inbound.Settings {
|
|||
let accounts;
|
||||
if (json.auth === 'password') {
|
||||
accounts = json.accounts.map(
|
||||
account => Inbound.MixedSettings.SocksAccount.fromJson(account)
|
||||
account => Inbound.SocksSettings.SocksAccount.fromJson(account)
|
||||
)
|
||||
}
|
||||
return new Inbound.MixedSettings(
|
||||
Protocols.MIXED,
|
||||
return new Inbound.SocksSettings(
|
||||
Protocols.SOCKS,
|
||||
json.auth,
|
||||
accounts,
|
||||
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)) {
|
||||
super();
|
||||
this.user = user;
|
||||
|
|
@ -2416,7 +2445,7 @@ Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
|
|||
}
|
||||
|
||||
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>
|
||||
<a-input-number :style="{ width: '50%' }" v-model.number="client.tgId" min="0"></a-input-number>
|
||||
</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-input v-model.trim="client.comment"></a-input>
|
||||
</a-form-item>
|
||||
|
|
|
|||
|
|
@ -512,6 +512,10 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
|||
cm["created_at"] = nowTs
|
||||
}
|
||||
cm["updated_at"] = nowTs
|
||||
|
||||
|
||||
cm["speedLimit"] = clients[i].SpeedLimit
|
||||
|
||||
interfaceClients[i] = cm
|
||||
}
|
||||
}
|
||||
|
|
@ -592,6 +596,9 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
|||
"flow": client.Flow,
|
||||
"password": client.Password,
|
||||
"cipher": cipher,
|
||||
|
||||
|
||||
"level": client.SpeedLimit,
|
||||
})
|
||||
if err1 == nil {
|
||||
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["updated_at"] = time.Now().Unix() * 1000
|
||||
newMap["speedLimit"] = clients[0].SpeedLimit
|
||||
interfaceClients[0] = newMap
|
||||
}
|
||||
}
|
||||
|
|
@ -855,6 +863,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
|||
"flow": clients[0].Flow,
|
||||
"password": clients[0].Password,
|
||||
"cipher": cipher,
|
||||
"level": clients[0].SpeedLimit,
|
||||
})
|
||||
if err1 == nil {
|
||||
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["updated_at"] = time.Now().Unix() * 1000
|
||||
|
||||
if _, ok := c["speedLimit"]; !ok {
|
||||
c["speedLimit"] = 0
|
||||
}
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
settings["clients"] = newClients
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import (
|
|||
"errors"
|
||||
"runtime"
|
||||
"sync"
|
||||
"strconv"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
json_util "x-ui/util/json_util"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
|
@ -30,6 +32,13 @@ func (s *XrayService) IsXrayRunning() bool {
|
|||
return p != nil && p.IsRunning()
|
||||
}
|
||||
|
||||
func (s *XrayService) GetApiPort() int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return p.GetAPIPort()
|
||||
}
|
||||
|
||||
func (s *XrayService) GetXrayErr() error {
|
||||
if p == nil {
|
||||
return nil
|
||||
|
|
@ -91,16 +100,234 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
s.inboundService.AddTraffic(nil, nil)
|
||||
|
||||
|
||||
inbounds, err := s.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
uniqueSpeeds := make(map[int]bool)
|
||||
for _, inbound := range inbounds {
|
||||
if !inbound.Enable {
|
||||
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
|
||||
settings := map[string]any{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
|
|
@ -176,7 +403,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
|||
inbound.StreamSettings = string(newStream)
|
||||
}
|
||||
|
||||
inboundConfig := inbound.GenXrayInboundConfig()
|
||||
|
||||
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
|
||||
}
|
||||
return xrayConfig, nil
|
||||
|
|
|
|||
|
|
@ -239,6 +239,8 @@
|
|||
"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."
|
||||
"info" = "Info"
|
||||
"speedLimit"="Independent speedLimit"
|
||||
"speedLimitDesc"="Set the maximum upload/download speed for this user in KB/s. 0 means unlimited speed."
|
||||
"same" = "Same"
|
||||
"inboundData" = "Inbound's Data"
|
||||
"exportInbound" = "Export Inbound"
|
||||
|
|
|
|||
|
|
@ -235,6 +235,8 @@
|
|||
"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ử)."
|
||||
"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"
|
||||
"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"
|
||||
|
|
|
|||
|
|
@ -235,6 +235,8 @@
|
|||
"IPLimitlog" = "IP 日志"
|
||||
"IPLimitlogDesc" = "IP 历史日志(要启用被禁用的入站流量,请清除日志)"
|
||||
"IPLimitlogclear" = "清除日志"
|
||||
"speedLimit"="独立限速"
|
||||
"speedLimitDesc"="设置该用户的最大〔上传/下载速度〕,\r\n单位 KB/s,0 表示不限速"
|
||||
"setDefaultCert" = "从面板设置证书"
|
||||
"telegramDesc" = "请提供Telegram聊天ID。(在机器人中使用'/id'命令)或(@userinfobot"
|
||||
"subscriptionDesc" = "要找到你的订阅 URL,请导航到“详细信息”。此外,你可以为多个客户端使用相同的名称。"
|
||||
|
|
|
|||
|
|
@ -235,6 +235,8 @@
|
|||
"IPLimitlog" = "IP 日誌"
|
||||
"IPLimitlogDesc" = "IP 歷史日誌(要啟用被禁用的入站流量,請清除日誌)"
|
||||
"IPLimitlogclear" = "清除日誌"
|
||||
"speedLimit"="獨立限速"
|
||||
"speedLimitDesc"="設定該使用者的最大〔上傳/下載速度〕,\r\n單位 KB/s,0 表示不限速"
|
||||
"setDefaultCert" = "從面板設定證書"
|
||||
"telegramDesc" = "請提供Telegram聊天ID。(在機器人中使用'/id'命令)或(@userinfobot"
|
||||
"subscriptionDesc" = "要找到你的訂閱 URL,請導航到“詳細資訊”。此外,你可以為多個客戶端使用相同的名稱。"
|
||||
|
|
|
|||
Loading…
Reference in a new issue