experimental/clashapi: emit "user" field in TrackerMetadata.MarshalJSON#4159
Open
PavelLizunov wants to merge 1 commit into
Open
experimental/clashapi: emit "user" field in TrackerMetadata.MarshalJSON#4159PavelLizunov wants to merge 1 commit into
PavelLizunov wants to merge 1 commit into
Conversation
The Clash-API `/traffic` and `/connections` streams emit a fixed metadata key set: `network, type, sourceIP, destinationIP, sourcePort, destinationPort, host, dnsMode, processPath`. The authenticated user (already populated server-side by every auth-bearing protocol — VLESS, TUIC, Trojan, etc — into `adapter.InboundContext.User`) is silently dropped. Downstream consumers that want per-user attribution (abuse detection, per-account quota visualisation, multi-tenant dashboards) have no path to associate a connection with the account that opened it. The information is server-local but not emitted on the wire. This patch adds a single key `"user": t.Metadata.User` to the marshalled metadata map, immediately after `"type"` to keep the high-frequency fields adjacent. For inbound contexts WITHOUT an authenticated user (direct/socks/tun) the field is an empty string — preserving the existing observable behaviour for those inbounds while unblocking per-user attribution where the auth layer has populated it. No behaviour change for the server itself, no schema migration, no compat break — the addition is a new optional key in a JSON object; old clients ignore unknown keys. Real-world driver: a Rust-based VPN-fleet control plane (github.com/PavelLizunov/vpnctl) polls clash-api on each node to attribute per-connection traffic to inv.db user_ids; the attribution path returns NULL on every row because `User` is never on the wire.
PavelLizunov
pushed a commit
to PavelLizunov/vpnctl
that referenced
this pull request
May 22, 2026
README had been frozen at v0.2 («v0.2 in progress», says it lists 4 crates and 2 protocols, no daemon, no admin UI). Reality: v0.8 is in flight with the full daemon + admin UI + 3 kernels + 8 protocols + restore close-out + uptime SLO chips + bulk-ack + bilingual EN/RU + 1010 workspace tests. Rewritten to a feature matrix that an external reader can scan in 30 seconds: «what ships today», «known gaps carried into v0.9», and a flow table mapping every operator action to both the web button and the CLI alternative (canonical surface is web per the «оператор НИКОГДА не должен открывать terminal» principle). ## Two v0.8 tasks closed 1. **`decode_form_value` UTF-8 review** — re-verified, NOT a bug. Implementation is bounds-checked, uses `from_utf8_lossy` (the correct lenient policy for HTML form input — paste-from-broken- Windows-clipboard MUST NOT 4xx the operator), and every consumer routes through `form_field` for per-field validation. The 2026-05-16 `e250789` audit's «deferred minor» note was about being LESS lenient — which on reflection would be a UX regression. Closed as «no fix needed». 2. **NM-11 upstream PR** — filed SagerNet/sing-box#4159. 1-line diff to `experimental/clashapi/trafficontrol/tracker.go` adding `"user": t.Metadata.User` in the JSON marshal map. Branch `feat/clashapi-emit-user-field` on `PavelLizunov/sing-box` fork. PR body explains the driver (vpnctld's NULL `vpn_connection_stats.user_id`), the compatibility (additive JSON key, no schema/protocol break), and includes a manual test plan. Targets the `testing` branch (verified the merge pattern via `gh pr list --state merged`; `dev-next` was my first wrong guess). ## CLAUDE.md hygiene * v0.8 in-progress: `decode_form_value` ⏳ → ✅ (closed as non-issue) * v0.8 in-progress: NM-11 ⏳ → ✅ (PR in flight upstream) * README references the upstream PR by number so future-me lands here from search engines. ## Layer coverage Pure docs/memory change. cargo fmt clean, clippy clean, all 1010 workspace tests pass. No daemon redeploy needed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TrackerMetadata.MarshalJSON(inexperimental/clashapi/trafficontrol/tracker.go)emits a fixed set of metadata keys but silently drops the authenticated
user identifier. This PR adds a single key
"user": t.Metadata.Usertothe marshalled JSON.
Why
adapter.InboundContext.Useris populated server-side by every auth-bearinginbound (VLESS, TUIC, Trojan, etc) — the data exists, it just never
reaches the Clash-API wire. Downstream consumers that need per-user
attribution (abuse detection, per-account quota visualisation,
multi-tenant dashboards) have no way to associate a connection with the
account that opened it.
What changes
For inbounds WITHOUT an authenticated user (direct/socks/tun)
Useristhe empty string — preserves existing observable behaviour for those
inbounds.
Compatibility
the de-facto convention.
upstream of
MarshalJSON; this only changes what gets marshalled.Real-world driver
A Rust-based VPN-fleet control plane (github.com/PavelLizunov/vpnctl)
polls clash-api on each node to attribute per-connection traffic to
inv.db user_ids. The attribution column in its
vpn_connection_statstable is
NULLon every row today becauseUseris never on the wire —even though authenticated VLESS connections populate it server-side.
Test plan
curl localhost:9090/connections→ metadata now contains"user": "<uuid-or-name>"."user": ""(no regression).