mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat: add inbound traffic multiplier
This commit is contained in:
parent
20edaee8ed
commit
8c74a4eff5
22 changed files with 359 additions and 46 deletions
|
|
@ -55,6 +55,7 @@ type Inbound struct {
|
|||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2"` // Traffic reset schedule
|
||||
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
|
||||
TrafficMultiplier float64 `json:"trafficMultiplier" form:"trafficMultiplier" gorm:"default:1"` // Multiplier for inbound/client traffic accounting
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
|
||||
|
||||
// Xray configuration fields
|
||||
|
|
|
|||
|
|
@ -312,6 +312,7 @@
|
|||
"remark": "VLESS-443",
|
||||
"enable": true,
|
||||
"expiryTime": 0,
|
||||
"trafficMultiplier": 1,
|
||||
"listen": "",
|
||||
"port": 443,
|
||||
"protocol": "vless",
|
||||
|
|
@ -500,6 +501,7 @@
|
|||
"protocol": "vless",
|
||||
"expiryTime": 0,
|
||||
"total": 0,
|
||||
"trafficMultiplier": 1,
|
||||
"settings": {
|
||||
"clients": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export type DBInboundInit = Partial<{
|
|||
expiryTime: number;
|
||||
trafficReset: string;
|
||||
lastTrafficResetTime: number;
|
||||
trafficMultiplier: number;
|
||||
listen: string;
|
||||
port: number;
|
||||
protocol: string;
|
||||
|
|
@ -73,6 +74,7 @@ export class DBInbound {
|
|||
expiryTime: number;
|
||||
trafficReset: string;
|
||||
lastTrafficResetTime: number;
|
||||
trafficMultiplier: number;
|
||||
|
||||
listen: string;
|
||||
port: number;
|
||||
|
|
@ -99,6 +101,7 @@ export class DBInbound {
|
|||
this.expiryTime = 0;
|
||||
this.trafficReset = "never";
|
||||
this.lastTrafficResetTime = 0;
|
||||
this.trafficMultiplier = 1;
|
||||
|
||||
this.listen = "";
|
||||
this.port = 0;
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export const sections: readonly Section[] = [
|
|||
path: '/panel/api/inbounds/list',
|
||||
summary: 'List every inbound owned by the authenticated user, including each inbound’s clientStats traffic counters. settings, streamSettings, and sniffing are returned as nested JSON objects (no escaped strings); legacy callers that send them back as JSON-encoded strings are still accepted on write.',
|
||||
response:
|
||||
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "userId": 1,\n "up": 0,\n "down": 0,\n "total": 0,\n "remark": "VLESS-443",\n "enable": true,\n "expiryTime": 0,\n "listen": "",\n "port": 443,\n "protocol": "vless",\n "settings": {\n "clients": [],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": { "show": false, "dest": "..." }\n },\n "tag": "inbound-443",\n "sniffing": {\n "enabled": true,\n "destOverride": ["http", "tls"]\n },\n "clientStats": []\n }\n ]\n}',
|
||||
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "userId": 1,\n "up": 0,\n "down": 0,\n "total": 0,\n "remark": "VLESS-443",\n "enable": true,\n "expiryTime": 0,\n "trafficMultiplier": 1,\n "listen": "",\n "port": 443,\n "protocol": "vless",\n "settings": {\n "clients": [],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": { "show": false, "dest": "..." }\n },\n "tag": "inbound-443",\n "sniffing": {\n "enabled": true,\n "destOverride": ["http", "tls"]\n },\n "clientStats": []\n }\n ]\n}',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
|
|
@ -137,7 +137,7 @@ export const sections: readonly Section[] = [
|
|||
path: '/panel/api/inbounds/add',
|
||||
summary: 'Create a new inbound. Send the full inbound payload (protocol, port, settings, streamSettings, sniffing, remark, expiryTime, total, enable). settings, streamSettings, and sniffing may be sent as nested JSON objects (preferred) or as JSON-encoded strings (legacy).',
|
||||
body:
|
||||
'{\n "enable": true,\n "remark": "VLESS-443",\n "listen": "",\n "port": 443,\n "protocol": "vless",\n "expiryTime": 0,\n "total": 0,\n "settings": {\n "clients": [{ "id": "...", "email": "user1" }],\n "decryption": "none",\n "fallbacks": []\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": { "show": false, "dest": "..." }\n },\n "sniffing": {\n "enabled": true,\n "destOverride": ["http", "tls"]\n }\n}',
|
||||
'{\n "enable": true,\n "remark": "VLESS-443",\n "listen": "",\n "port": 443,\n "protocol": "vless",\n "expiryTime": 0,\n "total": 0,\n "trafficMultiplier": 1,\n "settings": {\n "clients": [{ "id": "...", "email": "user1" }],\n "decryption": "none",\n "fallbacks": []\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": { "show": false, "dest": "..." }\n },\n "sniffing": {\n "enabled": true,\n "destOverride": ["http", "tls"]\n }\n}',
|
||||
errorResponse:
|
||||
'{\n "success": false,\n "msg": "Port 443 is already in use"\n}',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -940,6 +940,7 @@ export default function InboundFormModal({
|
|||
expiryTime: form.expiryTime,
|
||||
trafficReset: form.trafficReset,
|
||||
lastTrafficResetTime: form.lastTrafficResetTime || 0,
|
||||
trafficMultiplier: form.trafficMultiplier || 1,
|
||||
listen: ib.listen,
|
||||
port: ib.port,
|
||||
protocol: ib.protocol,
|
||||
|
|
@ -1052,6 +1053,18 @@ export default function InboundFormModal({
|
|||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('pages.inbounds.trafficMultiplier')}>
|
||||
<InputNumber
|
||||
value={form.trafficMultiplier || 1}
|
||||
min={0.01}
|
||||
step={0.1}
|
||||
onChange={(v) => {
|
||||
const next = Number(v);
|
||||
form.trafficMultiplier = next > 0 ? next : 1;
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('pages.inbounds.periodicTrafficResetTitle')}>
|
||||
<Select value={form.trafficReset} onChange={(v) => { form.trafficReset = v; refresh(); }}>
|
||||
{TRAFFIC_RESETS.map((r) => (
|
||||
|
|
|
|||
|
|
@ -363,6 +363,7 @@ export default function InboundsPage() {
|
|||
remark: `${dbInbound.remark} (clone)`,
|
||||
enable: false,
|
||||
expiryTime: 0,
|
||||
trafficMultiplier: dbInbound.trafficMultiplier || 1,
|
||||
listen: '',
|
||||
port: RandomUtil.randomInteger(10000, 60000),
|
||||
protocol: baseInbound.protocol,
|
||||
|
|
|
|||
|
|
@ -357,6 +357,7 @@ func wireInbound(ib *model.Inbound) url.Values {
|
|||
v.Set("remark", ib.Remark)
|
||||
v.Set("enable", strconv.FormatBool(ib.Enable))
|
||||
v.Set("expiryTime", strconv.FormatInt(ib.ExpiryTime, 10))
|
||||
v.Set("trafficMultiplier", strconv.FormatFloat(ib.TrafficMultiplier, 'f', -1, 64))
|
||||
v.Set("listen", ib.Listen)
|
||||
v.Set("port", strconv.Itoa(ib.Port))
|
||||
v.Set("protocol", string(ib.Protocol))
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -29,6 +30,69 @@ type InboundService struct {
|
|||
fallbackService FallbackService
|
||||
}
|
||||
|
||||
func normalizeTrafficMultiplier(multiplier float64) float64 {
|
||||
if multiplier <= 0 || math.IsNaN(multiplier) || math.IsInf(multiplier, 0) {
|
||||
return 1
|
||||
}
|
||||
return multiplier
|
||||
}
|
||||
|
||||
func scaleTraffic(value int64, multiplier float64) int64 {
|
||||
if value == 0 {
|
||||
return 0
|
||||
}
|
||||
scaled := float64(value) * normalizeTrafficMultiplier(multiplier)
|
||||
if scaled >= float64(math.MaxInt64) {
|
||||
return math.MaxInt64
|
||||
}
|
||||
if scaled <= float64(math.MinInt64) {
|
||||
return math.MinInt64
|
||||
}
|
||||
return int64(math.Round(scaled))
|
||||
}
|
||||
|
||||
func addScaledTraffic(current int64, value int64, multiplier float64) int64 {
|
||||
delta := scaleTraffic(value, multiplier)
|
||||
if delta > 0 && current > math.MaxInt64-delta {
|
||||
return math.MaxInt64
|
||||
}
|
||||
if delta == math.MinInt64 && current < 0 {
|
||||
return math.MinInt64
|
||||
}
|
||||
if delta < 0 && current < math.MinInt64-delta {
|
||||
return math.MinInt64
|
||||
}
|
||||
return current + delta
|
||||
}
|
||||
|
||||
func saturatedTrafficAddExpr(column string, delta int64) clause.Expr {
|
||||
if delta > 0 {
|
||||
return gorm.Expr(
|
||||
"CASE WHEN "+column+" > ? THEN ? ELSE "+column+" + ? END",
|
||||
math.MaxInt64-delta,
|
||||
int64(math.MaxInt64),
|
||||
delta,
|
||||
)
|
||||
}
|
||||
if delta < 0 {
|
||||
if delta == math.MinInt64 {
|
||||
return gorm.Expr(
|
||||
"CASE WHEN "+column+" < ? THEN ? ELSE "+column+" + ? END",
|
||||
int64(0),
|
||||
int64(math.MinInt64),
|
||||
delta,
|
||||
)
|
||||
}
|
||||
return gorm.Expr(
|
||||
"CASE WHEN "+column+" < ? THEN ? ELSE "+column+" + ? END",
|
||||
math.MinInt64-delta,
|
||||
int64(math.MinInt64),
|
||||
delta,
|
||||
)
|
||||
}
|
||||
return gorm.Expr(column+" + ?", delta)
|
||||
}
|
||||
|
||||
func (s *InboundService) runtimeFor(ib *model.Inbound) (runtime.Runtime, error) {
|
||||
mgr := runtime.GetManager()
|
||||
if mgr == nil {
|
||||
|
|
@ -467,6 +531,7 @@ func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
|
|||
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||
// Normalize streamSettings based on protocol
|
||||
s.normalizeStreamSettings(inbound)
|
||||
inbound.TrafficMultiplier = normalizeTrafficMultiplier(inbound.TrafficMultiplier)
|
||||
|
||||
exist, err := s.checkPortConflict(inbound, 0)
|
||||
if err != nil {
|
||||
|
|
@ -735,6 +800,7 @@ func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) {
|
|||
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||
// Normalize streamSettings based on protocol
|
||||
s.normalizeStreamSettings(inbound)
|
||||
inbound.TrafficMultiplier = normalizeTrafficMultiplier(inbound.TrafficMultiplier)
|
||||
|
||||
exist, err := s.checkPortConflict(inbound, inbound.Id)
|
||||
if err != nil {
|
||||
|
|
@ -831,6 +897,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||
oldInbound.Enable = inbound.Enable
|
||||
oldInbound.ExpiryTime = inbound.ExpiryTime
|
||||
oldInbound.TrafficReset = inbound.TrafficReset
|
||||
oldInbound.TrafficMultiplier = inbound.TrafficMultiplier
|
||||
oldInbound.Listen = inbound.Listen
|
||||
oldInbound.Port = inbound.Port
|
||||
oldInbound.Protocol = inbound.Protocol
|
||||
|
|
@ -1315,22 +1382,23 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
|||
c, ok := tagToCentral[snapIb.Tag]
|
||||
if !ok {
|
||||
newIb := model.Inbound{
|
||||
UserId: defaultUserId,
|
||||
NodeID: &nodeID,
|
||||
Tag: snapIb.Tag,
|
||||
Listen: snapIb.Listen,
|
||||
Port: snapIb.Port,
|
||||
Protocol: snapIb.Protocol,
|
||||
Settings: snapIb.Settings,
|
||||
StreamSettings: snapIb.StreamSettings,
|
||||
Sniffing: snapIb.Sniffing,
|
||||
TrafficReset: snapIb.TrafficReset,
|
||||
Enable: snapIb.Enable,
|
||||
Remark: snapIb.Remark,
|
||||
Total: snapIb.Total,
|
||||
ExpiryTime: snapIb.ExpiryTime,
|
||||
Up: snapIb.Up,
|
||||
Down: snapIb.Down,
|
||||
UserId: defaultUserId,
|
||||
NodeID: &nodeID,
|
||||
Tag: snapIb.Tag,
|
||||
Listen: snapIb.Listen,
|
||||
Port: snapIb.Port,
|
||||
Protocol: snapIb.Protocol,
|
||||
Settings: snapIb.Settings,
|
||||
StreamSettings: snapIb.StreamSettings,
|
||||
Sniffing: snapIb.Sniffing,
|
||||
TrafficReset: snapIb.TrafficReset,
|
||||
Enable: snapIb.Enable,
|
||||
Remark: snapIb.Remark,
|
||||
Total: snapIb.Total,
|
||||
ExpiryTime: snapIb.ExpiryTime,
|
||||
TrafficMultiplier: normalizeTrafficMultiplier(snapIb.TrafficMultiplier),
|
||||
Up: snapIb.Up,
|
||||
Down: snapIb.Down,
|
||||
}
|
||||
if err := tx.Create(&newIb).Error; err != nil {
|
||||
logger.Warning("setRemoteTraffic: create central inbound for tag", snapIb.Tag, "failed:", err)
|
||||
|
|
@ -1342,19 +1410,21 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
|||
}
|
||||
|
||||
inGrace := c.LastTrafficResetTime > 0 && now-c.LastTrafficResetTime < resetGracePeriodMs
|
||||
snapMultiplier := normalizeTrafficMultiplier(snapIb.TrafficMultiplier)
|
||||
|
||||
updates := map[string]any{
|
||||
"enable": snapIb.Enable,
|
||||
"remark": snapIb.Remark,
|
||||
"listen": snapIb.Listen,
|
||||
"port": snapIb.Port,
|
||||
"protocol": snapIb.Protocol,
|
||||
"total": snapIb.Total,
|
||||
"expiry_time": snapIb.ExpiryTime,
|
||||
"settings": snapIb.Settings,
|
||||
"stream_settings": snapIb.StreamSettings,
|
||||
"sniffing": snapIb.Sniffing,
|
||||
"traffic_reset": snapIb.TrafficReset,
|
||||
"enable": snapIb.Enable,
|
||||
"remark": snapIb.Remark,
|
||||
"listen": snapIb.Listen,
|
||||
"port": snapIb.Port,
|
||||
"protocol": snapIb.Protocol,
|
||||
"total": snapIb.Total,
|
||||
"expiry_time": snapIb.ExpiryTime,
|
||||
"traffic_multiplier": snapMultiplier,
|
||||
"settings": snapIb.Settings,
|
||||
"stream_settings": snapIb.StreamSettings,
|
||||
"sniffing": snapIb.Sniffing,
|
||||
"traffic_reset": snapIb.TrafficReset,
|
||||
}
|
||||
if !inGrace || (snapIb.Up+snapIb.Down) <= (c.Up+c.Down) {
|
||||
updates["up"] = snapIb.Up
|
||||
|
|
@ -1367,6 +1437,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
|||
c.Port != snapIb.Port ||
|
||||
c.Total != snapIb.Total ||
|
||||
c.ExpiryTime != snapIb.ExpiryTime ||
|
||||
c.TrafficMultiplier != snapMultiplier ||
|
||||
c.Enable != snapIb.Enable {
|
||||
structuralChange = true
|
||||
}
|
||||
|
|
@ -1640,13 +1711,49 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic
|
|||
}
|
||||
|
||||
var err error
|
||||
tags := make([]string, 0, len(traffics))
|
||||
seenTags := make(map[string]struct{}, len(traffics))
|
||||
for _, traffic := range traffics {
|
||||
if traffic == nil || !traffic.IsInbound || traffic.Tag == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenTags[traffic.Tag]; ok {
|
||||
continue
|
||||
}
|
||||
seenTags[traffic.Tag] = struct{}{}
|
||||
tags = append(tags, traffic.Tag)
|
||||
}
|
||||
|
||||
multiplierByTag := make(map[string]float64, len(tags))
|
||||
if len(tags) > 0 {
|
||||
var rows []struct {
|
||||
Tag string
|
||||
TrafficMultiplier float64
|
||||
}
|
||||
err = tx.Model(&model.Inbound{}).
|
||||
Select("tag, traffic_multiplier").
|
||||
Where("tag IN ? AND node_id IS NULL", tags).
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
multiplierByTag[row.Tag] = normalizeTrafficMultiplier(row.TrafficMultiplier)
|
||||
}
|
||||
}
|
||||
|
||||
for _, traffic := range traffics {
|
||||
if traffic == nil {
|
||||
continue
|
||||
}
|
||||
if traffic.IsInbound {
|
||||
multiplier := multiplierByTag[traffic.Tag]
|
||||
up := scaleTraffic(traffic.Up, multiplier)
|
||||
down := scaleTraffic(traffic.Down, multiplier)
|
||||
err = tx.Model(&model.Inbound{}).Where("tag = ? AND node_id IS NULL", traffic.Tag).
|
||||
Updates(map[string]any{
|
||||
"up": gorm.Expr("up + ?", traffic.Up),
|
||||
"down": gorm.Expr("down + ?", traffic.Down),
|
||||
"up": saturatedTrafficAddExpr("up", up),
|
||||
"down": saturatedTrafficAddExpr("down", down),
|
||||
}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -1684,6 +1791,36 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
|||
return err
|
||||
}
|
||||
|
||||
inboundIDs := make([]int, 0, len(dbClientTraffics))
|
||||
seenInboundIDs := make(map[int]struct{}, len(dbClientTraffics))
|
||||
for _, dbClientTraffic := range dbClientTraffics {
|
||||
if dbClientTraffic == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenInboundIDs[dbClientTraffic.InboundId]; ok {
|
||||
continue
|
||||
}
|
||||
seenInboundIDs[dbClientTraffic.InboundId] = struct{}{}
|
||||
inboundIDs = append(inboundIDs, dbClientTraffic.InboundId)
|
||||
}
|
||||
multiplierByInboundID := make(map[int]float64, len(inboundIDs))
|
||||
if len(inboundIDs) > 0 {
|
||||
var rows []struct {
|
||||
Id int
|
||||
TrafficMultiplier float64
|
||||
}
|
||||
err = tx.Model(&model.Inbound{}).
|
||||
Select("id, traffic_multiplier").
|
||||
Where("id IN ?", inboundIDs).
|
||||
Find(&rows).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
multiplierByInboundID[row.Id] = normalizeTrafficMultiplier(row.TrafficMultiplier)
|
||||
}
|
||||
}
|
||||
|
||||
// Index by email for O(N) merge — the previous nested loop was O(N²)
|
||||
// and dominated each cron tick on inbounds with thousands of active
|
||||
// clients (7500 × 7500 = 56M string comparisons every 10 seconds).
|
||||
|
|
@ -1699,8 +1836,9 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
|||
if !ok {
|
||||
continue
|
||||
}
|
||||
dbClientTraffics[dbTraffic_index].Up += t.Up
|
||||
dbClientTraffics[dbTraffic_index].Down += t.Down
|
||||
multiplier := multiplierByInboundID[dbClientTraffics[dbTraffic_index].InboundId]
|
||||
dbClientTraffics[dbTraffic_index].Up = addScaledTraffic(dbClientTraffics[dbTraffic_index].Up, t.Up, multiplier)
|
||||
dbClientTraffics[dbTraffic_index].Down = addScaledTraffic(dbClientTraffics[dbTraffic_index].Down, t.Down, multiplier)
|
||||
if t.Up+t.Down > 0 {
|
||||
dbClientTraffics[dbTraffic_index].LastOnline = now
|
||||
}
|
||||
|
|
|
|||
141
web/service/inbound_traffic_multiplier_test.go
Normal file
141
web/service/inbound_traffic_multiplier_test.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"math"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/xray"
|
||||
)
|
||||
|
||||
func TestAddTrafficAppliesInboundTrafficMultiplier(t *testing.T) {
|
||||
dbDir := t.TempDir()
|
||||
t.Setenv("XUI_DB_FOLDER", dbDir)
|
||||
if err := database.InitDB(filepath.Join(dbDir, "3x-ui.db")); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.CloseDB() })
|
||||
|
||||
db := database.GetDB()
|
||||
inbound := model.Inbound{
|
||||
UserId: 1,
|
||||
Tag: "inbound-test",
|
||||
Protocol: model.VLESS,
|
||||
Settings: `{"clients":[{"id":"11111111-1111-1111-1111-111111111111","email":"alice@example.com","enable":true}],"decryption":"none"}`,
|
||||
TrafficMultiplier: 2,
|
||||
Enable: true,
|
||||
}
|
||||
if err := db.Create(&inbound).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
if err := db.Create(&xray.ClientTraffic{
|
||||
InboundId: inbound.Id,
|
||||
Email: "alice@example.com",
|
||||
Enable: true,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create client traffic: %v", err)
|
||||
}
|
||||
|
||||
s := InboundService{}
|
||||
_, _, err := s.AddTraffic(
|
||||
[]*xray.Traffic{{
|
||||
IsInbound: true,
|
||||
Tag: inbound.Tag,
|
||||
Up: 100,
|
||||
Down: 200,
|
||||
}},
|
||||
[]*xray.ClientTraffic{{
|
||||
Email: "alice@example.com",
|
||||
Up: 10,
|
||||
Down: 20,
|
||||
}},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("AddTraffic: %v", err)
|
||||
}
|
||||
|
||||
var storedInbound model.Inbound
|
||||
if err := db.First(&storedInbound, inbound.Id).Error; err != nil {
|
||||
t.Fatalf("load inbound: %v", err)
|
||||
}
|
||||
if storedInbound.Up != 200 || storedInbound.Down != 400 {
|
||||
t.Fatalf("inbound traffic = up %d/down %d, want up 200/down 400", storedInbound.Up, storedInbound.Down)
|
||||
}
|
||||
|
||||
var storedClient xray.ClientTraffic
|
||||
if err := db.Where("email = ?", "alice@example.com").First(&storedClient).Error; err != nil {
|
||||
t.Fatalf("load client traffic: %v", err)
|
||||
}
|
||||
if storedClient.Up != 20 || storedClient.Down != 40 {
|
||||
t.Fatalf("client traffic = up %d/down %d, want up 20/down 40", storedClient.Up, storedClient.Down)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTrafficSaturatesMultipliedInboundCounters(t *testing.T) {
|
||||
dbDir := t.TempDir()
|
||||
t.Setenv("XUI_DB_FOLDER", dbDir)
|
||||
if err := database.InitDB(filepath.Join(dbDir, "3x-ui.db")); err != nil {
|
||||
t.Fatalf("InitDB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.CloseDB() })
|
||||
|
||||
db := database.GetDB()
|
||||
inbound := model.Inbound{
|
||||
UserId: 1,
|
||||
Tag: "inbound-overflow",
|
||||
Protocol: model.VLESS,
|
||||
Settings: `{"clients":[{"id":"22222222-2222-2222-2222-222222222222","email":"bob@example.com","enable":true}],"decryption":"none"}`,
|
||||
TrafficMultiplier: 2,
|
||||
Up: math.MaxInt64 - 5,
|
||||
Down: math.MaxInt64 - 5,
|
||||
Enable: true,
|
||||
}
|
||||
if err := db.Create(&inbound).Error; err != nil {
|
||||
t.Fatalf("create inbound: %v", err)
|
||||
}
|
||||
if err := db.Create(&xray.ClientTraffic{
|
||||
InboundId: inbound.Id,
|
||||
Email: "bob@example.com",
|
||||
Enable: true,
|
||||
Up: math.MaxInt64 - 5,
|
||||
Down: math.MaxInt64 - 5,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create client traffic: %v", err)
|
||||
}
|
||||
|
||||
s := InboundService{}
|
||||
_, _, err := s.AddTraffic(
|
||||
[]*xray.Traffic{{
|
||||
IsInbound: true,
|
||||
Tag: inbound.Tag,
|
||||
Up: 10,
|
||||
Down: 10,
|
||||
}},
|
||||
[]*xray.ClientTraffic{{
|
||||
Email: "bob@example.com",
|
||||
Up: 10,
|
||||
Down: 10,
|
||||
}},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("AddTraffic: %v", err)
|
||||
}
|
||||
|
||||
var storedInbound model.Inbound
|
||||
if err := db.First(&storedInbound, inbound.Id).Error; err != nil {
|
||||
t.Fatalf("load inbound: %v", err)
|
||||
}
|
||||
if storedInbound.Up != math.MaxInt64 || storedInbound.Down != math.MaxInt64 {
|
||||
t.Fatalf("inbound traffic = up %d/down %d, want saturated MaxInt64", storedInbound.Up, storedInbound.Down)
|
||||
}
|
||||
|
||||
var storedClient xray.ClientTraffic
|
||||
if err := db.Where("email = ?", "bob@example.com").First(&storedClient).Error; err != nil {
|
||||
t.Fatalf("load client traffic: %v", err)
|
||||
}
|
||||
if storedClient.Up != math.MaxInt64 || storedClient.Down != math.MaxInt64 {
|
||||
t.Fatalf("client traffic = up %d/down %d, want saturated MaxInt64", storedClient.Up, storedClient.Down)
|
||||
}
|
||||
}
|
||||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "سيبها فاضية لو عايز تستمع على كل الـ IPs",
|
||||
"meansNoLimit": "= غير محدود. (الوحدة: جيجابايت)",
|
||||
"totalFlow": "إجمالي التدفق",
|
||||
"trafficMultiplier": "مضاعف حركة المرور",
|
||||
"leaveBlankToNeverExpire": "سيبها فاضية عشان ماتنتهيش",
|
||||
"noRecommendKeepDefault": "ننصح باستخدام الافتراضي",
|
||||
"certificatePath": "مسار الملف",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "Leave blank to listen on all IPs",
|
||||
"meansNoLimit": "= Unlimited. (unit: GB)",
|
||||
"totalFlow": "Total Flow",
|
||||
"trafficMultiplier": "Traffic Multiplier",
|
||||
"leaveBlankToNeverExpire": "Leave blank to never expire",
|
||||
"noRecommendKeepDefault": "It is recommended to keep the default",
|
||||
"certificatePath": "File Path",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "Dejar en blanco por defecto",
|
||||
"meansNoLimit": " = illimitata. (unidad: GB)",
|
||||
"totalFlow": "Flujo Total",
|
||||
"trafficMultiplier": "Multiplicador de Tráfico",
|
||||
"leaveBlankToNeverExpire": "Dejar en Blanco para Nunca Expirar",
|
||||
"noRecommendKeepDefault": "No hay requisitos especiales para mantener la configuración predeterminada",
|
||||
"certificatePath": "Ruta Cert",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "بهطور پیشفرض خالیبگذارید",
|
||||
"meansNoLimit": "0 = واحد: گیگابایت) نامحدود)",
|
||||
"totalFlow": "ترافیک کل",
|
||||
"trafficMultiplier": "ضریب ترافیک",
|
||||
"leaveBlankToNeverExpire": "برای منقضینشدن خالیبگذارید",
|
||||
"noRecommendKeepDefault": "توصیهمیشود بهطور پیشفرض حفظشود",
|
||||
"certificatePath": "مسیر فایل",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "Biarkan kosong untuk mendengarkan semua IP",
|
||||
"meansNoLimit": "= Unlimited. (unit: GB)",
|
||||
"totalFlow": "Total Aliran",
|
||||
"trafficMultiplier": "Pengali Lalu Lintas",
|
||||
"leaveBlankToNeverExpire": "Biarkan kosong untuk tidak pernah kedaluwarsa",
|
||||
"noRecommendKeepDefault": "Disarankan untuk tetap menggunakan pengaturan default",
|
||||
"certificatePath": "Path Berkas",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "空白にするとすべてのIPを監視",
|
||||
"meansNoLimit": "= 無制限(単位:GB)",
|
||||
"totalFlow": "総トラフィック",
|
||||
"trafficMultiplier": "トラフィック倍率",
|
||||
"leaveBlankToNeverExpire": "空白にすると期限なし",
|
||||
"noRecommendKeepDefault": "デフォルト値を保持することをお勧めします",
|
||||
"certificatePath": "ファイルパス",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "Deixe em branco para ouvir todos os IPs",
|
||||
"meansNoLimit": "= Ilimitado. (unidade: GB)",
|
||||
"totalFlow": "Fluxo Total",
|
||||
"trafficMultiplier": "Multiplicador de Tráfego",
|
||||
"leaveBlankToNeverExpire": "Deixe em branco para nunca expirar",
|
||||
"noRecommendKeepDefault": "Recomenda-se manter o padrão",
|
||||
"certificatePath": "Caminho",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "Оставьте пустым для прослушивания всех IP-адресов",
|
||||
"meansNoLimit": "= Без ограничений (значение: ГБ)",
|
||||
"totalFlow": "Общий расход",
|
||||
"trafficMultiplier": "Множитель трафика",
|
||||
"leaveBlankToNeverExpire": "Оставьте пустым, чтобы было бесконечным",
|
||||
"noRecommendKeepDefault": "Рекомендуется оставить настройки по умолчанию",
|
||||
"certificatePath": "Путь к сертификату",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "Tüm IP'leri dinlemek için boş bırakın",
|
||||
"meansNoLimit": "= Sınırsız. (birim: GB)",
|
||||
"totalFlow": "Toplam Akış",
|
||||
"trafficMultiplier": "Trafik Çarpanı",
|
||||
"leaveBlankToNeverExpire": "Hiçbir zaman sona ermemesi için boş bırakın",
|
||||
"noRecommendKeepDefault": "Varsayılanı korumanız önerilir",
|
||||
"certificatePath": "Dosya Yolu",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "Залиште порожнім, щоб слухати всі IP-адреси",
|
||||
"meansNoLimit": "= Необмежено. (одиниця: ГБ)",
|
||||
"totalFlow": "Загальна витрата",
|
||||
"trafficMultiplier": "Множник трафіку",
|
||||
"leaveBlankToNeverExpire": "Залиште порожнім, щоб ніколи не закінчувався",
|
||||
"noRecommendKeepDefault": "Рекомендується зберегти значення за замовчуванням",
|
||||
"certificatePath": "Шлях до файлу",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "Mặc định để trống",
|
||||
"meansNoLimit": "= Không giới hạn (đơn vị: GB)",
|
||||
"totalFlow": "Tổng lưu lượng",
|
||||
"trafficMultiplier": "Hệ số lưu lượng",
|
||||
"leaveBlankToNeverExpire": "Để trống để không bao giờ hết hạn",
|
||||
"noRecommendKeepDefault": "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định",
|
||||
"certificatePath": "Đường dẫn tập",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "留空表示监听所有 IP",
|
||||
"meansNoLimit": "= 无限制(单位:GB)",
|
||||
"totalFlow": "总流量",
|
||||
"trafficMultiplier": "流量倍率",
|
||||
"leaveBlankToNeverExpire": "留空表示永不过期",
|
||||
"noRecommendKeepDefault": "建议保留默认值",
|
||||
"certificatePath": "文件路径",
|
||||
|
|
|
|||
|
|
@ -305,6 +305,7 @@
|
|||
"monitorDesc": "留空表示監聽所有 IP",
|
||||
"meansNoLimit": "= 無限制(單位:GB)",
|
||||
"totalFlow": "總流量",
|
||||
"trafficMultiplier": "流量倍率",
|
||||
"leaveBlankToNeverExpire": "留空表示永不過期",
|
||||
"noRecommendKeepDefault": "建議保留預設值",
|
||||
"certificatePath": "檔案路徑",
|
||||
|
|
|
|||
Loading…
Reference in a new issue