2023-02-09 19:18:06 +00:00
|
|
|
|
package job
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2025-02-22 09:45:14 +00:00
|
|
|
|
"encoding/json"
|
2025-09-18 20:06:01 +00:00
|
|
|
|
|
2026-05-10 00:13:42 +00:00
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/logger"
|
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/web/service"
|
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/web/websocket"
|
|
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/xray"
|
2025-02-22 09:45:14 +00:00
|
|
|
|
|
|
|
|
|
|
"github.com/valyala/fasthttp"
|
2023-02-09 19:18:06 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-20 07:35:50 +00:00
|
|
|
|
// XrayTrafficJob collects and processes traffic statistics from Xray, updating the database and optionally informing external APIs.
|
2023-02-09 19:18:06 +00:00
|
|
|
|
type XrayTrafficJob struct {
|
2025-02-22 09:45:14 +00:00
|
|
|
|
settingService service.SettingService
|
2024-01-29 20:37:20 +00:00
|
|
|
|
xrayService service.XrayService
|
|
|
|
|
|
inboundService service.InboundService
|
|
|
|
|
|
outboundService service.OutboundService
|
2023-02-09 19:18:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-20 07:35:50 +00:00
|
|
|
|
// NewXrayTrafficJob creates a new traffic collection job instance.
|
2023-02-09 19:18:06 +00:00
|
|
|
|
func NewXrayTrafficJob() *XrayTrafficJob {
|
|
|
|
|
|
return new(XrayTrafficJob)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
// Run collects traffic statistics from Xray, updates the database, and pushes
|
|
|
|
|
|
// real-time updates over WebSocket using compact delta payloads — no REST
|
|
|
|
|
|
// fallback, scales to 10k–20k+ clients per inbound.
|
2023-02-09 19:18:06 +00:00
|
|
|
|
func (j *XrayTrafficJob) Run() {
|
|
|
|
|
|
if !j.xrayService.IsXrayRunning() {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
traffics, clientTraffics, err := j.xrayService.GetXrayTraffic()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
needRestart0, clientsDisabled, err := j.inboundService.AddTraffic(traffics, clientTraffics)
|
2023-02-09 19:18:06 +00:00
|
|
|
|
if err != nil {
|
2024-01-29 20:37:20 +00:00
|
|
|
|
logger.Warning("add inbound traffic failed:", err)
|
2023-02-09 19:18:06 +00:00
|
|
|
|
}
|
2024-01-29 20:37:20 +00:00
|
|
|
|
err, needRestart1 := j.outboundService.AddTraffic(traffics, clientTraffics)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Warning("add outbound traffic failed:", err)
|
|
|
|
|
|
}
|
2026-05-04 21:19:25 +00:00
|
|
|
|
if clientsDisabled {
|
|
|
|
|
|
restartOnDisable, settingErr := j.settingService.GetRestartXrayOnClientDisable()
|
|
|
|
|
|
if settingErr != nil {
|
|
|
|
|
|
logger.Warning("get RestartXrayOnClientDisable failed:", settingErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
if restartOnDisable {
|
|
|
|
|
|
if err := j.xrayService.RestartXray(true); err != nil {
|
|
|
|
|
|
logger.Warning("restart xray after disabling clients failed:", err)
|
|
|
|
|
|
j.xrayService.SetToNeedRestart()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat(nodes): traffic-writer queue, full-mirror sync, WS event fixes
- Traffic-writer single-consumer queue (web/service/traffic_writer.go)
serialises every DB write that touches up/down/all_time/last_online
(AddTraffic, SetRemoteTraffic, Reset*, UpdateClientTrafficByEmail) so
overlapping goroutines can no longer clobber each other's column-scoped
Updates with a stale tx.Save.
- DB pool: WAL + busy_timeout=10s + synchronous=NORMAL + _txlock=
immediate, MaxOpenConns=8 / MaxIdleConns=4. The immediate-tx PRAGMA
fixes residual "database is locked [0ms]" cases where deferred-tx
writer-upgrade conflicts bypass busy_timeout.
- SetRemoteTraffic full-mirrors node-authoritative state into central:
settings JSON, remark, listen, port, total, expiry, all_time, enable,
plus per-client total/expiry/reset/all_time. Inbounds and
client_traffics rows present on node but missing from central are
created; rows missing from snap are deleted (with cascading
client_traffics removal).
- NodeTrafficSyncJob detects structural changes from the mirror and
broadcasts invalidate(inbounds) so open central UIs re-fetch via REST
on node-side add/del/edit without manual refresh.
- XrayTrafficJob broadcasts invalidate(inbounds) when auto-disable flips
client_traffics.enable so the per-client toggle reflects depletion
without manual refresh.
- Frontend: inbounds page now subscribes to the BroadcastInbounds 'inbounds'
WS event (full-list pushes from add/del/update controllers were silently
dropped). Fixes invalidate payload field (dataType -> type). Restart-
panel modal switched from Promise-wrap to onOk-only so Cancel actually
cancels.
- Node files trimmed of stale prose-comments; cron cadence dropped
10s -> 5s to match the inbounds page UX.
- README badges and Go module path bumped v2 -> v3 to match module rename.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 14:25:23 +00:00
|
|
|
|
websocket.BroadcastInvalidate(websocket.MessageTypeInbounds)
|
2026-05-04 21:19:25 +00:00
|
|
|
|
}
|
2025-02-22 09:45:14 +00:00
|
|
|
|
if ExternalTrafficInformEnable, err := j.settingService.GetExternalTrafficInformEnable(); ExternalTrafficInformEnable {
|
|
|
|
|
|
j.informTrafficToExternalAPI(traffics, clientTraffics)
|
|
|
|
|
|
} else if err != nil {
|
|
|
|
|
|
logger.Warning("get ExternalTrafficInformEnable failed:", err)
|
|
|
|
|
|
}
|
2024-01-29 20:37:20 +00:00
|
|
|
|
if needRestart0 || needRestart1 {
|
2023-08-26 11:49:51 +00:00
|
|
|
|
j.xrayService.SetToNeedRestart()
|
2023-02-09 19:18:06 +00:00
|
|
|
|
}
|
2026-01-03 04:26:00 +00:00
|
|
|
|
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
// If no frontend client is connected, skip all WebSocket broadcasting
|
|
|
|
|
|
// routines — including the active-client DB query and JSON marshaling.
|
2026-04-19 19:01:00 +00:00
|
|
|
|
if !websocket.HasClients() {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
// Online presence + traffic deltas — small payload, always fits in WS.
|
|
|
|
|
|
// Force non-nil slice/map so JSON marshalling produces [] / {} instead of
|
|
|
|
|
|
// `null` when everyone is offline. The frontend's traffic handler treats
|
|
|
|
|
|
// a missing/null onlineClients field as "no update", so without this the
|
|
|
|
|
|
// "everyone went offline" transition was silently dropped — stale online
|
|
|
|
|
|
// users lingered in the list and the online filter kept showing them.
|
2026-01-03 04:26:00 +00:00
|
|
|
|
onlineClients := j.inboundService.GetOnlineClients()
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
if onlineClients == nil {
|
|
|
|
|
|
onlineClients = []string{}
|
|
|
|
|
|
}
|
2026-01-03 04:26:00 +00:00
|
|
|
|
lastOnlineMap, err := j.inboundService.GetClientsLastOnline()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Warning("get clients last online failed:", err)
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
if lastOnlineMap == nil {
|
2026-01-03 04:26:00 +00:00
|
|
|
|
lastOnlineMap = make(map[string]int64)
|
|
|
|
|
|
}
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
websocket.BroadcastTraffic(map[string]any{
|
2026-04-19 19:01:00 +00:00
|
|
|
|
"traffics": traffics,
|
|
|
|
|
|
"clientTraffics": clientTraffics,
|
|
|
|
|
|
"onlineClients": onlineClients,
|
|
|
|
|
|
"lastOnlineMap": lastOnlineMap,
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
})
|
2026-04-19 19:01:00 +00:00
|
|
|
|
|
2026-05-13 23:31:49 +00:00
|
|
|
|
// Full snapshot every cycle: absolute per-client counters and inbound
|
|
|
|
|
|
// totals. Frontend overwrites both in place. The previous delta path
|
|
|
|
|
|
// (activeEmails -> GetActiveClientTraffics) silently omitted the
|
|
|
|
|
|
// clients array whenever nobody moved bytes in the cycle, leaving the
|
|
|
|
|
|
// client rows in the UI stuck at stale traffic/remained/all-time.
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
clientStatsPayload := map[string]any{}
|
2026-05-13 23:31:49 +00:00
|
|
|
|
if stats, err := j.inboundService.GetAllClientTraffics(); err != nil {
|
|
|
|
|
|
logger.Warning("get all client traffics for websocket failed:", err)
|
|
|
|
|
|
} else if len(stats) > 0 {
|
|
|
|
|
|
clientStatsPayload["clients"] = stats
|
2026-01-05 04:50:40 +00:00
|
|
|
|
}
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
if inboundSummary, err := j.inboundService.GetInboundsTrafficSummary(); err != nil {
|
|
|
|
|
|
logger.Warning("get inbounds traffic summary for websocket failed:", err)
|
|
|
|
|
|
} else if len(inboundSummary) > 0 {
|
|
|
|
|
|
clientStatsPayload["inbounds"] = inboundSummary
|
2026-01-05 04:50:40 +00:00
|
|
|
|
}
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
if len(clientStatsPayload) > 0 {
|
|
|
|
|
|
websocket.BroadcastClientStats(clientStatsPayload)
|
2026-01-05 04:50:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
// Outbounds list is small (one row per outbound, no per-client expansion)
|
|
|
|
|
|
// so the full snapshot still fits comfortably in WS.
|
|
|
|
|
|
if updatedOutbounds, err := j.outboundService.GetOutboundsTraffic(); err == nil && updatedOutbounds != nil {
|
2026-01-05 04:50:40 +00:00
|
|
|
|
websocket.BroadcastOutbounds(updatedOutbounds)
|
ws/inbounds: realtime fixes + perf for 10k+ client inbounds (#4123)
* ws/inbounds: realtime fixes + perf for 10k+ client inbounds
- hub: dedup, throttle, panic-restart, deadlock fix, race tests
- client: backoff cap + slow-retry instead of giving up
- broadcast: delta-only payload, count-based invalidate fallback
- filter: fix empty online list (Inbound has no .id, use dbInbound.toInbound)
- perf: O(N²)→O(N) traffic merge, bulk delete, /setEnable endpoint
- traffic: monotonic all_time + UI clamp + propagate in delta handler
- session: persist on update/logout (fixes logout-after-password-change)
- ui: protocol tags flex, traffic bar normalize
* Remove hub_test.go file
* fix: ws hub, inbound service, and frontend correctness
- propagate DelInbound error on disable path in SetInboundEnable
- skip empty emails in updateClientTraffics to avoid constraint violations
- use consistent IN ? clause, drop redundant ErrRecordNotFound guards
- Hub.Unregister: direct removeClient fallback when channel is full
- applyClientStatsDelta: O(1) email lookup via per-inbound Map cache
- WS payload size check: Blob.size instead of .length for real byte count
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: chunk large IN ? queries and fix IPv6 same-origin check
* fix: unify clientStats cache, throttle clarity, hub constants
* fix(ui): align traffic/expiry cell columns across all rows
* style(ui): redesign outbounds table for visual consistency
* style(ui): redesign routing table for visual consistency
* fix:
* fix:
* fix:
* fix:
* fix:
* fix: font
* refactor: simplify outbound tone functions for consistency and maintainability
---------
Co-authored-by: lolka1333 <test123@gmail.com>
2026-05-05 15:27:49 +00:00
|
|
|
|
} else if err != nil {
|
|
|
|
|
|
logger.Warning("get all outbounds for websocket failed:", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-22 09:45:14 +00:00
|
|
|
|
func (j *XrayTrafficJob) informTrafficToExternalAPI(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) {
|
|
|
|
|
|
informURL, err := j.settingService.GetExternalTrafficInformURI()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Warning("get ExternalTrafficInformURI failed:", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
Security hardening: sessions, SSRF, CSP nonce, CSRF logout, trusted proxies (#4275)
* refactor(session): store user ID in session instead of full struct
Replaces storing the full User object in the session cookie with just
the user ID. GetLoginUser now re-fetches the user from the database on
every request so credential/permission changes take effect immediately
without requiring a re-login. Includes a backward-compatible migration
path for existing sessions that still carry the old struct payload.
* feat(auth): block panel with default admin/admin credentials and guide credential change
checkLogin middleware now detects default admin/admin credentials and
redirects every panel route to /panel/settings until they are changed.
The settings page auto-opens the Authentication tab, shows a
non-dismissible error banner, and lists 'Default credentials' first in
the security checklist. Login response includes mustChangeCredentials
so the login page can redirect directly. Logout is now POST-only.
Password must be at least 10 characters and cannot be admin/admin.
* feat(settings): redact secrets in AllSettingView and add TrustedProxyCIDRs
Introduces AllSettingView which strips tgBotToken, twoFactorToken,
ldapPassword, apiToken and warp/nord secrets before sending them to
the browser, replacing them with boolean hasFoo presence flags. A new
/panel/setting/secret endpoint allows updating individual secrets by
key. Secrets that arrive blank on a save are preserved from the DB
rather than overwritten. Adds TrustedProxyCIDRs as a configurable
setting (defaults to localhost CIDRs). URL fields are validated before
save.
* fix(security): SSRF prevention, trusted-proxy header gating, CSP nonce, HTTP timeouts
Adds SanitizeHTTPURL / SanitizePublicHTTPURL to reject private-range
and loopback targets before any outbound HTTP request (node probe,
xray download, outbound test, external traffic inform, tgbot API
server, panel updater). Forwarded headers (X-Real-IP, X-Forwarded-For,
X-Forwarded-Host) are now only trusted when the direct connection
arrives from a CIDR in TrustedProxyCIDRs. CSP policy is tightened with
a per-request nonce. HTTP server gains read/write/idle timeouts. Panel
updater downloads the script to a temp file instead of piping curl into
shell. Xray archive download adds a size cap and response-code check.
backuptotgbot is changed from GET to POST.
* feat(nodes): add allow-private-address toggle per node
Adds AllowPrivateAddress to the Node model (DB default false). When
enabled it bypasses the SSRF private-range check for that node's probe
URL, allowing nodes hosted on RFC-1918 or loopback addresses (e.g.
a private VPN or LAN setup).
* chore: frontend UX improvements, CI pipeline, and dev tooling
- AppSidebar: logout via POST /logout instead of navigating to GET
- InboundList: persist filter state (search, protocol, node) to
localStorage across page reloads; add protocol and node filter dropdowns
- IndexPage: add health status strip (Xray, CPU, Memory, Update) with
quick-action buttons
- dependabot: weekly go mod and npm update schedule
- ci.yml: add GitHub Actions workflow for build and vet
- .nvmrc: pin Node 22 for local development
- frontend: bump package.json and package-lock.json
- SubPage, DnsPresetsModal, api-docs: minor fixes
* fix(ci): stub web/dist before go list to satisfy go:embed at compile time
* chore(ui): remove health-strip bar from dashboard top
* Revert "feat(auth): block panel with default admin/admin credentials and guide credential change"
This reverts commit 56ce6073ce09f08147f989858e0e88b3a4359546.
* fix(auth): make logout POST+CSRF and propagate session loss to other tabs
- Switch /logout from GET to POST with CSRFMiddleware so it matches the
SPA's existing HttpUtil.post('/logout') call (previously 404'd silently)
and blocks GET-based logout via image tags or link prefetchers. Handler
now returns JSON; the SPA already navigates client-side.
- Return 401 (instead of 404) from /panel/api/* when the caller is a
browser XHR (X-Requested-With: XMLHttpRequest) so the axios interceptor
redirects to the login page on logout-in-another-tab, cookie expiry,
and server restart. Anonymous callers still get 404 to keep endpoints
hidden from casual scanners.
- One-shot the 401 redirect in axios-init.js and hang the rejected
promise so queued polls don't stack reloads or surface error toasts
while the browser is navigating away.
- Add the CSP nonce to the runtime-injected <script> in dist.go so the
panel loads under the existing script-src 'nonce-...' policy.
- Update api-docs endpoints.js: GET /logout doc entry was missing.
* fix(settings): POST /logout after credential change
* fix(auth): invalidate other sessions when credentials change
When the admin changes username/password from one machine, sessions
on every other machine kept working until they manually logged out
because session storage is a signed client-side cookie — there is
no server-side session list to revoke.
Add a per-user LoginEpoch counter stamped into the session at login
and re-verified on every authenticated request. UpdateUser and
UpdateFirstUser bump the epoch (UpdateUser via gorm.Expr so a single
update statement is atomic), so any cookie issued before the change
no longer matches the user's current epoch and GetLoginUser returns
nil — the SPA's 401 interceptor then redirects to the login page.
Backward compatible: the column defaults to 0 and missing cookie
values are treated as 0, so sessions issued before this change
remain valid until the first credential update.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-05-13 10:52:52 +00:00
|
|
|
|
informURL, err = service.SanitizePublicHTTPURL(informURL, false)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Warning("ExternalTrafficInformURI blocked:", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-03-12 19:13:51 +00:00
|
|
|
|
requestBody, err := json.Marshal(map[string]any{"clientTraffics": clientTraffics, "inboundTraffics": inboundTraffics})
|
2025-02-22 09:45:14 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
logger.Warning("parse client/inbound traffic failed:", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
request := fasthttp.AcquireRequest()
|
|
|
|
|
|
defer fasthttp.ReleaseRequest(request)
|
|
|
|
|
|
request.Header.SetMethod("POST")
|
|
|
|
|
|
request.Header.SetContentType("application/json; charset=UTF-8")
|
|
|
|
|
|
request.SetBody([]byte(requestBody))
|
|
|
|
|
|
request.SetRequestURI(informURL)
|
|
|
|
|
|
response := fasthttp.AcquireResponse()
|
|
|
|
|
|
defer fasthttp.ReleaseResponse(response)
|
|
|
|
|
|
if err := fasthttp.Do(request, response); err != nil {
|
|
|
|
|
|
logger.Warning("POST ExternalTrafficInformURI failed:", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|