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
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-05-15 09:41:29 +00:00
|
|
|
|
j.inboundService.RefreshOnlineClientsFromMap(lastOnlineMap)
|
|
|
|
|
|
|
fix: address open bug reports (#4539, #4538, #4535, #4531, #4515) (#4545)
* fix: hash-storage panic on SIGHUP and seeder dup-key on cold restart (#4539)
Two bugs that combine into an unrecoverable crash loop after a user
enables the Telegram bot in settings on a fresh install.
1. CheckHashStorageJob.Run panics with a nil pointer dereference. The
cron job is scheduled whenever settings say the bot is enabled, but
the package-level hash storage is only initialized inside
Tgbot.Start, which StartPanelOnly intentionally skips
(startTgBot=false). Toggling the bot on via the panel triggers
SIGHUP, the storage stays nil, and the cron fires 2 minutes later
and panics, exiting 2.
2. seedClientsFromInboundJSON is not idempotent. The fresh-install
early-return path recorded only UserPasswordHash + ApiTokensTable,
never ClientsTable. After the admin adds clients via the panel
(which writes to the clients table through SyncInbound), the next
start runs the seeder for the first time, finds matching emails
already in the table, and fails with SQLSTATE 23505 on
idx_clients_email, turning the panic above into an unrecoverable
crash loop on PostgreSQL.
Fixes:
- web/job/check_hash_storage.go: nil-check the storage before calling
RemoveExpiredHashes.
- database/db.go: in the fresh-install early-return path, also record
ClientsTable so the seeder never re-runs against panel-added data.
- database/db.go: hydrate seedClientsFromInboundJSON's byEmail cache
from existing rows so it merges instead of inserting when a row with
the same email already lives in the clients table.
Regression tests cover both paths.
Closes #4539
* fix(clients): preserve protocol-specific credentials across multi-inbound syncs (#4538)
fillProtocolDefaults only populates the credential relevant to the
inbound's protocol (c.ID for VLESS, c.Auth for Hysteria, c.Password
for Trojan/Shadowsocks). Each inbound's settings.clients JSON
therefore carries the same client with only one of those fields set.
SyncInbound's update path was unconditionally copying every credential
column from incoming to the existing clients row, so the second sync
(e.g. Hysteria after VLESS) would write UUID="" over a valid VLESS
UUID and Auth="" the other way around. The next GetXrayConfig then
emitted VLESS client entries with no "id" field, and xray-core
crashed on startup with "common/uuid: invalid UUID:".
Guard UUID/Password/Auth/Flow/Security/Reverse against empty
overwrites so each protocol's sync only writes the credentials it
actually owns. Other fields (LimitIP, TotalGB, Comment, etc.) keep
the existing copy-everything behavior so admins can still clear them
through the panel.
Regression test in client_sync_multiprotocol_test.go.
Closes #4538
* fix(expiry): show delayed-start countdown in subscribe and client info (#4535)
A client with "start after first use" expiry stores the duration as a
negative number of milliseconds (e.g. -86400000 = 1 day after first
connect). The clients page row already renders this correctly as
"Delayed start: 1d", but two other surfaces treated negative values as
zero and rendered them as unlimited:
- Subscription header: the index==0 / index>0 branches in subService,
subClashService and subJsonService only carried ExpiryTime forward
when > 0, so traffic.ExpiryTime stayed at zero and the header sent
expire=0. Every imported client appeared to have no expiry, and the
built-in subscribe page rendered the "unlimited" tag.
- ClientInfoModal: both the expiryLabel helper and the rendering check
treated <= 0 as the "no expiry" branch, so the modal showed an
infinity tag instead of "Delayed start: Nd".
Add subscriptionExpiryFromClient to map negative durations onto a
"now + |value|" timestamp so subscription clients see an actual expiry
they can count down from. Update ClientInfoModal's helper and render
to match the clients-page convention.
Regression test in subService_test.go covers the helper.
Refs #4535
* feat(clash): emit xhttp and httpupgrade transports in subscription (#4531)
applyTransport's switch only covered tcp/ws/grpc; xhttp and
httpupgrade inbounds fell through to the default branch and returned
false. buildProxy then returned a nil map and the inbound was dropped
from the Clash subscription. When the subscription only contained
xhttp/httpupgrade inbounds, the proxies list ended up empty and the
client saw a 404 (or an "Error!" body on older builds), then refused
to parse.
Add a case for each, mapping the inbound's stream settings onto the
Mihomo-format opts blocks:
xhttp -> xhttp-opts: { path, host, mode }
httpupgrade -> http-upgrade-opts: { path, headers: { Host } }
Host falls back to the headers map when the dedicated `host` field is
empty, matching the existing ws behavior.
Closes #4531
* fix(online): refresh online-clients list even when no WS frontend is connected (#4515)
XrayTrafficJob and NodeTrafficSyncJob both gated the entire
post-traffic-write block behind websocket.HasClients() to skip
expensive broadcasts when no browser is open. The block included the
RefreshOnlineClientsFromMap call that keeps the in-memory
p.onlineClients list current.
Several non-WS consumers read that same list:
- Telegram bot (tgbot.go calls p.GetOnlineClients in 3 places)
- REST GET /panel/api/onlines (returned to API callers)
- Internal alerts that check whether a client is online
When no browser was watching the dashboard, the list went stale and
stayed empty, so the bot reported "nobody online" and the onlines API
returned [] even when xray had active sessions.
Move RefreshOnlineClientsFromMap above the HasClients guard so the
in-memory list is always fresh. Only the actual BroadcastTraffic /
BroadcastClientStats / BroadcastOutbounds calls (and the
GetAllClientTraffics / GetInboundsTrafficSummary work that feeds them)
remain gated by HasClients.
Closes #4515
* fix: address copilot review on #4545
Two issues raised by the Copilot review:
1) subscriptionExpiryFromClient called time.Now() per invocation.
Two clients with the same delayed-start duration normalized to
timestamps a few milliseconds apart, so the aggregator's
"if normalized != traffic.ExpiryTime" check tripped and the
subscription header expire= dropped back to 0 — the exact bug
the helper was meant to fix, just one client later.
Take nowMs as a parameter; each of GetSubs / GetClash / GetConfig
captures one timestamp per request and reuses it.
2) Guarding Flow against empty incoming values in SyncInbound
prevented a user from ever clearing a VLESS flow via the panel.
FlowOverride on client_inbounds is the per-inbound mechanism that
already preserves flow correctly across protocols, so the guard
on the shared clients.flow column is the wrong place.
Drop the Flow guard, keep the rest (UUID/Password/Auth/Security/
Reverse — none of which have a per-inbound override column).
Adds a regression test that asserts clearing flow on the owning
inbound makes ListForInbound return flow="".
The existing cross-protocol test is rewritten to assert on the
user-visible behavior (ListForInbound flow) instead of the shared
clients.flow column.
2026-05-24 22:08:06 +00:00
|
|
|
|
if !websocket.HasClients() {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 09:41:29 +00:00
|
|
|
|
onlineClients := j.inboundService.GetOnlineClients()
|
|
|
|
|
|
if onlineClients == nil {
|
|
|
|
|
|
onlineClients = []string{}
|
|
|
|
|
|
}
|
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
|
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|