You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
GET /api/connections (current spec and PR #1's redesign) is a
pull-based snapshot. Two things a dashboard needs cannot be built from
it, no matter how fast it polls:
Short-lived flows are invisible. Connections that open and close
between two polls — plain DNS lookups, health checks, chatty apps —
never appear.
Traffic history cannot be accurate. "Bytes per site / process /
outbound / node over hours or days" needs each flow's final byte
totals, which only exist at close time and are never observed by
polling.
Proposal
Add a push-based lifecycle event stream:
GET /api/connections/stream (SSE or WebSocket, capability-gated per #1)
Events embed feat(api): add some based honk/dae pat #1's connection object; a kind field distinguishes established (flow opened) from closed (carries the flow's finalupload_bytes / download_bytes).
Two attributes dashboards need most, beyond feat(api): add some based honk/dae pat #1's current object: pname and the selected node (dae's outbound names the routing
target — typically the group — not the node that carried the flow).
Backpressure: the engine must never let a slow consumer stall the
data path — bounded queue with drop under pressure. Loss accounting
travels inside the stream: per-event sequence numbers (dropped events
leave a detectable gap) plus periodic stats events carrying a
cumulative dropped count and dropped-byte totals, so clients can
bound the error.
With this, a dashboard records every flow it observes, once, with exact
final totals — no polling losses, no diffing.
Prior art
I maintain a dae dashboard that consumes this kind of stream via a
small patch (one JSON event per datagram on a unix socket, exported
from the routing plane): it powers a 30-day traffic history with
drill-down by site / process / outbound / node. The routing plane
already knows each flow's metadata and byte totals at close time, so
the plumbing is cheap. The same instrumentation point would also
serve my routing-trace proposal (docs: draft /api/routing/trace endpoint #2).
Clash/mihomo's WS /connections — which honk already exposes via experimental.clash_api — pushes periodic full-state snapshots.
Attribution exists for the flows a push happens to catch, but
between-push flows and their final totals are lost. Lifecycle events
fix this by pushing deltas instead of snapshots.
Open questions
Transport: SSE vs WebSocket (either way — browsers can set custom
headers on neither, so credential passing needs a decision).
Baseline for flows already open at subscribe time (synthetic established replay, or snapshot + dedupe by id).
UDP session semantics: what established / closed mean without a
handshake (idle timeout).
Sequence numbering: global or per-subscription (interacts with
server-side filtering, if we add it later — filtered-out events look
like gaps).
Behavior across engine reloads/restarts (generation id on events?).
Alternatives considered
Poll faster — sampling losses never reach zero.
Push snapshots over WS (Clash-style, today's incumbent) —
inherits the sampling gaps; only aggregate totals stay exact.
Pull-based changelog (GET .../events?since=<cursor>) — same
event source without the transport questions; same drop accounting.
Server-side aggregation endpoint — moves history persistence
into the engine, a role change the current design avoids.
External observers (pcap/conntrack/eBPF) — no outbound/node
attribution; that mapping lives inside the engine.
Problem
GET /api/connections(current spec and PR #1's redesign) is apull-based snapshot. Two things a dashboard needs cannot be built from
it, no matter how fast it polls:
between two polls — plain DNS lookups, health checks, chatty apps —
never appear.
outbound / node over hours or days" needs each flow's final byte
totals, which only exist at close time and are never observed by
polling.
Proposal
Add a push-based lifecycle event stream:
kindfield distinguishesestablished(flow opened) fromclosed(carries the flow'sfinal
upload_bytes/download_bytes).pnameand the selected node (dae'soutboundnames the routingtarget — typically the group — not the node that carried the flow).
data path — bounded queue with drop under pressure. Loss accounting
travels inside the stream: per-event sequence numbers (dropped events
leave a detectable gap) plus periodic
statsevents carrying acumulative
droppedcount and dropped-byte totals, so clients canbound the error.
With this, a dashboard records every flow it observes, once, with exact
final totals — no polling losses, no diffing.
Prior art
small patch (one JSON event per datagram on a unix socket, exported
from the routing plane): it powers a 30-day traffic history with
drill-down by site / process / outbound / node. The routing plane
already knows each flow's metadata and byte totals at close time, so
the plumbing is cheap. The same instrumentation point would also
serve my routing-trace proposal (docs: draft /api/routing/trace endpoint #2).
WS /connections— which honk already exposes viaexperimental.clash_api— pushes periodic full-state snapshots.Attribution exists for the flows a push happens to catch, but
between-push flows and their final totals are lost. Lifecycle events
fix this by pushing deltas instead of snapshots.
Open questions
headers on neither, so credential passing needs a decision).
establishedreplay, or snapshot + dedupe byid).established/closedmean without ahandshake (idle timeout).
server-side filtering, if we add it later — filtered-out events look
like gaps).
Alternatives considered
inherits the sampling gaps; only aggregate totals stay exact.
GET .../events?since=<cursor>) — sameevent source without the transport questions; same drop accounting.
into the engine, a role change the current design avoids.
outbound/nodeattribution; that mapping lives inside the engine.