feat: add inbound traffic multiplier

This commit is contained in:
byang37 2026-05-27 00:58:04 +08:00
parent 20edaee8ed
commit 8c74a4eff5
22 changed files with 359 additions and 46 deletions

View file

@ -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

View file

@ -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": [
{

View file

@ -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;

View file

@ -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 inbounds 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}',
},

View file

@ -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) => (

View file

@ -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,

View file

@ -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))

View file

@ -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
}

View 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)
}
}

View file

@ -305,6 +305,7 @@
"monitorDesc": "سيبها فاضية لو عايز تستمع على كل الـ IPs",
"meansNoLimit": "= غير محدود. (الوحدة: جيجابايت)",
"totalFlow": "إجمالي التدفق",
"trafficMultiplier": "مضاعف حركة المرور",
"leaveBlankToNeverExpire": "سيبها فاضية عشان ماتنتهيش",
"noRecommendKeepDefault": "ننصح باستخدام الافتراضي",
"certificatePath": "مسار الملف",

View file

@ -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",

View file

@ -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",

View file

@ -305,6 +305,7 @@
"monitorDesc": "به‌طور پیش‌فرض خالی‌بگذارید",
"meansNoLimit": "0 = واحد: گیگابایت) نامحدود)",
"totalFlow": "ترافیک کل",
"trafficMultiplier": "ضریب ترافیک",
"leaveBlankToNeverExpire": "برای منقضی‌نشدن خالی‌بگذارید",
"noRecommendKeepDefault": "توصیه‌می‌شود به‌طور پیش‌فرض حفظ‌شود",
"certificatePath": "مسیر فایل",

View file

@ -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",

View file

@ -305,6 +305,7 @@
"monitorDesc": "空白にするとすべてのIPを監視",
"meansNoLimit": "= 無制限単位GB",
"totalFlow": "総トラフィック",
"trafficMultiplier": "トラフィック倍率",
"leaveBlankToNeverExpire": "空白にすると期限なし",
"noRecommendKeepDefault": "デフォルト値を保持することをお勧めします",
"certificatePath": "ファイルパス",

View file

@ -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",

View file

@ -305,6 +305,7 @@
"monitorDesc": "Оставьте пустым для прослушивания всех IP-адресов",
"meansNoLimit": "= Без ограничений (значение: ГБ)",
"totalFlow": "Общий расход",
"trafficMultiplier": "Множитель трафика",
"leaveBlankToNeverExpire": "Оставьте пустым, чтобы было бесконечным",
"noRecommendKeepDefault": "Рекомендуется оставить настройки по умолчанию",
"certificatePath": "Путь к сертификату",

View file

@ -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",

View file

@ -305,6 +305,7 @@
"monitorDesc": "Залиште порожнім, щоб слухати всі IP-адреси",
"meansNoLimit": "= Необмежено. (одиниця: ГБ)",
"totalFlow": "Загальна витрата",
"trafficMultiplier": "Множник трафіку",
"leaveBlankToNeverExpire": "Залиште порожнім, щоб ніколи не закінчувався",
"noRecommendKeepDefault": "Рекомендується зберегти значення за замовчуванням",
"certificatePath": "Шлях до файлу",

View file

@ -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",

View file

@ -305,6 +305,7 @@
"monitorDesc": "留空表示监听所有 IP",
"meansNoLimit": "= 无限制单位GB)",
"totalFlow": "总流量",
"trafficMultiplier": "流量倍率",
"leaveBlankToNeverExpire": "留空表示永不过期",
"noRecommendKeepDefault": "建议保留默认值",
"certificatePath": "文件路径",

View file

@ -305,6 +305,7 @@
"monitorDesc": "留空表示監聽所有 IP",
"meansNoLimit": "= 無限制單位GB)",
"totalFlow": "總流量",
"trafficMultiplier": "流量倍率",
"leaveBlankToNeverExpire": "留空表示永不過期",
"noRecommendKeepDefault": "建議保留預設值",
"certificatePath": "檔案路徑",