mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
feat: add per-inbound download/upload speed limit (KB/s) via Linux tc
Agent-Logs-Url: https://github.com/xAlokyx/3x-ui/sessions/9e10f937-9919-4186-8ac8-caa00f2593b8 Co-authored-by: xAlokyx <234771438+xAlokyx@users.noreply.github.com>
This commit is contained in:
parent
59782779fd
commit
51a4d33a91
20 changed files with 306 additions and 5 deletions
|
|
@ -45,6 +45,10 @@ type Inbound struct {
|
||||||
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
|
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
|
||||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
|
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
|
||||||
|
|
||||||
|
// Speed limit fields (KB/s, 0 = unlimited)
|
||||||
|
SpeedLimitDown int64 `json:"speedLimitDown" form:"speedLimitDown" gorm:"default:0"` // Download speed limit in KB/s
|
||||||
|
SpeedLimitUp int64 `json:"speedLimitUp" form:"speedLimitUp" gorm:"default:0"` // Upload speed limit in KB/s
|
||||||
|
|
||||||
// Xray configuration fields
|
// Xray configuration fields
|
||||||
Listen string `json:"listen" form:"listen"`
|
Listen string `json:"listen" form:"listen"`
|
||||||
Port int `json:"port" form:"port"`
|
Port int `json:"port" form:"port"`
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ class DBInbound {
|
||||||
this.expiryTime = 0;
|
this.expiryTime = 0;
|
||||||
this.trafficReset = "never";
|
this.trafficReset = "never";
|
||||||
this.lastTrafficResetTime = 0;
|
this.lastTrafficResetTime = 0;
|
||||||
|
this.speedLimitDown = 0;
|
||||||
|
this.speedLimitUp = 0;
|
||||||
|
|
||||||
this.listen = "";
|
this.listen = "";
|
||||||
this.port = 0;
|
this.port = 0;
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,34 @@
|
||||||
:min="0"></a-input-number>
|
: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>
|
||||||
|
{{ i18n "pages.inbounds.downloadSpeedLimit" }}
|
||||||
|
<a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-input-number v-model.number="dbInbound.speedLimitDown"
|
||||||
|
: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>
|
||||||
|
{{ i18n "pages.inbounds.uploadSpeedLimit" }}
|
||||||
|
<a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-input-number v-model.number="dbInbound.speedLimitUp"
|
||||||
|
:min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
|
|
|
||||||
|
|
@ -1063,6 +1063,8 @@
|
||||||
expiryTime: dbInbound.expiryTime,
|
expiryTime: dbInbound.expiryTime,
|
||||||
trafficReset: dbInbound.trafficReset,
|
trafficReset: dbInbound.trafficReset,
|
||||||
lastTrafficResetTime: dbInbound.lastTrafficResetTime,
|
lastTrafficResetTime: dbInbound.lastTrafficResetTime,
|
||||||
|
speedLimitDown: dbInbound.speedLimitDown || 0,
|
||||||
|
speedLimitUp: dbInbound.speedLimitUp || 0,
|
||||||
|
|
||||||
listen: inbound.listen,
|
listen: inbound.listen,
|
||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
|
|
@ -1088,6 +1090,8 @@
|
||||||
expiryTime: dbInbound.expiryTime,
|
expiryTime: dbInbound.expiryTime,
|
||||||
trafficReset: dbInbound.trafficReset,
|
trafficReset: dbInbound.trafficReset,
|
||||||
lastTrafficResetTime: dbInbound.lastTrafficResetTime,
|
lastTrafficResetTime: dbInbound.lastTrafficResetTime,
|
||||||
|
speedLimitDown: dbInbound.speedLimitDown || 0,
|
||||||
|
speedLimitUp: dbInbound.speedLimitUp || 0,
|
||||||
|
|
||||||
listen: inbound.listen,
|
listen: inbound.listen,
|
||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ import (
|
||||||
// It handles CRUD operations for inbounds, client management, traffic monitoring,
|
// It handles CRUD operations for inbounds, client management, traffic monitoring,
|
||||||
// and integration with the Xray API for real-time updates.
|
// and integration with the Xray API for real-time updates.
|
||||||
type InboundService struct {
|
type InboundService struct {
|
||||||
xrayApi xray.XrayAPI
|
xrayApi xray.XrayAPI
|
||||||
|
speedLimitService SpeedLimitService
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInbounds retrieves all inbounds for a specific user.
|
// GetInbounds retrieves all inbounds for a specific user.
|
||||||
|
|
@ -316,6 +317,12 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||||
s.xrayApi.Close()
|
s.xrayApi.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
if applyErr := s.speedLimitService.ApplySpeedLimit(inbound); applyErr != nil {
|
||||||
|
logger.Warningf("AddInbound: speed limit: %v", applyErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return inbound, needRestart, err
|
return inbound, needRestart, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,6 +369,9 @@ func (s *InboundService) DelInbound(id int) (bool, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove tc speed limit rules before deleting the record.
|
||||||
|
s.speedLimitService.RemoveSpeedLimit(inbound)
|
||||||
|
|
||||||
return needRestart, db.Delete(model.Inbound{}, id).Error
|
return needRestart, db.Delete(model.Inbound{}, id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -510,7 +520,16 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||||
}
|
}
|
||||||
s.xrayApi.Close()
|
s.xrayApi.Close()
|
||||||
|
|
||||||
return inbound, needRestart, tx.Save(oldInbound).Error
|
// Propagate the new speed limits to tc (must happen after the save so the ID is stable).
|
||||||
|
oldInbound.SpeedLimitDown = inbound.SpeedLimitDown
|
||||||
|
oldInbound.SpeedLimitUp = inbound.SpeedLimitUp
|
||||||
|
saveErr := tx.Save(oldInbound).Error
|
||||||
|
if saveErr == nil {
|
||||||
|
if applyErr := s.speedLimitService.ApplySpeedLimit(oldInbound); applyErr != nil {
|
||||||
|
logger.Warningf("UpdateInbound: speed limit: %v", applyErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inbound, needRestart, saveErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) updateClientTraffics(tx *gorm.DB, oldInbound *model.Inbound, newInbound *model.Inbound) error {
|
func (s *InboundService) updateClientTraffics(tx *gorm.DB, oldInbound *model.Inbound, newInbound *model.Inbound) error {
|
||||||
|
|
|
||||||
192
web/service/speedlimit.go
Normal file
192
web/service/speedlimit.go
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tcMaxRate = "10gbit"
|
||||||
|
tcMaxBurst = "100m"
|
||||||
|
tcRootHandle = "1:"
|
||||||
|
tcRootClass = "1:1"
|
||||||
|
tcDefaultClass = "1:65535"
|
||||||
|
tcDefaultPrio = "65535"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SpeedLimitService manages per-inbound bandwidth limiting using Linux tc (traffic control).
|
||||||
|
// Download limits are enforced via HTB egress classes; upload limits via ingress police filters.
|
||||||
|
// The service is stateless – all state lives in the kernel's tc tables.
|
||||||
|
type SpeedLimitService struct{}
|
||||||
|
|
||||||
|
// getDefaultInterface returns the name of the primary network interface used for the default route.
|
||||||
|
func (s *SpeedLimitService) getDefaultInterface() (string, error) {
|
||||||
|
out, err := exec.Command("ip", "route", "show", "default").Output()
|
||||||
|
if err == nil && len(out) > 0 {
|
||||||
|
fields := strings.Fields(string(out))
|
||||||
|
for i, f := range fields {
|
||||||
|
if f == "dev" && i+1 < len(fields) {
|
||||||
|
return fields[i+1], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("cannot determine default network interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTC(args ...string) error {
|
||||||
|
cmd := exec.Command("tc", args...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("tc %v: %v: %s", args, err, string(output))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureHTBRoot ensures the root HTB qdisc and required parent classes exist on the interface.
|
||||||
|
// It is safe to call multiple times (idempotent via add-then-change fallback).
|
||||||
|
func (s *SpeedLimitService) ensureHTBRoot(iface string) {
|
||||||
|
// Try to add; if it fails the qdisc already exists – try change in case it is already HTB.
|
||||||
|
if err := runTC("qdisc", "add", "dev", iface, "root",
|
||||||
|
"handle", tcRootHandle, "htb", "default", tcDefaultPrio); err != nil {
|
||||||
|
runTC("qdisc", "change", "dev", iface, "root",
|
||||||
|
"handle", tcRootHandle, "htb", "default", tcDefaultPrio)
|
||||||
|
}
|
||||||
|
// Root class (1:1) — full speed ceiling for all children.
|
||||||
|
if err := runTC("class", "add", "dev", iface, "parent", tcRootHandle,
|
||||||
|
"classid", tcRootClass, "htb", "rate", tcMaxRate, "burst", tcMaxBurst); err != nil {
|
||||||
|
runTC("class", "change", "dev", iface, "parent", tcRootHandle,
|
||||||
|
"classid", tcRootClass, "htb", "rate", tcMaxRate, "burst", tcMaxBurst)
|
||||||
|
}
|
||||||
|
// Default leaf class (1:65535) — unlimited, used by unmatched traffic.
|
||||||
|
if err := runTC("class", "add", "dev", iface, "parent", tcRootClass,
|
||||||
|
"classid", tcDefaultClass, "htb", "rate", tcMaxRate, "burst", tcMaxBurst); err != nil {
|
||||||
|
runTC("class", "change", "dev", iface, "parent", tcRootClass,
|
||||||
|
"classid", tcDefaultClass, "htb", "rate", tcMaxRate, "burst", tcMaxBurst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureIngress ensures an ingress qdisc exists on the interface.
|
||||||
|
func (s *SpeedLimitService) ensureIngress(iface string) {
|
||||||
|
// Ignore error – qdisc may already be present.
|
||||||
|
runTC("qdisc", "add", "dev", iface, "handle", "ffff:", "ingress")
|
||||||
|
}
|
||||||
|
|
||||||
|
// classID returns the HTB class identifier string for the given inbound.
|
||||||
|
func classID(inbound *model.Inbound) string {
|
||||||
|
return fmt.Sprintf("1:%d", inbound.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prioStr returns the tc filter priority string for the given inbound.
|
||||||
|
// Using the inbound ID keeps it unique and stable across service restarts.
|
||||||
|
func prioStr(inbound *model.Inbound) string {
|
||||||
|
return fmt.Sprintf("%d", inbound.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// kbitRate converts a KB/s value to a kbit/s string suitable for tc rate arguments.
|
||||||
|
func kbitRate(kbps int64) string {
|
||||||
|
return fmt.Sprintf("%dkbit", kbps*8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// burstValue returns a sensible burst size string for the given KB/s rate.
|
||||||
|
func burstValue(kbps int64) string {
|
||||||
|
burst := kbps / 8
|
||||||
|
if burst < 1 {
|
||||||
|
burst = 1
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%dk", burst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplySpeedLimit configures tc rules to enforce download/upload limits for the inbound.
|
||||||
|
// Calling with SpeedLimitDown == 0 && SpeedLimitUp == 0 removes any existing rules.
|
||||||
|
func (s *SpeedLimitService) ApplySpeedLimit(inbound *model.Inbound) error {
|
||||||
|
if inbound.SpeedLimitDown == 0 && inbound.SpeedLimitUp == 0 {
|
||||||
|
s.RemoveSpeedLimit(inbound)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
iface, err := s.getDefaultInterface()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("speed limit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove existing rules for this inbound first (idempotent).
|
||||||
|
s.RemoveSpeedLimit(inbound)
|
||||||
|
|
||||||
|
port := fmt.Sprintf("%d", inbound.Port)
|
||||||
|
prio := prioStr(inbound)
|
||||||
|
|
||||||
|
if inbound.SpeedLimitDown > 0 {
|
||||||
|
// Egress: limit traffic leaving the server (= client download).
|
||||||
|
s.ensureHTBRoot(iface)
|
||||||
|
|
||||||
|
rate := kbitRate(inbound.SpeedLimitDown)
|
||||||
|
burst := burstValue(inbound.SpeedLimitDown)
|
||||||
|
|
||||||
|
if err := runTC("class", "add", "dev", iface, "parent", tcRootClass,
|
||||||
|
"classid", classID(inbound), "htb", "rate", rate, "burst", burst); err != nil {
|
||||||
|
logger.Warningf("speed limit: failed to add egress class for inbound %d: %v", inbound.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runTC("filter", "add", "dev", iface, "parent", tcRootHandle,
|
||||||
|
"protocol", "ip", "prio", prio,
|
||||||
|
"u32", "match", "ip", "sport", port, "0xffff",
|
||||||
|
"flowid", classID(inbound)); err != nil {
|
||||||
|
logger.Warningf("speed limit: failed to add egress filter for inbound %d: %v", inbound.Id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if inbound.SpeedLimitUp > 0 {
|
||||||
|
// Ingress: police traffic arriving at the server (= client upload).
|
||||||
|
s.ensureIngress(iface)
|
||||||
|
|
||||||
|
rate := kbitRate(inbound.SpeedLimitUp)
|
||||||
|
burst := burstValue(inbound.SpeedLimitUp)
|
||||||
|
|
||||||
|
if err := runTC("filter", "add", "dev", iface, "parent", "ffff:",
|
||||||
|
"protocol", "ip", "prio", prio,
|
||||||
|
"u32", "match", "ip", "dport", port, "0xffff",
|
||||||
|
"police", "rate", rate, "burst", burst, "drop"); err != nil {
|
||||||
|
logger.Warningf("speed limit: failed to add ingress filter for inbound %d: %v", inbound.Id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveSpeedLimit deletes any tc rules previously applied for the inbound.
|
||||||
|
func (s *SpeedLimitService) RemoveSpeedLimit(inbound *model.Inbound) {
|
||||||
|
iface, err := s.getDefaultInterface()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("speed limit cleanup: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prio := prioStr(inbound)
|
||||||
|
|
||||||
|
// Delete egress filter and class (errors are expected when rules don't exist).
|
||||||
|
runTC("filter", "del", "dev", iface, "parent", tcRootHandle, "prio", prio)
|
||||||
|
runTC("class", "del", "dev", iface, "classid", classID(inbound))
|
||||||
|
|
||||||
|
// Delete ingress filter.
|
||||||
|
runTC("filter", "del", "dev", iface, "parent", "ffff:", "prio", prio)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreAllLimits re-applies tc rules for every enabled inbound that has a speed limit set.
|
||||||
|
// This should be called once on server startup since tc rules do not survive reboots.
|
||||||
|
func (s *SpeedLimitService) RestoreAllLimits(inbounds []*model.Inbound) {
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
if !inbound.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inbound.SpeedLimitDown > 0 || inbound.SpeedLimitUp > 0 {
|
||||||
|
if err := s.ApplySpeedLimit(inbound); err != nil {
|
||||||
|
logger.Errorf("speed limit restore: inbound %d: %v", inbound.Id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "سيبها فاضية لو عايز تستمع على كل الـ IPs"
|
"monitorDesc" = "سيبها فاضية لو عايز تستمع على كل الـ IPs"
|
||||||
"meansNoLimit" = "= غير محدود. (الوحدة: جيجابايت)"
|
"meansNoLimit" = "= غير محدود. (الوحدة: جيجابايت)"
|
||||||
"totalFlow" = "إجمالي التدفق"
|
"totalFlow" = "إجمالي التدفق"
|
||||||
|
"downloadSpeedLimit" = "حد سرعة التنزيل (كيلوبايت/ث)"
|
||||||
|
"uploadSpeedLimit" = "حد سرعة الرفع (كيلوبايت/ث)"
|
||||||
|
"speedLimitDesc" = "0 = غير محدود. (الوحدة: كيلوبايت/ث)"
|
||||||
"leaveBlankToNeverExpire" = "سيبها فاضية عشان ماتنتهيش"
|
"leaveBlankToNeverExpire" = "سيبها فاضية عشان ماتنتهيش"
|
||||||
"noRecommendKeepDefault" = "ننصح باستخدام الافتراضي"
|
"noRecommendKeepDefault" = "ننصح باستخدام الافتراضي"
|
||||||
"certificatePath" = "مسار الملف"
|
"certificatePath" = "مسار الملف"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "Leave blank to listen on all IPs"
|
"monitorDesc" = "Leave blank to listen on all IPs"
|
||||||
"meansNoLimit" = "= Unlimited. (unit: GB)"
|
"meansNoLimit" = "= Unlimited. (unit: GB)"
|
||||||
"totalFlow" = "Total Flow"
|
"totalFlow" = "Total Flow"
|
||||||
|
"downloadSpeedLimit" = "Download Speed Limit (KB/s)"
|
||||||
|
"uploadSpeedLimit" = "Upload Speed Limit (KB/s)"
|
||||||
|
"speedLimitDesc" = "0 = Unlimited. (unit: KB/s)"
|
||||||
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
||||||
"noRecommendKeepDefault" = "It is recommended to keep the default"
|
"noRecommendKeepDefault" = "It is recommended to keep the default"
|
||||||
"certificatePath" = "File Path"
|
"certificatePath" = "File Path"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "Dejar en blanco por defecto"
|
"monitorDesc" = "Dejar en blanco por defecto"
|
||||||
"meansNoLimit" = " = illimitata. (unidad: GB)"
|
"meansNoLimit" = " = illimitata. (unidad: GB)"
|
||||||
"totalFlow" = "Flujo Total"
|
"totalFlow" = "Flujo Total"
|
||||||
|
"downloadSpeedLimit" = "Límite de Velocidad de Descarga (KB/s)"
|
||||||
|
"uploadSpeedLimit" = "Límite de Velocidad de Subida (KB/s)"
|
||||||
|
"speedLimitDesc" = "0 = Ilimitado. (unidad: KB/s)"
|
||||||
"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar"
|
"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar"
|
||||||
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
|
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
|
||||||
"certificatePath" = "Ruta Cert"
|
"certificatePath" = "Ruta Cert"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "بهطور پیشفرض خالیبگذارید"
|
"monitorDesc" = "بهطور پیشفرض خالیبگذارید"
|
||||||
"meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)"
|
"meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)"
|
||||||
"totalFlow" = "ترافیک کل"
|
"totalFlow" = "ترافیک کل"
|
||||||
|
"downloadSpeedLimit" = "محدودیت سرعت دانلود (کیلوبایت/ث)"
|
||||||
|
"uploadSpeedLimit" = "محدودیت سرعت آپلود (کیلوبایت/ث)"
|
||||||
|
"speedLimitDesc" = "0 = نامحدود. (واحد: کیلوبایت/ث)"
|
||||||
"leaveBlankToNeverExpire" = "برای منقضینشدن خالیبگذارید"
|
"leaveBlankToNeverExpire" = "برای منقضینشدن خالیبگذارید"
|
||||||
"noRecommendKeepDefault" = "توصیهمیشود بهطور پیشفرض حفظشود"
|
"noRecommendKeepDefault" = "توصیهمیشود بهطور پیشفرض حفظشود"
|
||||||
"certificatePath" = "مسیر فایل"
|
"certificatePath" = "مسیر فایل"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP"
|
"monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP"
|
||||||
"meansNoLimit" = "= Unlimited. (unit: GB)"
|
"meansNoLimit" = "= Unlimited. (unit: GB)"
|
||||||
"totalFlow" = "Total Aliran"
|
"totalFlow" = "Total Aliran"
|
||||||
|
"downloadSpeedLimit" = "Batas Kecepatan Unduh (KB/s)"
|
||||||
|
"uploadSpeedLimit" = "Batas Kecepatan Unggah (KB/s)"
|
||||||
|
"speedLimitDesc" = "0 = Tidak Terbatas. (satuan: KB/s)"
|
||||||
"leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa"
|
"leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa"
|
||||||
"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default"
|
"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default"
|
||||||
"certificatePath" = "Path Berkas"
|
"certificatePath" = "Path Berkas"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "空白にするとすべてのIPを監視"
|
"monitorDesc" = "空白にするとすべてのIPを監視"
|
||||||
"meansNoLimit" = "= 無制限(単位:GB)"
|
"meansNoLimit" = "= 無制限(単位:GB)"
|
||||||
"totalFlow" = "総トラフィック"
|
"totalFlow" = "総トラフィック"
|
||||||
|
"downloadSpeedLimit" = "ダウンロード速度制限 (KB/s)"
|
||||||
|
"uploadSpeedLimit" = "アップロード速度制限 (KB/s)"
|
||||||
|
"speedLimitDesc" = "0 = 無制限(単位:KB/s)"
|
||||||
"leaveBlankToNeverExpire" = "空白にすると期限なし"
|
"leaveBlankToNeverExpire" = "空白にすると期限なし"
|
||||||
"noRecommendKeepDefault" = "デフォルト値を保持することをお勧めします"
|
"noRecommendKeepDefault" = "デフォルト値を保持することをお勧めします"
|
||||||
"certificatePath" = "ファイルパス"
|
"certificatePath" = "ファイルパス"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "Deixe em branco para ouvir todos os IPs"
|
"monitorDesc" = "Deixe em branco para ouvir todos os IPs"
|
||||||
"meansNoLimit" = "= Ilimitado. (unidade: GB)"
|
"meansNoLimit" = "= Ilimitado. (unidade: GB)"
|
||||||
"totalFlow" = "Fluxo Total"
|
"totalFlow" = "Fluxo Total"
|
||||||
|
"downloadSpeedLimit" = "Limite de Velocidade de Download (KB/s)"
|
||||||
|
"uploadSpeedLimit" = "Limite de Velocidade de Upload (KB/s)"
|
||||||
|
"speedLimitDesc" = "0 = Ilimitado. (unidade: KB/s)"
|
||||||
"leaveBlankToNeverExpire" = "Deixe em branco para nunca expirar"
|
"leaveBlankToNeverExpire" = "Deixe em branco para nunca expirar"
|
||||||
"noRecommendKeepDefault" = "Recomenda-se manter o padrão"
|
"noRecommendKeepDefault" = "Recomenda-se manter o padrão"
|
||||||
"certificatePath" = "Caminho"
|
"certificatePath" = "Caminho"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "Оставьте пустым для прослушивания всех IP-адресов"
|
"monitorDesc" = "Оставьте пустым для прослушивания всех IP-адресов"
|
||||||
"meansNoLimit" = "= Без ограничений (значение: ГБ)"
|
"meansNoLimit" = "= Без ограничений (значение: ГБ)"
|
||||||
"totalFlow" = "Общий расход"
|
"totalFlow" = "Общий расход"
|
||||||
|
"downloadSpeedLimit" = "Ограничение скорости загрузки (КБ/с)"
|
||||||
|
"uploadSpeedLimit" = "Ограничение скорости выгрузки (КБ/с)"
|
||||||
|
"speedLimitDesc" = "0 = Без ограничений. (единица: КБ/с)"
|
||||||
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы было бесконечным"
|
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы было бесконечным"
|
||||||
"noRecommendKeepDefault" = "Рекомендуется оставить настройки по умолчанию"
|
"noRecommendKeepDefault" = "Рекомендуется оставить настройки по умолчанию"
|
||||||
"certificatePath" = "Путь к сертификату"
|
"certificatePath" = "Путь к сертификату"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "Tüm IP'leri dinlemek için boş bırakın"
|
"monitorDesc" = "Tüm IP'leri dinlemek için boş bırakın"
|
||||||
"meansNoLimit" = "= Sınırsız. (birim: GB)"
|
"meansNoLimit" = "= Sınırsız. (birim: GB)"
|
||||||
"totalFlow" = "Toplam Akış"
|
"totalFlow" = "Toplam Akış"
|
||||||
|
"downloadSpeedLimit" = "İndirme Hız Limiti (KB/s)"
|
||||||
|
"uploadSpeedLimit" = "Yükleme Hız Limiti (KB/s)"
|
||||||
|
"speedLimitDesc" = "0 = Sınırsız. (birim: KB/s)"
|
||||||
"leaveBlankToNeverExpire" = "Hiçbir zaman sona ermemesi için boş bırakın"
|
"leaveBlankToNeverExpire" = "Hiçbir zaman sona ermemesi için boş bırakın"
|
||||||
"noRecommendKeepDefault" = "Varsayılanı korumanız önerilir"
|
"noRecommendKeepDefault" = "Varsayılanı korumanız önerilir"
|
||||||
"certificatePath" = "Dosya Yolu"
|
"certificatePath" = "Dosya Yolu"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "Залиште порожнім, щоб слухати всі IP-адреси"
|
"monitorDesc" = "Залиште порожнім, щоб слухати всі IP-адреси"
|
||||||
"meansNoLimit" = "= Необмежено. (одиниця: ГБ)"
|
"meansNoLimit" = "= Необмежено. (одиниця: ГБ)"
|
||||||
"totalFlow" = "Загальна витрата"
|
"totalFlow" = "Загальна витрата"
|
||||||
|
"downloadSpeedLimit" = "Ліміт швидкості завантаження (КБ/с)"
|
||||||
|
"uploadSpeedLimit" = "Ліміт швидкості вивантаження (КБ/с)"
|
||||||
|
"speedLimitDesc" = "0 = Без обмежень. (одиниця: КБ/с)"
|
||||||
"leaveBlankToNeverExpire" = "Залиште порожнім, щоб ніколи не закінчувався"
|
"leaveBlankToNeverExpire" = "Залиште порожнім, щоб ніколи не закінчувався"
|
||||||
"noRecommendKeepDefault" = "Рекомендується зберегти значення за замовчуванням"
|
"noRecommendKeepDefault" = "Рекомендується зберегти значення за замовчуванням"
|
||||||
"certificatePath" = "Шлях до файлу"
|
"certificatePath" = "Шлях до файлу"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "Mặc định để trống"
|
"monitorDesc" = "Mặc định để trống"
|
||||||
"meansNoLimit" = "= Không giới hạn (đơn vị: GB)"
|
"meansNoLimit" = "= Không giới hạn (đơn vị: GB)"
|
||||||
"totalFlow" = "Tổng lưu lượng"
|
"totalFlow" = "Tổng lưu lượng"
|
||||||
|
"downloadSpeedLimit" = "Giới hạn tốc độ tải xuống (KB/s)"
|
||||||
|
"uploadSpeedLimit" = "Giới hạn tốc độ tải lên (KB/s)"
|
||||||
|
"speedLimitDesc" = "0 = Không giới hạn. (đơn vị: KB/s)"
|
||||||
"leaveBlankToNeverExpire" = "Để trống để không bao giờ hết hạn"
|
"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"
|
"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"
|
"certificatePath" = "Đường dẫn tập"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "留空表示监听所有 IP"
|
"monitorDesc" = "留空表示监听所有 IP"
|
||||||
"meansNoLimit" = "= 无限制(单位:GB)"
|
"meansNoLimit" = "= 无限制(单位:GB)"
|
||||||
"totalFlow" = "总流量"
|
"totalFlow" = "总流量"
|
||||||
|
"downloadSpeedLimit" = "下载速度限制 (KB/s)"
|
||||||
|
"uploadSpeedLimit" = "上传速度限制 (KB/s)"
|
||||||
|
"speedLimitDesc" = "0 = 无限制(单位:KB/s)"
|
||||||
"leaveBlankToNeverExpire" = "留空表示永不过期"
|
"leaveBlankToNeverExpire" = "留空表示永不过期"
|
||||||
"noRecommendKeepDefault" = "建议保留默认值"
|
"noRecommendKeepDefault" = "建议保留默认值"
|
||||||
"certificatePath" = "文件路径"
|
"certificatePath" = "文件路径"
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,9 @@
|
||||||
"monitorDesc" = "留空表示監聽所有 IP"
|
"monitorDesc" = "留空表示監聽所有 IP"
|
||||||
"meansNoLimit" = "= 無限制(單位:GB)"
|
"meansNoLimit" = "= 無限制(單位:GB)"
|
||||||
"totalFlow" = "總流量"
|
"totalFlow" = "總流量"
|
||||||
|
"downloadSpeedLimit" = "下載速度限制 (KB/s)"
|
||||||
|
"uploadSpeedLimit" = "上傳速度限制 (KB/s)"
|
||||||
|
"speedLimitDesc" = "0 = 無限制(單位:KB/s)"
|
||||||
"leaveBlankToNeverExpire" = "留空表示永不過期"
|
"leaveBlankToNeverExpire" = "留空表示永不過期"
|
||||||
"noRecommendKeepDefault" = "建議保留預設值"
|
"noRecommendKeepDefault" = "建議保留預設值"
|
||||||
"certificatePath" = "檔案路徑"
|
"certificatePath" = "檔案路徑"
|
||||||
|
|
|
||||||
19
web/web.go
19
web/web.go
|
|
@ -101,9 +101,10 @@ type Server struct {
|
||||||
api *controller.APIController
|
api *controller.APIController
|
||||||
ws *controller.WebSocketController
|
ws *controller.WebSocketController
|
||||||
|
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
tgbotService service.Tgbot
|
tgbotService service.Tgbot
|
||||||
|
speedLimitService service.SpeedLimitService
|
||||||
|
|
||||||
wsHub *websocket.Hub
|
wsHub *websocket.Hub
|
||||||
|
|
||||||
|
|
@ -299,6 +300,18 @@ func (s *Server) startTask() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("start xray failed:", err)
|
logger.Warning("start xray failed:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore per-inbound tc speed-limit rules (non-persistent across reboots).
|
||||||
|
go func() {
|
||||||
|
inboundService := service.InboundService{}
|
||||||
|
inbounds, err := inboundService.GetAllInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("speed limit restore: failed to get inbounds:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.speedLimitService.RestoreAllLimits(inbounds)
|
||||||
|
}()
|
||||||
|
|
||||||
// Check whether xray is running every second
|
// Check whether xray is running every second
|
||||||
s.cron.AddJob("@every 1s", job.NewCheckXrayRunningJob())
|
s.cron.AddJob("@every 1s", job.NewCheckXrayRunningJob())
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue