mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-10 16:05:01 +03:00
7c84ca9689
Invariant: a client whose stats row is switched off is served by no local runtime of any inbound it is attached to. client_traffics is email-keyed, and AddClientStat re-points its inbound_id at the last inbound attached, yet the runtime push builder and the MTProto, TUIC and AmneziaWG desired- instance builders looked the row up by inbound_id. On every other inbound of a multi-inbound client the depletion filter saw nothing, so a depleted client stayed served there whenever its settings entry still read enabled - the state the stale settings writes left in existing databases. All four now resolve the flag through trafficDisabledEmails, keyed by the emails the inbound actually lists. GetXrayConfig already backfills sibling rows by email (backfillClientStats) and is unchanged.
148 lines
4.4 KiB
Go
148 lines
4.4 KiB
Go
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/mtproto"
|
|
)
|
|
|
|
// DesiredMtprotoInstances derives the mtg sidecar configs this panel should be
|
|
// running: one instance per enabled local mtproto inbound, serving only the
|
|
// secrets of clients that are both enabled in the inbound settings and not
|
|
// depletion-disabled in client_traffics. That is the same effective client set
|
|
// buildInboundForLocalRuntime pushes on interactive edits, so the reconcile job
|
|
// and the push paths agree on one fingerprint — a disagreement would surface
|
|
// as a needless mtg restart, and a job that read only the raw settings would
|
|
// keep serving depleted clients until an unrelated restart. Inbounds whose
|
|
// every secret is filtered away are omitted so Reconcile stops their sidecar.
|
|
func (s *InboundService) DesiredMtprotoInstances() ([]mtproto.Instance, error) {
|
|
db := database.GetDB()
|
|
var inbounds []*model.Inbound
|
|
err := db.Model(model.Inbound{}).
|
|
Where("protocol = ? AND enable = ? AND node_id IS NULL", model.MTProto, true).
|
|
Find(&inbounds).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(inbounds) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
instances := make([]mtproto.Instance, 0, len(inbounds))
|
|
for _, ib := range inbounds {
|
|
inst, ok := mtproto.InstanceFromInbound(ib)
|
|
if !ok {
|
|
continue
|
|
}
|
|
instances = append(instances, inst)
|
|
}
|
|
emails := make([]string, 0)
|
|
for _, inst := range instances {
|
|
for _, e := range inst.Secrets {
|
|
emails = append(emails, e.Name)
|
|
}
|
|
}
|
|
disabled, err := trafficDisabledEmails(db, emails)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
served := instances[:0]
|
|
for _, inst := range instances {
|
|
kept := make([]mtproto.SecretEntry, 0, len(inst.Secrets))
|
|
for _, e := range inst.Secrets {
|
|
if _, off := disabled[e.Name]; !off {
|
|
kept = append(kept, e)
|
|
}
|
|
}
|
|
inst.Secrets = kept
|
|
if len(kept) > 0 {
|
|
served = append(served, inst)
|
|
}
|
|
}
|
|
return served, nil
|
|
}
|
|
|
|
// applyLocalMtproto pushes a single local mtproto inbound's current client set
|
|
// to its mtg sidecar right after a client edit commits, so an add, removal,
|
|
// re-key or enable-toggle takes effect immediately instead of waiting up to
|
|
// 10s for the reconcile job. With a reload-capable mtg the change is applied in
|
|
// place without dropping other clients; older binaries fall back to a restart
|
|
// inside the manager. It re-reads the inbound so it sees the committed settings,
|
|
// filters depleted clients exactly like the reconcile job, and is a no-op for
|
|
// node-owned or non-mtproto inbounds. Failures are logged and swallowed: the
|
|
// reconcile job is the backstop, and an xray restart cannot help the sidecar.
|
|
func (s *InboundService) applyLocalMtproto(inboundId int) {
|
|
inbound, err := s.GetInbound(inboundId)
|
|
if err != nil || inbound == nil || inbound.Protocol != model.MTProto || 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("mtproto: immediate client apply failed for inbound", inboundId, ":", err)
|
|
}
|
|
}
|
|
|
|
func (s *InboundService) resetMtprotoClientQuota(email string) {
|
|
mgr := mtproto.GetManager()
|
|
if !mgr.HasRunning() {
|
|
return
|
|
}
|
|
id, ok := s.localMtprotoInboundIdForEmail(email)
|
|
if !ok {
|
|
return
|
|
}
|
|
s.applyLocalMtproto(id)
|
|
mgr.ResetQuota(email)
|
|
}
|
|
|
|
func (s *InboundService) resetAllMtprotoQuotas() {
|
|
mgr := mtproto.GetManager()
|
|
if !mgr.HasRunning() {
|
|
return
|
|
}
|
|
desired, err := s.DesiredMtprotoInstances()
|
|
if err != nil {
|
|
return
|
|
}
|
|
mgr.Reconcile(desired)
|
|
for _, inst := range desired {
|
|
for _, sec := range inst.Secrets {
|
|
mgr.ResetQuota(sec.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *InboundService) localMtprotoInboundIdForEmail(email string) (int, bool) {
|
|
db := database.GetDB()
|
|
var inbounds []*model.Inbound
|
|
if err := db.Model(model.Inbound{}).
|
|
Where("protocol = ? AND node_id IS NULL", model.MTProto).
|
|
Find(&inbounds).Error; err != nil {
|
|
return 0, false
|
|
}
|
|
for _, ib := range inbounds {
|
|
inst, ok := mtproto.InstanceFromInbound(ib)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, sec := range inst.Secrets {
|
|
if sec.Name == email {
|
|
return ib.Id, true
|
|
}
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|