Feature/tuic v5 (#6337)

* Feat(tuic): Implement native TUIC v5 protocol support via Rust sidecar daemon

- Add internal/tuic package for official tuic-server sidecar lifecycle management, configuration generation, and graceful process control
- Bridge decrypted TUIC QUIC traffic into loopback Xray SOCKS5 inbounds (63200+id) for traffic accounting, statistics, and routing rules
- Implement periodic reconciliation job (cadence @every 10s) and immediate runtime synchronization on inbound/client mutations
- Add TUIC inbound & multi-user client settings (UUID + Password authentication) in Web UI with SNI auto-fill and panel certificate loader
- Integrate tuic:// subscription links and Clash.Meta (Mihomo) proxy generation for TUIC
- Update install.sh to automatically download and install official tuic-server release for x86_64, aarch64, and armv7
- Add full localization for TUIC protocol across all 13 supported languages

* Feat(install): Support custom repository and branch in install and update scripts

* Ci(release): Enable publish-dev for feature branch and workflow dispatch

* Feat(sub): Add TUIC to subscription resolution and client QR config generator

- Add 'tuic' to getInboundsBySubId SQL allowlist to resolve TUIC inbounds in subscriptions and sub links
- Enhance buildTuicProxy in Clash subscription generator with robust host and credentials resolution
- Add tuicConfig.ts to generate standalone Clash/Mihomo YAML configuration
- Add dedicated TUIC Config tab in ClientQrModal with QR code and .yaml download button
- Add localization keys for TUIC config across all 13 supported languages

* Fix(tuic): Exclude TUIC from native Xray inbounds and strip udp_relay_mode from server config

- Exclude model.TUIC from native Xray inbounds in GetXrayConfig to prevent Xray startup failure
- Remove udp_relay_mode from tuic-server JSON configuration builder
- Update install.sh to install tuic-server binary to both xui_folder/bin and /usr/local/bin

* Fix(install): Fallback to dev-latest when releases/latest is not present on fork

* Feat(tuic): Add real-time online status and LastOnline tracking for TUIC clients

- Track client activity by mapping client UUID in tuic-server logs to email
- Integrate TUIC active clients into XrayTrafficJob to refresh local online clients
- Bump LastOnline timestamp in database and broadcast live online status over WebSocket

* Feat(tuic): Implement real-time traffic statistics and live speed reporting for TUIC

- Collect precise I/O traffic deltas for tuic-server child processes via /proc/<pid>/io
- Aggregate and attribute TUIC traffic deltas per client in tuic Manager
- Integrate TUIC traffic deltas into XrayTrafficJob to update database and broadcast live speed

* Feat(tuic): Finalize TUIC v5 integration with 1:1 traffic counting and orphan process cleanup
- Use exact 1:1 byte delta accounting from /proc/<pid>/io
- Add killStrayTuicProcesses to terminate orphan sidecars on panel startup
- Fully integrate TUIC with subscriptions, live speed meter, and all 13 locales

* Feat(frontend): Polish TUIC UI, support bulk operations, and update translations

- Align TUIC inbound certificate form with standard 3X-UI layout (Set Default Cert, Clear)
- Remove extra subtitle hint text from TUIC inbound form fields
- Support TUIC in client bulk attach/detach and bulk add modals
- Add TUIC badge color to client info modal, clients table, and host list
- Update password tooltip across all 13 locales to include TUIC
- Remove obsolete dead translation keys across all 13 locales

* Chore(ci): Finalize TUIC v5 bundling across release workflow, Docker, and scripts

* Feat(openapi): Update OpenAPI generator and schemas for TUIC types

* Fix(backend): Address core review findings for TUIC types, port checks, and xray bridge

* Refactor(traffic): Isolate proc reading with build tags and decouple TUIC metering into TuicJob

* Feat(client): Add TuicServer to InboundOption, fix config export and clean share links

* Fix(frontend): Register TUIC in multi-user helpers, tracked protocols, and tag derivation

* Chore(openapi): Re-generate OpenAPI specification and sync Zod schemas

* Chore(scripts): Add Alpine musl binaries, 386 and Windows packaging, and anchor pkill

* Fix(review): Remove stale import, correct binary names, switch to musl, and drop unreachable relay gate

* Feat(frontend): Show share link in Inbound Info and display UDP tag for TUIC

* Docs: Add TUIC v5 configuration guide and link specifications

* Docs(tuic): Correct Clash Meta configuration parameter to reduce-rtt

* Fix(tuic): Generate client credentials on copy, enforce ID/password validation, and add i386 to DockerInit

* Fix(tuic): drop unused relay, fix traffic accounting, and honor host endpoints

- Drop unused loopback SOCKS relay and eliminate port collision with AmneziaWG
- Correct inbound traffic calculation without double-counting
- Drop heuristic client traffic division while retaining online tracking
- Support externalProxy host fan-out and conditional parameters in share links
- Scope orphan process termination to managed config directory

* Fix(tuic): enforce client quotas, decouple Xray restart, and sync openapi schemas

- Regenerate OpenAPI, Zod schemas, and TypeScript types without route_through_xray
- Populate clientTraffics in TuicJob to enforce client quotas and first-use expiry
- Split process I/O delta into up and down in Process.CollectTraffic
- Remove SetNeedRestart from updateTuicInbound to prevent Xray session drops
- Use InstanceFromInbound for default ALPN and UDP relay mode in tuic:// share links
- Support allow_insecure on externalProxy host endpoints without parameter collision

* Fix(tuic): attribute client traffic only on single-user inbounds and sync link defaults

- Attribute I/O deltas to the client only when the inbound has exactly one configured client, avoiding false billing and disablings on multi-user inbounds
- Aggregate client traffic by email in TuicJob so clients on multiple inbounds don't lose deltas
- Match frontend genTuicLink defaults for alpn and udp_relay_mode with backend subscription links

* Fix(tuic): gate client traffic by total sidecar clients and require client email

* Fix(tuic): enforce inbound-only traffic limits and disable client totalGB

* fix(tuic): restore delayed start, remove client totalGB rejection, and document linux-only limits

* fix(tuic): anchor pkill, fix io baseline/split, escape yaml, and deduplicate start errors

* fix(tuic): prevent traffic double-counting, ensure info log level for delayed start, and broaden pkill matching

* fix(tuic): address review round 11 findings

- internal/sub/json_service: skip tuic protocol in json subscription to prevent direct routing leak
- internal/sub/clash_service: honor externalProxy/host row allowInsecure, sni, and alpn in buildTuicProxy
- internal/web/runtime: decouple tuic inbound add/delete from xray restart
- internal/tuic/config: restore user log-level options (warn, error) without forced info clamp
- frontend/src/lib/xray/inbound-link: fix duplicate remark suffix and apply externalProxy TLS overrides
- frontend/src/schemas/protocols/stream/external-proxy: propagate allowInsecure through host mapping
- tests: add coverage for json sub skip, clash proxy overrides, and link generation

* fix(tuic): meter inbound traffic through a UDP relay and bracket IPv6 binds

Review repairs on the TUIC v5 sidecar integration:

- Inbound traffic was read from the sidecar's /proc/<pid>/io rchar, but
  the kernel only counts read()/write() there and tuic-server moves its
  sockets with recvfrom/recvmmsg/sendmmsg/sendto, so an inbound's up/down
  stayed at 0 forever and inbound total limits never tripped (measured:
  12 MiB relayed, rchar delta 0). The panel now owns the inbound's public
  UDP port with a small relay and runs tuic-server behind it on a loopback
  port, counting up/down exactly on every OS. tuic-server therefore logs
  127.0.0.1 as every client's address; per-client attribution stays
  unsupported since QUIC is opaque.
- Instance.BindTo formatted an IPv6 listen address as ":::8443", which
  tuic-server rejects with "invalid socket address syntax", so an inbound
  listening on "::" or any IPv6 literal never started. It now uses
  net.JoinHostPort; IPv4 output is unchanged.
- The log level is passed to the sidecar as chosen. Online status,
  last-online and delayed start are read from its Info lines, so the Log
  Level field now says that Warn and Error switch them off for the
  inbound, and the docs say the same.
- Drop two frontend tests that only exercised a getter and a set lookup,
  and strip the trailing blank line that made gofumpt fail on two of the
  new Go test files.

* fix(tuic): harden tag updates, runtime routing, and relay stability

---------

Co-authored-by: poise52 <equipoise52@gmail.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
amae
2026-09-12 13:15:48 +05:00
committed by GitHub
parent 0a2cd789ba
commit 6d96accd63
100 changed files with 4139 additions and 52 deletions
+84
View File
@@ -0,0 +1,84 @@
package job
import (
"time"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/tuic"
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
type TuicJob struct {
inboundService service.InboundService
}
func NewTuicJob() *TuicJob {
return new(TuicJob)
}
func (j *TuicJob) Run() {
desired, err := j.inboundService.DesiredTuicInstances()
if err != nil {
logger.Warning("tuic job: get desired instances failed:", err)
return
}
activeTags := make([]string, 0, len(desired))
for _, inst := range desired {
activeTags = append(activeTags, inst.Tag)
}
mgr := tuic.GetManager()
mgr.Reconcile(desired)
deltas := mgr.CollectTraffic()
onlineEmails, _ := mgr.GetActiveClients(30 * time.Second)
inboundUp := make(map[string]int64)
inboundDown := make(map[string]int64)
for _, d := range deltas {
inboundUp[d.Tag] += d.Up
inboundDown[d.Tag] += d.Down
}
traffics := make([]*xray.Traffic, 0, len(inboundUp))
for tag, up := range inboundUp {
traffics = append(traffics, &xray.Traffic{
IsInbound: true,
Tag: tag,
Up: up,
Down: inboundDown[tag],
})
}
// Build zero-byte client traffic entries for active clients so adjustTraffics can
// activate delayed-start expiryTime for TUIC clients without inflating traffic.
clientTraffics := make([]*xray.ClientTraffic, 0, len(onlineEmails))
for _, email := range onlineEmails {
clientTraffics = append(clientTraffics, &xray.ClientTraffic{
Email: email,
Up: 0,
Down: 0,
})
}
if len(traffics) > 0 || len(clientTraffics) > 0 {
needRestart, _, err := j.inboundService.AddTraffic(traffics, clientTraffics)
if err != nil {
logger.Warning("tuic job: add traffic failed:", err)
} else if needRestart {
if desired, err := j.inboundService.DesiredTuicInstances(); err == nil {
mgr.Reconcile(desired)
}
}
}
if len(onlineEmails) > 0 {
if err := j.inboundService.BumpClientsLastOnline(onlineEmails); err != nil {
logger.Warning("tuic job: bump last online for tuic clients failed:", err)
}
}
j.inboundService.RefreshLocalOnlineClients(onlineEmails, activeTags)
}
+40 -2
View File
@@ -12,6 +12,7 @@ import (
"github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/mtproto"
"github.com/mhsanaei/3x-ui/v3/internal/tuic"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
@@ -84,6 +85,13 @@ func (l *Local) AddInbound(_ context.Context, ib *model.Inbound) error {
}
return err
}
if ib.Protocol == model.TUIC {
inst, ok := tuic.InstanceFromInbound(ib)
if !ok {
return nil
}
return tuic.GetManager().Ensure(inst)
}
body, err := json.MarshalIndent(ib.GenXrayInboundConfig(), "", " ")
if err != nil {
return err
@@ -108,6 +116,10 @@ func (l *Local) DelInbound(_ context.Context, ib *model.Inbound) error {
}
return nil
}
if ib.Protocol == model.TUIC {
tuic.GetManager().Remove(ib.Id)
return nil
}
return l.withAPI(func(api *xray.XrayAPI) error {
return api.DelInbound(ib.Tag)
})
@@ -120,6 +132,9 @@ func (l *Local) UpdateInbound(ctx context.Context, oldIb, newIb *model.Inbound)
if oldIb.Protocol == model.AmneziaWG || newIb.Protocol == model.AmneziaWG {
return l.updateAmneziaWGInbound(ctx, oldIb, newIb)
}
if oldIb.Protocol == model.TUIC || newIb.Protocol == model.TUIC {
return l.updateTuicInbound(ctx, oldIb, newIb)
}
_ = l.DelInbound(ctx, oldIb)
if !newIb.Enable {
return nil
@@ -210,8 +225,31 @@ func (l *Local) updateAmneziaWGInbound(ctx context.Context, oldIb, newIb *model.
})
}
func (l *Local) updateTuicInbound(ctx context.Context, oldIb, newIb *model.Inbound) error {
if oldIb.Protocol == model.TUIC && newIb.Protocol != model.TUIC {
tuic.GetManager().Remove(oldIb.Id)
if !newIb.Enable {
return nil
}
return l.AddInbound(ctx, newIb)
}
if oldIb.Protocol != model.TUIC {
_ = l.DelInbound(ctx, oldIb)
}
if !newIb.Enable {
tuic.GetManager().Remove(newIb.Id)
return nil
}
inst, ok := tuic.InstanceFromInbound(newIb)
if !ok {
tuic.GetManager().Remove(newIb.Id)
return nil
}
return tuic.GetManager().Ensure(inst)
}
func (l *Local) AddUser(_ context.Context, ib *model.Inbound, userMap map[string]any) error {
if ib.Protocol == model.MTProto || ib.Protocol == model.AmneziaWG {
if ib.Protocol == model.MTProto || ib.Protocol == model.AmneziaWG || ib.Protocol == model.TUIC {
return nil
}
return l.withAPI(func(api *xray.XrayAPI) error {
@@ -220,7 +258,7 @@ func (l *Local) AddUser(_ context.Context, ib *model.Inbound, userMap map[string
}
func (l *Local) RemoveUser(_ context.Context, ib *model.Inbound, email string) error {
if ib.Protocol == model.MTProto || ib.Protocol == model.AmneziaWG {
if ib.Protocol == model.MTProto || ib.Protocol == model.AmneziaWG || ib.Protocol == model.TUIC {
return nil
}
return l.withAPI(func(api *xray.XrayAPI) error {
+7
View File
@@ -408,6 +408,13 @@ func (s *ClientService) fillProtocolDefaults(c *model.Client, ib *model.Inbound)
if c.Secret == "" {
c.Secret = model.GenerateFakeTLSSecret(mtprotoDomainFromSettings(ib.Settings))
}
case model.TUIC:
if c.ID == "" {
c.ID = uuid.NewString()
}
if c.Password == "" {
c.Password = strings.ReplaceAll(uuid.NewString(), "-", "")
}
}
return nil
}
@@ -451,6 +451,16 @@ func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model
if client.AdTag != "" && !model.ValidMtprotoAdTag(client.AdTag) {
return false, common.NewError("mtproto client ad tag must be 32 hex characters")
}
case "tuic":
if client.ID == "" {
return false, common.NewError("empty client ID")
}
if client.Password == "" {
return false, common.NewError("tuic client requires a password")
}
if client.Email == "" {
return false, common.NewError("empty client email")
}
default:
if client.ID == "" {
return false, common.NewError("empty client ID")
@@ -563,6 +573,8 @@ func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model
inboundSvc.applyLocalMtproto(oldInbound.Id)
} else if oldInbound.Protocol == model.AmneziaWG {
inboundSvc.applyLocalAmneziaWG(oldInbound.Id)
} else if oldInbound.Protocol == model.TUIC {
inboundSvc.applyLocalTuic(oldInbound.Id)
} else {
for _, client := range clients {
if len(client.Email) == 0 {
@@ -986,6 +998,8 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
inboundSvc.applyLocalMtproto(oldInbound.Id)
} else if oldInbound.Protocol == model.AmneziaWG {
inboundSvc.applyLocalAmneziaWG(oldInbound.Id)
} else if oldInbound.Protocol == model.TUIC {
inboundSvc.applyLocalTuic(oldInbound.Id)
} else {
if oldClients[clientIndex].Enable {
err1 := rt.RemoveUser(context.Background(), oldInbound, oldEmail)
@@ -1170,6 +1184,8 @@ func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inbo
// Same reasoning as MTProto above: the interface config is
// regenerated from the full peer set, so any delete re-applies it.
inboundSvc.applyLocalAmneziaWG(oldInbound.Id)
} else if oldInbound.Protocol == model.TUIC {
inboundSvc.applyLocalTuic(oldInbound.Id)
} else if needApiDel {
// Local inbound: a disabled client isn't in the running Xray, so only
// a live one (needApiDel) needs an API removal.
+50 -6
View File
@@ -22,6 +22,7 @@ import (
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/mtproto"
"github.com/mhsanaei/3x-ui/v3/internal/tuic"
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
"github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
@@ -345,7 +346,8 @@ type InboundOption struct {
// AwgServer carries the full AmneziaWG server block (keys, subnet,
// obfuscation params) so the clients page can render a downloadable
// per-client .conf without a second round trip.
AwgServer *amneziawg.ServerSettings `json:"awgServer,omitempty"`
AwgServer *amneziawg.ServerSettings `json:"awgServer,omitempty"`
TuicServer *tuic.TuicServerSettings `json:"tuicServer,omitempty"`
// Hosting node; nil for this panel's own inbounds. Lets the clients
// page map a node filter onto inbound IDs (#4997).
NodeId *int `json:"nodeId,omitempty"`
@@ -412,6 +414,7 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
WgDns: wgDns,
MtprotoDomain: inboundMtprotoDomain(r.Protocol, r.Settings),
AwgServer: inboundAmneziaWGServer(r.Protocol, r.Settings),
TuicServer: inboundTuicServer(r.Protocol, r.Settings),
NodeId: r.NodeId,
NodeAddress: r.NodeAddress,
Listen: r.Listen,
@@ -506,6 +509,21 @@ func inboundAmneziaWGServer(protocol string, settings string) *amneziawg.ServerS
return &redacted
}
func inboundTuicServer(protocol string, settings string) *tuic.TuicServerSettings {
if protocol != string(model.TUIC) || strings.TrimSpace(settings) == "" {
return nil
}
var parsed struct {
Server *tuic.TuicServerSettings `json:"server"`
}
if err := json.Unmarshal([]byte(settings), &parsed); err != nil || parsed.Server == nil {
return nil
}
redacted := *parsed.Server
redacted.PrivateKey = ""
return &redacted
}
// inboundMtprotoDomain returns the inbound-level FakeTLS default domain, used by
// the clients UI to seed a new mtproto client's secret with the right fronting
// hostname.
@@ -1181,6 +1199,16 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
if client.AdTag != "" && !model.ValidMtprotoAdTag(client.AdTag) {
return inbound, false, common.NewError("mtproto client ad tag must be 32 hex characters")
}
case "tuic":
if client.ID == "" {
return inbound, false, common.NewError("empty client ID")
}
if client.Password == "" {
return inbound, false, common.NewError("tuic client requires a password")
}
if client.Email == "" {
return inbound, false, common.NewError("empty client email")
}
default:
if client.ID == "" {
return inbound, false, common.NewError("empty client ID")
@@ -1271,7 +1299,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
if push {
payload := inbound
pushable := true
if inbound.Protocol == model.MTProto {
if inbound.Protocol == model.MTProto || inbound.Protocol == model.TUIC {
if built, bErr := s.buildInboundForLocalRuntime(tx, inbound); bErr == nil {
payload = built
} else {
@@ -1285,7 +1313,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
logger.Debug("New inbound added on", rt.Name(), ":", inbound.Tag)
} else {
logger.Debug("Unable to add inbound on", rt.Name(), ":", err1)
needRestart = true
if inbound.Protocol != model.MTProto && inbound.Protocol != model.TUIC {
needRestart = true
}
}
}
}
@@ -1629,6 +1659,20 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
}
}
}
if inbound.Protocol == model.TUIC {
for _, client := range clients {
if client.ID == "" {
return inbound, false, common.NewError("empty client ID")
}
if client.Password == "" {
return inbound, false, common.NewError("tuic client requires a password")
}
if client.Email == "" {
return inbound, false, common.NewError("empty client email")
}
}
}
// Grandfather a row that was already stored incomplete so it stays editable;
// only a save that breaks a previously valid TLS block is refused.
if !s.FromNodeSync {
@@ -1801,7 +1845,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
}
if !push {
needRestart = true
} else if oldProtocol == model.MTProto || oldInbound.Protocol == model.MTProto {
} else if oldProtocol == model.MTProto || oldInbound.Protocol == model.MTProto || oldProtocol == model.TUIC || oldInbound.Protocol == model.TUIC {
oldSnapshot := *oldInbound
oldSnapshot.Tag = tag
oldSnapshot.Protocol = oldProtocol
@@ -1815,14 +1859,14 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
pushable = false
}
}
newProtocolIsMtproto := oldInbound.Protocol == model.MTProto
newProtocolIsSidecar := oldInbound.Protocol == model.MTProto || oldInbound.Protocol == model.TUIC
if pushable {
postCommitApply = func() {
if err2 := rt.UpdateInbound(context.Background(), &oldSnapshot, payload); err2 == nil {
logger.Debug("Updated inbound applied on", rt.Name(), ":", oldInbound.Tag)
} else {
logger.Debug("Unable to update inbound on", rt.Name(), ":", err2)
if !newProtocolIsMtproto {
if !newProtocolIsSidecar {
needRestart = true
}
}
+3
View File
@@ -233,6 +233,9 @@ func (s *InboundService) buildTargetClientFromSource(source model.Client, target
target.Auth = s.generateRandomCredential(targetProtocol)
case model.MTProto:
target.Secret = model.GenerateFakeTLSSecret(mtprotoDomainFromSettings(targetInbound.Settings))
case model.TUIC:
target.ID = uuid.NewString()
target.Password = s.generateRandomCredential(targetProtocol)
default:
target.ID = s.generateRandomCredential(targetProtocol)
}
@@ -0,0 +1,66 @@
package service
import (
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
func TestBuildTargetClientFromSourceTuic(t *testing.T) {
s := &InboundService{}
source := model.Client{
Email: "test@example.com",
ID: "old-uuid",
Password: "old-password",
}
targetInbound := &model.Inbound{
Protocol: model.TUIC,
}
target, err := s.buildTargetClientFromSource(source, targetInbound, "test@example.com", "")
if err != nil {
t.Fatalf("buildTargetClientFromSource failed: %v", err)
}
if target.ID == "" || target.ID == "old-uuid" {
t.Fatalf("expected new UUID for TUIC client, got %q", target.ID)
}
if target.Password == "" || target.Password == "old-password" {
t.Fatalf("expected new password for TUIC client, got %q", target.Password)
}
}
func TestAddInboundTuicClientValidation(t *testing.T) {
setupConflictDB(t)
s := &InboundService{}
ib := &model.Inbound{
Tag: "tuic-test-1",
Protocol: model.TUIC,
Settings: `{"clients":[{"id":"uuid-1","password":""}]}`,
}
_, _, err := s.AddInbound(ib)
if err == nil || strings.TrimSpace(err.Error()) != "tuic client requires a password" {
t.Fatalf("expected 'tuic client requires a password' error, got %v", err)
}
ibNoID := &model.Inbound{
Tag: "tuic-test-2",
Protocol: model.TUIC,
Settings: `{"clients":[{"id":"","password":"pass"}]}`,
}
_, _, errNoID := s.AddInbound(ibNoID)
if errNoID == nil || strings.TrimSpace(errNoID.Error()) != "empty client ID" {
t.Fatalf("expected 'empty client ID' error, got %v", errNoID)
}
ibNoEmail := &model.Inbound{
Tag: "tuic-test-3",
Protocol: model.TUIC,
Settings: `{"clients":[{"id":"uuid-3","password":"pass","email":""}]}`,
}
_, _, errNoEmail := s.AddInbound(ibNoEmail)
if errNoEmail == nil || strings.TrimSpace(errNoEmail.Error()) != "empty client email" {
t.Fatalf("expected 'empty client email' error, got %v", errNoEmail)
}
}
@@ -83,6 +83,10 @@ func (s *InboundService) applyTrafficMutationBatch(b *trafficMutationBatch) bool
s.applyLocalAmneziaWG(plan.inbound.Id)
continue
}
if plan.inbound.Protocol == model.TUIC {
s.applyLocalTuic(plan.inbound.Id)
continue
}
rt, err := s.runtimeFor(&plan.inbound)
if err == nil {
switch plan.action {
+87
View File
@@ -0,0 +1,87 @@
package service
import (
"context"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/tuic"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
func (s *InboundService) DesiredTuicInstances() ([]tuic.Instance, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).
Where("protocol = ? AND enable = ? AND node_id IS NULL", model.TUIC, true).
Find(&inbounds).Error
if err != nil {
return nil, err
}
if len(inbounds) == 0 {
return nil, nil
}
ids := make([]int, 0, len(inbounds))
for _, ib := range inbounds {
ids = append(ids, ib.Id)
}
var disabledRows []xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).
Where("inbound_id IN ? AND enable = ?", ids, false).
Select("inbound_id", "email").
Find(&disabledRows).Error
if err != nil {
return nil, err
}
disabled := make(map[int]map[string]struct{}, len(disabledRows))
for _, row := range disabledRows {
if disabled[row.InboundId] == nil {
disabled[row.InboundId] = map[string]struct{}{}
}
disabled[row.InboundId][row.Email] = struct{}{}
}
instances := make([]tuic.Instance, 0, len(inbounds))
for _, ib := range inbounds {
inst, ok := tuic.InstanceFromInbound(ib)
if !ok {
continue
}
if off := disabled[ib.Id]; len(off) > 0 {
kept := make([]tuic.TuicClientSettings, 0, len(inst.Clients))
for _, c := range inst.Clients {
if _, skip := off[c.Email]; !skip {
kept = append(kept, c)
}
}
inst.Clients = kept
}
if len(inst.Clients) == 0 {
continue
}
instances = append(instances, inst)
}
return instances, nil
}
func (s *InboundService) applyLocalTuic(inboundId int) {
inbound, err := s.GetInbound(inboundId)
if err != nil || inbound == nil || inbound.Protocol != model.TUIC || inbound.NodeID != nil {
return
}
rt, err := s.runtimeFor(inbound)
if err != nil {
return
}
payload := inbound
if inbound.Enable {
if built, bErr := s.buildInboundForLocalRuntime(database.GetDB(), inbound); bErr == nil {
payload = built
}
}
if err := rt.UpdateInbound(context.Background(), inbound, payload); err != nil {
logger.Debug("tuic: immediate client apply failed for inbound", inboundId, ":", err)
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ const (
func inboundTransports(protocol model.Protocol, streamSettings, settings string) transportBits {
// protocols that ignore streamSettings entirely.
switch protocol {
case model.Hysteria, model.WireGuard, model.AmneziaWG:
case model.Hysteria, model.WireGuard, model.AmneziaWG, model.TUIC:
return transportUDP
case model.MTProto:
return transportTCP
+1 -1
View File
@@ -180,7 +180,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if inbound.NodeID != nil {
continue
}
if inbound.Protocol == model.MTProto || inbound.Protocol == model.AmneziaWG {
if inbound.Protocol == model.MTProto || inbound.Protocol == model.AmneziaWG || inbound.Protocol == model.TUIC {
continue
}
settings := map[string]any{}
+19 -2
View File
@@ -675,7 +675,8 @@
"bytesPerSec": "بايت في الثانية",
"bytesPerSecTip": "حد السرعة (بايت/ثانية) المطبّق على مرور الـ fallback بعد العتبة، عشان الفحوصات ماتقدرش تستخدم سيرفرك كنطاق ترددي مجاني للهدف. 0 = بلا حد (بيعطّل الاتجاه ده).",
"burstBytesPerSec": "بايت في الثانية للدفقة",
"burstBytesPerSecTip": "السماح بدفقات قصيرة فوق المعدل الثابت (حجم token-bucket). لو أقل من بايت في الثانية بيترفع ليطابقه."
"burstBytesPerSecTip": "السماح بدفقات قصيرة فوق المعدل الثابت (حجم token-bucket). لو أقل من بايت في الثانية بيترفع ليطابقه.",
"autoFill": "الملء التلقائي"
},
"info": {
"mode": "الوضع",
@@ -694,7 +695,8 @@
"peerNumber": "Peer {n}",
"peerNumberConfig": "تكوين Peer {n}"
},
"sniffingDestOverride": "تجاوز الوجهة"
"sniffingDestOverride": "تجاوز الوجهة",
"advancedTitle": "الإعدادات المتقدمة"
},
"clients": {
"tabBasics": "أساسي",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "إعدادات AmneziaWG",
"tuicConfig": "إعدادات TUIC (Clash)",
"mtprotoSecret": "سر MTProto",
"mtprotoSecretHint": "سر FakeTLS الخاص بالعميل. أعد التوليد لتغييره.",
"mtprotoAdTag": "علامة إعلانية (قناة مموّلة)",
@@ -915,6 +918,7 @@
"renewOnDay": "يوم التجديد",
"renewOnDayDesc": "يتم التجديد في هذا اليوم من كل شهر ميلادي، عند منتصف الليل بتوقيت اللوحة، بدلاً من كل N يوم. إذا كان الشهر أقصر من اليوم المختار، يتم التجديد في آخر يوم منه. القيمة 0 تُبقي وضع الفاصل اليومي.",
"renewsUsed": "التجديدات المستخدمة",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "أدخل علامة من 32 حرفًا ست عشريًا للتعيين، أو 'none' للمسح في منافذ MTProto."
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "التحكم في الازدحام",
"alpn": "ALPN",
"udpRelayMode": "وضع ترحيل UDP",
"zeroRttHandshake": "مصافحة 0-RTT",
"logLevel": "مستوى السجل",
"logLevelHint": "يتم استنتاج حالة الاتصال وآخر ظهور و«البدء بعد أول استخدام» من سطور Info الخاصة بالعملية الجانبية؛ اختيار Warn أو Error يعطّلها لهذا الإنباوند.",
"maxIdleTime": "أقصى وقت خمول (ثوانٍ)",
"authTimeout": "مهلة المصادقة (ثوانٍ)",
"maxUdpRelayPacketSize": "أقصى حجم لحزمة UDP",
"sni": "SNI",
"sniHint": "تجاوز إشارة اسم الخادم"
},
"tun": {
"userLevel": "مستوى المستخدم"
},
+18 -1
View File
@@ -410,6 +410,7 @@
"stream": "Stream",
"streamHelp": "Xray stream block wrapper:"
},
"advancedTitle": "Advanced Settings",
"subSortIndex": "Sub order",
"inboundInfo": "Inbound Information",
"exportInbound": "Export Inbound",
@@ -457,6 +458,7 @@
"moreIssues": "{message} (+{count} more)"
},
"form": {
"autoFill": "Auto Fill",
"moveUp": "Move up",
"moveDown": "Move down",
"addAll": "Add all",
@@ -768,7 +770,7 @@
"limitIpFail2banWindows": "Fail2ban is not available on Windows, so the IP limit cannot be enforced.",
"limitIpDisabled": "The IP limit feature is disabled on this server.",
"password": "Password",
"passwordDesc": "Only used by Trojan and Shadowsocks clients; ignored for VLESS, VMess, Hysteria, and WireGuard.",
"passwordDesc": "Used by Trojan, Shadowsocks, and TUIC clients; ignored for VLESS, VMess, Hysteria, and WireGuard.",
"subId": "Subscription ID",
"online": "Online",
"email": "Email",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "AmneziaWG config",
"tuicConfig": "TUIC config (Clash)",
"mtprotoSecret": "MTProto secret",
"mtprotoSecretHint": "The client's FakeTLS secret. Regenerate to rotate it.",
"mtprotoAdTag": "Ad-tag (sponsored channel)",
@@ -915,6 +918,7 @@
"renewOnDay": "Renew on day",
"renewOnDayDesc": "Renew on this day of every calendar month, at midnight in the panel's time zone, instead of every N days. A month too short for the chosen day renews on its last day. 0 keeps the day-interval mode.",
"renewsUsed": "Renewals used",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "Enter a 32-character hex tag to set, or 'none' to clear sponsor channel on MTProto inbounds."
},
"groups": {
@@ -2078,6 +2082,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "Congestion Control",
"alpn": "ALPN",
"udpRelayMode": "UDP Relay Mode",
"zeroRttHandshake": "Zero-RTT Handshake",
"logLevel": "Log Level",
"logLevelHint": "Online status, last online and \"start after first use\" are read from the sidecar's Info lines; Warn or Error turns them off for this inbound.",
"maxIdleTime": "Max Idle Time (s)",
"authTimeout": "Auth Timeout (s)",
"maxUdpRelayPacketSize": "Max UDP Packet Size",
"sni": "SNI",
"sniHint": "Server Name Indication override"
},
"tun": {
"userLevel": "User Level"
},
+19 -2
View File
@@ -675,7 +675,8 @@
"node": "Dirección del nodo",
"listen": "Dirección de escucha del inbound",
"custom": "Personalizada"
}
},
"autoFill": "Autocompletar"
},
"info": {
"mode": "Modo",
@@ -694,7 +695,8 @@
"peerNumber": "Peer {n}",
"peerNumberConfig": "Config Peer {n}"
},
"sniffingDestOverride": "Anulación de destino"
"sniffingDestOverride": "Anulación de destino",
"advancedTitle": "Configuración avanzada"
},
"clients": {
"tabBasics": "Básico",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "Configuración de AmneziaWG",
"tuicConfig": "Configuración de TUIC (Clash)",
"mtprotoSecret": "Secreto MTProto",
"mtprotoSecretHint": "El secreto FakeTLS del cliente. Vuelve a generarlo para cambiarlo.",
"mtprotoAdTag": "Ad-tag (canal patrocinado)",
@@ -915,6 +918,7 @@
"renewOnDay": "Renovar el día",
"renewOnDayDesc": "Renueva este día de cada mes natural, a medianoche en la zona horaria del panel, en lugar de cada N días. Si el mes es demasiado corto para el día elegido, renueva su último día. 0 mantiene el modo de intervalo en días.",
"renewsUsed": "Renovaciones usadas",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "Introduce una etiqueta hexadecimal de 32 caracteres para asignar, o 'none' para borrarla en MTProto."
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "Control de congestión",
"alpn": "ALPN",
"udpRelayMode": "Modo de retransmisión UDP",
"zeroRttHandshake": "Apretón de manos 0-RTT",
"logLevel": "Nivel de registro",
"logLevelHint": "El estado en línea, la última conexión y «iniciar tras el primer uso» se leen de las líneas Info del sidecar; Warn o Error los desactivan para este inbound.",
"maxIdleTime": "Tiempo máx. de inactividad (s)",
"authTimeout": "Tiempo de espera de autenticación (s)",
"maxUdpRelayPacketSize": "Tamaño máx. de paquete UDP",
"sni": "SNI",
"sniHint": "Sobrescritura de Server Name Indication"
},
"tun": {
"userLevel": "Nivel de Usuario"
},
+19 -2
View File
@@ -675,7 +675,8 @@
"node": "آدرس نود",
"listen": "آدرس شنود ورودی",
"custom": "سفارشی"
}
},
"autoFill": "تکمیل خودکار"
},
"info": {
"mode": "حالت",
@@ -694,7 +695,8 @@
"peerNumber": "Peer {n}",
"peerNumberConfig": "پیکربندی Peer {n}"
},
"sniffingDestOverride": "بازنویسی مقصد"
"sniffingDestOverride": "بازنویسی مقصد",
"advancedTitle": "تنظیمات پیشرفته"
},
"clients": {
"tabBasics": "پایه",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "پیکربندی AmneziaWG",
"tuicConfig": "پیکربندی TUIC (Clash)",
"mtprotoSecret": "سکرت MTProto",
"mtprotoSecretHint": "سکرت FakeTLS این کلاینت. برای تعویض، دوباره تولید کنید.",
"mtprotoAdTag": "برچسب تبلیغاتی (کانال حامی)",
@@ -915,6 +918,7 @@
"renewOnDay": "روز تمدید",
"renewOnDayDesc": "در این روز از هر ماه تقویمی، در نیمه‌شب به وقت پنل تمدید می‌شود، به جای هر N روز. اگر ماه کوتاه‌تر از روز انتخابی باشد، در آخرین روز آن ماه تمدید می‌شود. مقدار ۰ حالت بازهٔ روزانه را حفظ می‌کند.",
"renewsUsed": "تمدیدهای استفاده‌شده",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "یک برچسب هگز ۳۲ کاراکتری برای تنظیم وارد کنید، یا 'none' برای پاک کردن در MTProto."
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "کنترل ازدحام",
"alpn": "ALPN",
"udpRelayMode": "حالت رله UDP",
"zeroRttHandshake": "دست دادن 0-RTT",
"logLevel": "سطح گزارش",
"logLevelHint": "وضعیت آنلاین، آخرین اتصال و «شروع پس از اولین استفاده» از خطوط Info سایدکار خوانده می‌شوند؛ Warn یا Error آن‌ها را برای این اینباند غیرفعال می‌کند.",
"maxIdleTime": "حداکثر زمان بیکاری (ثانیه)",
"authTimeout": "مهلت زمانی احراز هویت (ثانیه)",
"maxUdpRelayPacketSize": "حداکثر اندازه بسته UDP",
"sni": "SNI",
"sniHint": "بازنویسی نشانگر نام سرور"
},
"tun": {
"userLevel": "سطح کاربر"
},
+19 -2
View File
@@ -675,7 +675,8 @@
"bytesPerSec": "Byte per detik",
"bytesPerSecTip": "Batas kecepatan (byte/detik) yang diterapkan pada lalu lintas fallback setelah ambang batas, agar probe tidak dapat memakai server Anda sebagai bandwidth gratis menuju target. 0 = tanpa batas (menonaktifkan arah ini).",
"burstBytesPerSec": "Byte per detik burst",
"burstBytesPerSecTip": "Kelonggaran untuk burst singkat di atas laju tetap (ukuran token-bucket). Jika lebih kecil dari Byte per detik, nilainya dinaikkan agar sama."
"burstBytesPerSecTip": "Kelonggaran untuk burst singkat di atas laju tetap (ukuran token-bucket). Jika lebih kecil dari Byte per detik, nilainya dinaikkan agar sama.",
"autoFill": "Isi Otomatis"
},
"info": {
"mode": "Mode",
@@ -694,7 +695,8 @@
"peerNumber": "Peer {n}",
"peerNumberConfig": "Konfig Peer {n}"
},
"sniffingDestOverride": "Penggantian tujuan"
"sniffingDestOverride": "Penggantian tujuan",
"advancedTitle": "Pengaturan Lanjutan"
},
"clients": {
"tabBasics": "Dasar",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "Konfigurasi AmneziaWG",
"tuicConfig": "Konfigurasi TUIC (Clash)",
"mtprotoSecret": "Secret MTProto",
"mtprotoSecretHint": "Secret FakeTLS klien. Buat ulang untuk menggantinya.",
"mtprotoAdTag": "Ad-tag (kanal bersponsor)",
@@ -915,6 +918,7 @@
"renewOnDay": "Perpanjang pada tanggal",
"renewOnDayDesc": "Perpanjang pada tanggal ini setiap bulan kalender, pada tengah malam menurut zona waktu panel, alih-alih setiap N hari. Bulan yang terlalu pendek untuk tanggal yang dipilih diperpanjang pada hari terakhirnya. 0 mempertahankan mode interval hari.",
"renewsUsed": "Perpanjangan terpakai",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "Masukkan tag heksadesimal 32 karakter untuk mengatur, atau 'none' untuk menghapus pada MTProto."
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "Kontrol Kemacetan",
"alpn": "ALPN",
"udpRelayMode": "Mode Relai UDP",
"zeroRttHandshake": "Jabat Tangan 0-RTT",
"logLevel": "Tingkat Log",
"logLevelHint": "Status online, terakhir online, dan \"mulai setelah penggunaan pertama\" dibaca dari baris Info sidecar; Warn atau Error menonaktifkannya untuk inbound ini.",
"maxIdleTime": "Waktu Diam Maks (dtk)",
"authTimeout": "Batas Waktu Otentikasi (dtk)",
"maxUdpRelayPacketSize": "Ukuran Paket UDP Maks",
"sni": "SNI",
"sniHint": "Penggantian Server Name Indication"
},
"tun": {
"userLevel": "Level Pengguna"
},
+19 -2
View File
@@ -675,7 +675,8 @@
"node": "ノードアドレス",
"listen": "インバウンドのリッスンアドレス",
"custom": "カスタム"
}
},
"autoFill": "自動入力"
},
"info": {
"mode": "モード",
@@ -694,7 +695,8 @@
"peerNumber": "Peer {n}",
"peerNumberConfig": "Peer {n} 設定"
},
"sniffingDestOverride": "宛先のオーバーライド"
"sniffingDestOverride": "宛先のオーバーライド",
"advancedTitle": "詳細設定"
},
"clients": {
"tabBasics": "基本",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "AmneziaWG 設定",
"tuicConfig": "TUIC 設定 (Clash)",
"mtprotoSecret": "MTProto シークレット",
"mtprotoSecretHint": "このクライアントの FakeTLS シークレット。変更するには再生成します。",
"mtprotoAdTag": "広告タグ(スポンサーチャンネル)",
@@ -915,6 +918,7 @@
"renewOnDay": "更新する日",
"renewOnDayDesc": "毎月この日の深夜(パネルのタイムゾーン基準)に更新します。N 日ごとの更新の代わりになります。その日が存在しない月は月末に更新されます。0 で日数間隔モードのままになります。",
"renewsUsed": "使用済み更新回数",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "設定するには 32 文字の 16 進数タグを入力し、クリアするには 'none' を入力します (MTProto 用)。"
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "輻輳制御",
"alpn": "ALPN",
"udpRelayMode": "UDP リレーモード",
"zeroRttHandshake": "0-RTT ハンドシェイク",
"logLevel": "ログレベル",
"logLevelHint": "オンライン状態、最終オンライン、「初回使用後に開始」はサイドカーの Info 行から検出されます。Warn または Error を選ぶとこのインバウンドでは無効になります。",
"maxIdleTime": "最大アイドル時間 (秒)",
"authTimeout": "認証タイムアウト (秒)",
"maxUdpRelayPacketSize": "最大 UDP パケットサイズ",
"sni": "SNI",
"sniHint": "Server Name Indication の上書き"
},
"tun": {
"userLevel": "ユーザーレベル"
},
+19 -2
View File
@@ -675,7 +675,8 @@
"node": "Endereço do nó",
"listen": "Endereço de escuta do inbound",
"custom": "Personalizada"
}
},
"autoFill": "Preenchimento automático"
},
"info": {
"mode": "Modo",
@@ -694,7 +695,8 @@
"peerNumber": "Peer {n}",
"peerNumberConfig": "Config Peer {n}"
},
"sniffingDestOverride": "Substituição de destino"
"sniffingDestOverride": "Substituição de destino",
"advancedTitle": "Configurações avançadas"
},
"clients": {
"tabBasics": "Básico",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "Configuração do AmneziaWG",
"tuicConfig": "Configuração do TUIC (Clash)",
"mtprotoSecret": "Segredo MTProto",
"mtprotoSecretHint": "O segredo FakeTLS do cliente. Gere novamente para trocá-lo.",
"mtprotoAdTag": "Ad-tag (canal patrocinado)",
@@ -915,6 +918,7 @@
"renewOnDay": "Renovar no dia",
"renewOnDayDesc": "Renova neste dia de cada mês do calendário, à meia-noite no fuso horário do painel, em vez de a cada N dias. Se o mês for curto demais para o dia escolhido, renova no último dia dele. 0 mantém o modo de intervalo em dias.",
"renewsUsed": "Renovações usadas",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "Insira uma tag hexadecimal de 32 caracteres para definir ou 'none' para limpar no MTProto."
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "Controle de congestionamento",
"alpn": "ALPN",
"udpRelayMode": "Modo de retransmissão UDP",
"zeroRttHandshake": "Handshake 0-RTT",
"logLevel": "Nível de registro",
"logLevelHint": "Status online, último acesso e \"iniciar após o primeiro uso\" são lidos das linhas Info do sidecar; Warn ou Error os desativam para este inbound.",
"maxIdleTime": "Tempo máx. ocioso (s)",
"authTimeout": "Tempo limite de autenticação (s)",
"maxUdpRelayPacketSize": "Tamanho máx. do pacote UDP",
"sni": "SNI",
"sniHint": "Substituição do Server Name Indication"
},
"tun": {
"userLevel": "Nível do Usuário"
},
+18 -1
View File
@@ -410,6 +410,7 @@
"stream": "Stream",
"streamHelp": "Обёртка блока stream Xray:"
},
"advancedTitle": "Дополнительные настройки",
"subSortIndex": "Порядок",
"inboundInfo": "Информация о подключении",
"exportInbound": "Экспорт подключений",
@@ -457,6 +458,7 @@
"moreIssues": "{message} (+{count} ещё)"
},
"form": {
"autoFill": "Автозаполнение",
"echSockopt": "ECH Sockopt",
"echSockoptTip": "Параметры сокета для соединения, через которое Xray получает список конфигураций ECH (например, можно направить запрос через исходящее dialerProxy). Оставьте отключённым для значений по умолчанию.",
"curvePreferences": "Предпочтения кривых",
@@ -768,7 +770,7 @@
"limitIpFail2banWindows": "Fail2ban недоступен в Windows, поэтому ограничение по IP не может быть применено.",
"limitIpDisabled": "Функция ограничения по IP отключена на этом сервере.",
"password": "Пароль",
"passwordDesc": "Используется только клиентами Trojan и Shadowsocks; игнорируется для VLESS, VMess, Hysteria и WireGuard.",
"passwordDesc": "Используется клиентами Trojan, Shadowsocks и TUIC; игнорируется для VLESS, VMess, Hysteria и WireGuard.",
"subId": "ID подписки",
"online": "В сети",
"email": "Email",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (секунды)",
"tunnelKeepAliveHint": "Как часто клиент шлёт keepalive-пакет. Обычное значение 25: оно держит NAT-маппинг открытым и возвращает простаивающий пир сам после любого разрыва, включая рестарт панели. 0 отключает — тогда молчащий клиент остаётся отключённым до первого своего пакета.",
"amneziaWgConfig": "Конфиг AmneziaWG",
"tuicConfig": "Конфиг TUIC (Clash)",
"mtprotoSecret": "Секрет MTProto",
"mtprotoSecretHint": "Секрет FakeTLS клиента. Перегенерируйте, чтобы сменить.",
"mtprotoAdTag": "Рекламный тег (спонсорский канал)",
@@ -915,6 +918,7 @@
"renewOnDay": "Продлевать числа",
"renewOnDayDesc": "Продлевать этого числа каждого месяца, в полночь по часовому поясу панели, вместо интервала в днях. Если в месяце такого числа нет, продление придётся на последний день. 0 — оставить режим интервала.",
"renewsUsed": "Продлений израсходовано",
"tuicTotalGBDesc": "TUIC не поддерживает персональные квоты трафика; задайте лимит на уровне инбаунда.",
"bulkAdTagHint": "Введите 32-значный hex-тег для установки или 'none' для очистки спонсорского канала на MTProto."
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "Контроль перегрузок",
"alpn": "ALPN",
"udpRelayMode": "Режим ретрансляции UDP",
"zeroRttHandshake": "0-RTT рукопожатие",
"logLevel": "Уровень логирования",
"logLevelHint": "Статус онлайн, «последний раз онлайн» и «старт после первого использования» читаются из строк Info сайдкара; Warn или Error отключают их для этого инбаунда.",
"maxIdleTime": "Макс. время простоя (сек)",
"authTimeout": "Таймаут аутентификации (сек)",
"maxUdpRelayPacketSize": "Макс. размер пакета UDP",
"sni": "SNI",
"sniHint": "Переопределение Server Name Indication"
},
"tun": {
"userLevel": "Уровень пользователя"
},
+19 -2
View File
@@ -675,7 +675,8 @@
"bytesPerSec": "Saniye Başına Bayt",
"bytesPerSecTip": "Eşik aşıldıktan sonra fallback trafiğine uygulanan hız sınırı (bayt/sn); böylece sondalar sunucunuzu hedefe ücretsiz bant genişliği olarak kullanamaz. 0 = sınır yok (bu yönü devre dışı bırakır).",
"burstBytesPerSec": "Saniye Başına Patlama Baytı",
"burstBytesPerSecTip": "Sabit hızın üzerindeki kısa patlamalar için pay (token-bucket boyutu). Saniye Başına Bayt değerinden düşükse ona eşitlenecek şekilde yükseltilir."
"burstBytesPerSecTip": "Sabit hızın üzerindeki kısa patlamalar için pay (token-bucket boyutu). Saniye Başına Bayt değerinden düşükse ona eşitlenecek şekilde yükseltilir.",
"autoFill": "Otomatik Doldur"
},
"info": {
"mode": "Mod",
@@ -694,7 +695,8 @@
"peerNumber": "Eş {n}",
"peerNumberConfig": "Peer {n} Yapılandırması"
},
"sniffingDestOverride": "Hedef geçersiz kılma"
"sniffingDestOverride": "Hedef geçersiz kılma",
"advancedTitle": "Gelişmiş Ayarlar"
},
"clients": {
"tabBasics": "Temel",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "AmneziaWG Yapılandırması",
"tuicConfig": "TUIC Yapılandırması (Clash)",
"mtprotoSecret": "MTProto sırrı",
"mtprotoSecretHint": "İstemcinin FakeTLS sırrı. Değiştirmek için yeniden oluşturun.",
"mtprotoAdTag": "Reklam etiketi (sponsorlu kanal)",
@@ -915,6 +918,7 @@
"renewOnDay": "Yenileme günü",
"renewOnDayDesc": "Her N günde bir yerine, her takvim ayının bu gününde, panel saat diliminde gece yarısı yeniler. Seçilen gün için kısa olan aylarda ayın son gününde yeniler. 0 gün aralığı modunu korur.",
"renewsUsed": "Kullanılan yenileme",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "Aramak için 32 karakterlik onaltılık etiket girin veya MTProto üzerinde temizlemek için 'none' yazın."
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "Tıkanıklık Kontrolü",
"alpn": "ALPN",
"udpRelayMode": "UDP İletim Modu",
"zeroRttHandshake": "0-RTT El Sıkışması",
"logLevel": "Günlük Seviyesi",
"logLevelHint": "Çevrimiçi durumu, son görülme ve \"ilk kullanımdan sonra başlat\" sidecar'ın Info satırlarından okunur; Warn veya Error bunları bu inbound için kapatır.",
"maxIdleTime": "Maksimum Boşta Kalma Süresi (sn)",
"authTimeout": "Kimlik Doğrulama Zaman Aşımı (sn)",
"maxUdpRelayPacketSize": "Maksimum UDP Paket Boyutu",
"sni": "SNI",
"sniHint": "Sunucu Adı Belirtimi (SNI) geçersiz kılma"
},
"tun": {
"userLevel": "Kullanıcı Seviyesi"
},
+19 -2
View File
@@ -675,7 +675,8 @@
"node": "Адреса вузла",
"listen": "Адреса прослуховування inbound",
"custom": "Користувацька"
}
},
"autoFill": "Автозаповнення"
},
"info": {
"mode": "Режим",
@@ -694,7 +695,8 @@
"peerNumber": "Peer {n}",
"peerNumberConfig": "Конфіг Peer {n}"
},
"sniffingDestOverride": "Перевизначення призначення"
"sniffingDestOverride": "Перевизначення призначення",
"advancedTitle": "Додаткові налаштування"
},
"clients": {
"tabBasics": "Основні",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "Конфігурація AmneziaWG",
"tuicConfig": "Конфігурація TUIC (Clash)",
"mtprotoSecret": "Секрет MTProto",
"mtprotoSecretHint": "Секрет FakeTLS клієнта. Згенеруйте заново, щоб змінити.",
"mtprotoAdTag": "Рекламний тег (спонсорський канал)",
@@ -915,6 +918,7 @@
"renewOnDay": "Подовжувати числа",
"renewOnDayDesc": "Подовжувати цього числа кожного місяця, опівночі за часовим поясом панелі, замість інтервалу в днях. Якщо в місяці такого числа немає, подовження припаде на останній день. 0 — залишити режим інтервалу.",
"renewsUsed": "Подовжень витрачено",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "Введіть 32-значний hex-тег для встановлення або 'none' для очищення спонсорського каналу на MTProto."
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "Контроль перевантаження",
"alpn": "ALPN",
"udpRelayMode": "Режим ретрансляції UDP",
"zeroRttHandshake": "0-RTT рукостискання",
"logLevel": "Рівень журналювання",
"logLevelHint": "Статус онлайн, «востаннє онлайн» і «старт після першого використання» читаються з рядків Info сайдкара; Warn або Error вимикають їх для цього інбаунда.",
"maxIdleTime": "Макс. час простою (сек)",
"authTimeout": "Таймаут автентифікації (сек)",
"maxUdpRelayPacketSize": "Макс. розмір пакета UDP",
"sni": "SNI",
"sniHint": "Перевизначення Server Name Indication"
},
"tun": {
"userLevel": "Рівень користувача"
},
+19 -2
View File
@@ -675,7 +675,8 @@
"node": "Địa chỉ node",
"listen": "Địa chỉ listen inbound",
"custom": "Tùy chỉnh"
}
},
"autoFill": "Tự động điền"
},
"info": {
"mode": "Chế độ",
@@ -694,7 +695,8 @@
"peerNumber": "Peer {n}",
"peerNumberConfig": "Cấu hình Peer {n}"
},
"sniffingDestOverride": "Ghi đè đích"
"sniffingDestOverride": "Ghi đè đích",
"advancedTitle": "Cài đặt nâng cao"
},
"clients": {
"tabBasics": "Cơ bản",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "Cấu hình AmneziaWG",
"tuicConfig": "Cấu hình TUIC (Clash)",
"mtprotoSecret": "Secret MTProto",
"mtprotoSecretHint": "Secret FakeTLS của client. Tạo lại để thay đổi.",
"mtprotoAdTag": "Ad-tag (kênh tài trợ)",
@@ -915,6 +918,7 @@
"renewOnDay": "Gia hạn vào ngày",
"renewOnDayDesc": "Gia hạn vào ngày này của mỗi tháng dương lịch, lúc nửa đêm theo múi giờ của bảng điều khiển, thay vì mỗi N ngày. Tháng không có ngày đã chọn sẽ gia hạn vào ngày cuối cùng của tháng. 0 giữ nguyên chế độ khoảng cách theo ngày.",
"renewsUsed": "Số lần gia hạn đã dùng",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "Nhập thẻ hex 32 ký tự để đặt hoặc 'none' để xóa kênh tài trợ trên MTProto."
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "Kiểm soát tắc nghẽn",
"alpn": "ALPN",
"udpRelayMode": "Chế độ chuyển tiếp UDP",
"zeroRttHandshake": "Bắt tay 0-RTT",
"logLevel": "Mức ghi nhật ký",
"logLevelHint": "Trạng thái trực tuyến, lần trực tuyến cuối và \"bắt đầu sau lần dùng đầu tiên\" được đọc từ các dòng Info của sidecar; Warn hoặc Error sẽ tắt chúng cho inbound này.",
"maxIdleTime": "Thời gian nhàn rỗi tối đa (giây)",
"authTimeout": "Thời gian chờ xác thực (giây)",
"maxUdpRelayPacketSize": "Kích thước gói UDP tối đa",
"sni": "SNI",
"sniHint": "Ghi đè Server Name Indication"
},
"tun": {
"userLevel": "Mức Người Dùng"
},
+20 -3
View File
@@ -675,7 +675,8 @@
"listen": "入站监听地址",
"custom": "自定义"
},
"verifyPeerCertByName": "按名称验证对端证书"
"verifyPeerCertByName": "按名称验证对端证书",
"autoFill": "自动填充"
},
"info": {
"mode": "模式",
@@ -694,7 +695,8 @@
"peerNumber": "Peer {n}",
"peerNumberConfig": "Peer {n} 配置"
},
"sniffingDestOverride": "目标覆盖"
"sniffingDestOverride": "目标覆盖",
"advancedTitle": "高级设置"
},
"clients": {
"tabBasics": "基本",
@@ -768,7 +770,7 @@
"limitIpFail2banWindows": "Windows 上不支持 Fail2ban,无法实施 IP 限制。",
"limitIpDisabled": "此服务器已禁用 IP 限制功能。",
"password": "密码",
"passwordDesc": " TrojanShadowsocks 客户端使用;VLESS、VMess、Hysteria 和 WireGuard 忽略此项。",
"passwordDesc": "用于 TrojanShadowsocks 和 TUIC 客户端;对 VLESS、VMess、Hysteria 和 WireGuard 忽略。",
"subId": "订阅 ID",
"online": "在线",
"email": "邮箱",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "AmneziaWG 配置",
"tuicConfig": "TUIC 配置 (Clash)",
"mtprotoSecret": "MTProto 密钥",
"mtprotoSecretHint": "该客户端的 FakeTLS 密钥。重新生成即可更换。",
"mtprotoAdTag": "广告标签(赞助频道)",
@@ -915,6 +918,7 @@
"renewOnDay": "按日期续期",
"renewOnDayDesc": "每个自然月的这一天午夜(按面板时区)续期,而不是每 N 天续期一次。若当月没有该日期,则在当月最后一天续期。填 0 保持按天间隔模式。",
"renewsUsed": "已用续期次数",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "输入 32 位十六进制标签以设置,或输入 'none' 清除 MTProto 赞助频道。"
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "拥塞控制算法",
"alpn": "ALPN",
"udpRelayMode": "UDP 转发模式",
"zeroRttHandshake": "0-RTT 握手",
"logLevel": "日志级别",
"logLevelHint": "在线状态、最后在线时间和“首次使用后开始”来自 sidecar 的 Info 日志行;选择 Warn 或 Error 会使此入站失去这些功能。",
"maxIdleTime": "最大空闲时间 (秒)",
"authTimeout": "认证超时时间 (秒)",
"maxUdpRelayPacketSize": "最大 UDP 数据包大小",
"sni": "SNI",
"sniHint": "服务器名称指示 (SNI) 覆盖"
},
"tun": {
"userLevel": "用户级别"
},
+19 -2
View File
@@ -675,7 +675,8 @@
"node": "節點地址",
"listen": "入站監聽地址",
"custom": "自訂"
}
},
"autoFill": "自動填入"
},
"info": {
"mode": "模式",
@@ -694,7 +695,8 @@
"peerNumber": "Peer {n}",
"peerNumberConfig": "Peer {n} 設定"
},
"sniffingDestOverride": "目標覆寫"
"sniffingDestOverride": "目標覆寫",
"advancedTitle": "進階設定"
},
"clients": {
"tabBasics": "基本",
@@ -882,6 +884,7 @@
"tunnelKeepAlive": "Keepalive (seconds)",
"tunnelKeepAliveHint": "How often the client sends a keepalive packet. 25 is the usual value: it holds the NAT mapping open and brings an idle peer back on its own after any drop, including a panel restart. 0 disables it — an idle client then stays disconnected until it sends traffic.",
"amneziaWgConfig": "AmneziaWG 設定",
"tuicConfig": "TUIC 設定 (Clash)",
"mtprotoSecret": "MTProto 金鑰",
"mtprotoSecretHint": "該用戶端的 FakeTLS 金鑰。重新產生即可更換。",
"mtprotoAdTag": "廣告標籤(贊助頻道)",
@@ -915,6 +918,7 @@
"renewOnDay": "按日期續期",
"renewOnDayDesc": "每個自然月的這一天午夜(依面板時區)續期,而不是每 N 天續期一次。若當月沒有該日期,則在當月最後一天續期。填 0 保持按天間隔模式。",
"renewsUsed": "已用續期次數",
"tuicTotalGBDesc": "TUIC does not support per-client traffic limits; set traffic limit on the inbound instead.",
"bulkAdTagHint": "輸入 32 位十六進位標籤以設定,或輸入 'none' 清除 MTProto 贊助頻道。"
},
"groups": {
@@ -1960,6 +1964,19 @@
"listenPortHint": "Fixed local UDP source port. Leave 0 to pick one automatically.",
"outboundObfuscationHint": "Must exactly match the server side parameters."
},
"tuic": {
"congestionControl": "壅塞控制演算法",
"alpn": "ALPN",
"udpRelayMode": "UDP 轉發模式",
"zeroRttHandshake": "0-RTT 握手",
"logLevel": "日誌層級",
"logLevelHint": "線上狀態、最後上線時間和「首次使用後開始」來自 sidecar 的 Info 日誌行;選擇 Warn 或 Error 會使此入站失去這些功能。",
"maxIdleTime": "最大閒置時間 (秒)",
"authTimeout": "認證逾時時間 (秒)",
"maxUdpRelayPacketSize": "最大 UDP 封包大小",
"sni": "SNI",
"sniHint": "伺服器名稱指示 (SNI) 覆蓋"
},
"tun": {
"userLevel": "用戶級別"
},
+7
View File
@@ -21,6 +21,7 @@ import (
"github.com/mhsanaei/3x-ui/v3/internal/eventbus"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/mtproto"
"github.com/mhsanaei/3x-ui/v3/internal/tuic"
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
"github.com/mhsanaei/3x-ui/v3/internal/util/sys"
"github.com/mhsanaei/3x-ui/v3/internal/web/controller"
@@ -293,6 +294,7 @@ const (
cadenceXrayTraffic = "@every 5s"
cadenceMtproto = "@every 10s"
cadenceAmneziaWG = "@every 10s"
cadenceTuic = "@every 10s"
cadenceClientIPScan = "@every 10s"
cadenceNodeHeartbeat = "@every 5s"
cadenceNodeTraffic = "@every 5s"
@@ -339,6 +341,10 @@ func (s *Server) startTask(restartXray bool, loc *time.Location) {
_, _ = s.cron.AddJob(cadenceAmneziaWG, awgJob)
go awgJob.Run()
tuicJob := job.NewTuicJob()
_, _ = s.cron.AddJob(cadenceTuic, tuicJob)
go tuicJob.Run()
// check client ips from log file every 10 sec
_, _ = s.cron.AddJob(cadenceClientIPScan, job.NewCheckClientIpJob())
@@ -699,6 +705,7 @@ func (s *Server) stop(stopXray bool, stopTgBot bool) error {
_ = s.xrayService.StopXray()
mtproto.GetManager().StopAll()
amneziawgnet.GetManager().StopAll()
tuic.GetManager().StopAll()
amneziawgnet.GetOutboundManager().StopAll()
}
if s.cron != nil {