Skip to content

experimental/clashapi: emit "user" field in TrackerMetadata.MarshalJSON#4159

Open
PavelLizunov wants to merge 1 commit into
SagerNet:testingfrom
PavelLizunov:feat/clashapi-emit-user-field
Open

experimental/clashapi: emit "user" field in TrackerMetadata.MarshalJSON#4159
PavelLizunov wants to merge 1 commit into
SagerNet:testingfrom
PavelLizunov:feat/clashapi-emit-user-field

Conversation

@PavelLizunov
Copy link
Copy Markdown

Summary

TrackerMetadata.MarshalJSON (in experimental/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.User to
the marshalled JSON.

Why

adapter.InboundContext.User is populated server-side by every auth-bearing
inbound (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

"metadata": map[string]any{
    "network":         t.Metadata.Network,
    "type":            inbound,
+   "user":            t.Metadata.User,    // ← added
    "sourceIP":        t.Metadata.Source.Addr,
    ...
}

For inbounds WITHOUT an authenticated user (direct/socks/tun) User is
the empty string — preserves existing observable behaviour for those
inbounds.

Compatibility

  • No schema migration — JSON object gets one new optional key.
  • No protocol break — Clash-API clients ignore unknown fields per
    the de-facto convention.
  • No server-side behaviour change — the field is already populated
    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_stats
table is NULL on every row today because User is never on the wire —
even though authenticated VLESS connections populate it server-side.

Test plan

  • Builds clean (1-line addition to an existing literal).
  • Manual: enable clash-api on a dev sing-box, authenticate via VLESS,
    curl localhost:9090/connections → metadata now contains "user": "<uuid-or-name>".
  • Unauthenticated inbound (direct/tun) still emits "user": "" (no regression).

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant