mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-30 01:34:59 +03:00
fix(traffic): show live Speed for AmneziaWG and MTProto inbounds/clients
The Speed column showed "--" for AmneziaWG (and MTProto, which has the identical gap) even while cumulative traffic totals were correct. XrayTrafficJob drives live speed by querying xray-core's own stats API and broadcasting the delta over websocket -- but AmneziaWG/MTProto never run inside xray-core's own runtime inbounds, so they're invisible to that API. Their own jobs already compute the same per-poll delta shape (that's what keeps cumulative totals correct) but never broadcast it. Reusing the existing "traffics"/"clientTraffics" broadcast would have two real bugs: the frontend's existing scope/replace logic would let each side clobber the other's speed on its next unrelated tick, and the websocket hub's per-message-type throttle is keyed only by message type, not caller -- since both sidecar jobs run on identical "@every 10s" grids registered milliseconds apart, one would silently lose almost every broadcast if both protocols were ever configured together. Fixed with a small unthrottled broadcast path (both sidecar jobs are already self-rate-limited by their own cron cadence) and protocol- namespaced wire keys, tracked in their own frontend state and merged into the existing inboundSpeed/clientSpeed only at read time -- so every existing consumer needs zero changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package job
|
||||
|
||||
import (
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
@@ -84,5 +85,9 @@ func (j *AmneziaWGJob) Run() {
|
||||
}
|
||||
}
|
||||
|
||||
// Live speed: AmneziaWG never runs inside xray-core, so XrayTrafficJob's
|
||||
// own 5s broadcast never mentions these tags. See sidecar_traffic.go.
|
||||
broadcastSidecarTraffic(string(model.AmneziaWG), traffics, clientTraffics)
|
||||
|
||||
j.inboundService.RefreshLocalOnlineClients(onlineEmails, activeTags)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package job
|
||||
|
||||
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/web/service"
|
||||
@@ -77,5 +78,12 @@ func (j *MtprotoJob) Run() {
|
||||
}
|
||||
}
|
||||
|
||||
// Live speed: mtproto's mtg sidecar never runs inside xray-core, so
|
||||
// XrayTrafficJob's own 5s broadcast never mentions these tags. traffics
|
||||
// here already excludes routed-through-xray inbound tags (existing
|
||||
// logic above, for cumulative-totals reasons) -- reused as-is, not
|
||||
// recomputed. See sidecar_traffic.go.
|
||||
broadcastSidecarTraffic(string(model.MTProto), traffics, clientTraffics)
|
||||
|
||||
j.inboundService.RefreshLocalOnlineClients(onlineEmails, activeTags)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/web/websocket"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
|
||||
// sidecarTrafficPayload builds the websocket broadcast map for one poll of a
|
||||
// sidecar protocol's traffic delta. "Sidecar" means a protocol that never
|
||||
// runs inside xray-core's own inbounds -- AmneziaWG's kernel tunnel and
|
||||
// MTProto's mtg process are the only two today (both explicitly skipped from
|
||||
// claiming an xray-core tag in GenXrayInboundConfig). XrayTrafficJob's own 5s
|
||||
// broadcast (xray_traffic_job.go) carries "traffics"/"clientTraffics" for
|
||||
// xray-native inbounds/clients and never mentions sidecar tags, so reusing
|
||||
// those keys would let each side's broadcast clobber the other's speed on
|
||||
// its very next, unrelated tick. protocol namespaces the keys instead:
|
||||
// "amneziawg" produces "amneziawgTraffics"/"amneziawgClientTraffics",
|
||||
// "mtproto" produces "mtprotoTraffics"/"mtprotoClientTraffics" -- distinct
|
||||
// pairs per protocol, so the frontend tracks each independently and never
|
||||
// has to reconcile two protocols within one map (see useInbounds.ts /
|
||||
// useClients.ts).
|
||||
func sidecarTrafficPayload(protocol string, traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) map[string]any {
|
||||
return map[string]any{
|
||||
protocol + "Traffics": traffics,
|
||||
protocol + "ClientTraffics": clientTraffics,
|
||||
}
|
||||
}
|
||||
|
||||
// broadcastSidecarTraffic pushes this poll's live-speed snapshot for a
|
||||
// sidecar protocol ("amneziawg" or "mtproto") over the websocket, so its
|
||||
// inbound/client rows show a live Speed value the same way xray-native rows
|
||||
// already do. Call this every tick -- even with empty slices -- so a peer
|
||||
// that just went idle clears to no-speed on the frontend instead of sticking
|
||||
// at its last nonzero reading; only the AddTraffic/RefreshLocalOnlineClients
|
||||
// calls around it are conditioned on non-empty data, since cumulative-totals
|
||||
// accounting doesn't need this signal. Uses BroadcastSidecarTraffic (not
|
||||
// BroadcastTraffic) -- see its doc comment for why.
|
||||
func broadcastSidecarTraffic(protocol string, traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) {
|
||||
if !websocket.HasClients() {
|
||||
return
|
||||
}
|
||||
websocket.BroadcastSidecarTraffic(sidecarTrafficPayload(protocol, traffics, clientTraffics))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
|
||||
func TestSidecarTrafficPayload_UsesProtocolNamespacedKeys(t *testing.T) {
|
||||
traffics := []*xray.Traffic{{IsInbound: true, Tag: "amneziawg-1", Up: 10, Down: 20}}
|
||||
clientTraffics := []*xray.ClientTraffic{{Email: "a@b.c", Up: 10, Down: 20}}
|
||||
|
||||
payload := sidecarTrafficPayload("amneziawg", traffics, clientTraffics)
|
||||
|
||||
if _, ok := payload["amneziawgTraffics"]; !ok {
|
||||
t.Fatal("expected amneziawgTraffics key")
|
||||
}
|
||||
if _, ok := payload["amneziawgClientTraffics"]; !ok {
|
||||
t.Fatal("expected amneziawgClientTraffics key")
|
||||
}
|
||||
for _, wrongKey := range []string{"traffics", "clientTraffics", "mtprotoTraffics", "mtprotoClientTraffics"} {
|
||||
if _, ok := payload[wrongKey]; ok {
|
||||
t.Fatalf("payload must not contain %q -- it would collide with a different broadcast source", wrongKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSidecarTrafficPayload_DistinctProtocolsNamespacedIndependently(t *testing.T) {
|
||||
amneziawgPayload := sidecarTrafficPayload("amneziawg", nil, nil)
|
||||
mtprotoPayload := sidecarTrafficPayload("mtproto", nil, nil)
|
||||
|
||||
if _, ok := amneziawgPayload["mtprotoTraffics"]; ok {
|
||||
t.Fatal("amneziawg payload must not contain mtproto keys")
|
||||
}
|
||||
if _, ok := mtprotoPayload["amneziawgTraffics"]; ok {
|
||||
t.Fatal("mtproto payload must not contain amneziawg keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSidecarTrafficPayload_EmptyInputsStillProduceBothKeys(t *testing.T) {
|
||||
// The frontend clears a peer's speed when its tag/email is absent from
|
||||
// the payload's arrays -- but only if the key itself is present. If the
|
||||
// key vanished entirely for an idle poll, idle-clearing would never
|
||||
// trigger and the last nonzero speed would stick forever.
|
||||
payload := sidecarTrafficPayload("amneziawg", nil, nil)
|
||||
|
||||
if _, ok := payload["amneziawgTraffics"]; !ok {
|
||||
t.Fatal("expected amneziawgTraffics key present even for nil input")
|
||||
}
|
||||
if _, ok := payload["amneziawgClientTraffics"]; !ok {
|
||||
t.Fatal("expected amneziawgClientTraffics key present even for nil input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcastSidecarTraffic_NoOpWithoutHub(t *testing.T) {
|
||||
// No global web server/hub is configured in this test binary, so
|
||||
// websocket.HasClients() is false -- this must return without panicking.
|
||||
broadcastSidecarTraffic("amneziawg", nil, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user