Skip to content

peer: report direct-sharing relayed bytes in a snapshot - #689

Merged
myleshorton merged 8 commits into
mainfrom
fisk/peer-relay-snapshot
Oct 19, 2026
Merged

myleshorton merged 8 commits into
mainfrom
fisk/peer-relay-snapshot

Conversation

@myleshorton

@myleshorton myleshorton commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #688. Merge that first, and GitHub will retarget this PR to main.

Why

Advanced stats shows speed, session data and per-country bytes for Unbounded volunteers, but nothing for direct sharing (Share My Connection): radiance had no byte counts for peer connections. getlantern/lantern-box#371 attaches a live peerconn.Counter to every samizdat connection, and this PR turns those counters into something the app can display.

What changes

  • peer.relayStats sums the counters per session and per client IP. Open connections are read live. A close folds in the connection's final count, so the totals keep it after the connection ends.
  • Open connections are keyed by their *Counter, not by source. samizdat multiplexes streams over one TCP connection, so several open streams can share an ip:port. The same counter returns on each stream's close, so verifier streams are now matched by counter too, and peer: hide lantern-cloud's verify callback from connection events #688's per-source count is only the fallback for events without one.
  • Tallies are pruned as new connections arrive as well as on each snapshot, so a session no one polls doesn't keep every client it has seen. On Stop, end() starts a retention timer for connections still open, whose closes are dropped while draining. They're read live for 60s, then folded into the totals and released.
  • Client.RelaySnapshot() → LocalBackend.PeerRelaySnapshot() → GET /peer/snapshot → ipc.Client.PeerRelaySnapshot(). The JSON matches unbounded.Snapshot (epoch, running, peers, bytesDown, bytesUp, peerBytes[{ip,down,up}]), so the app can fold it with its existing relay counter code.
  • Like Unbounded's, the epoch is fixed for the process, and totals are cumulative across sessions. A Stop and Start between two polls loses nothing, and connections a stopped session left open are folded in at the next begin. Each session has a generation that also advances at end, so a listener callback that resumes after Stop (or after Stop and Start) isn't counted.
  • A departed client's totals stay in the snapshot for 60s, matching unbounded.peerRetention, so the app's 1s poller sees its final count.
  • The verifier connections peer: hide lantern-cloud's verify callback from connection events #688 filters never reach the tally.
sequenceDiagram
    autonumber
    participant SB as samizdat inbound<br/>lantern-box#371
    participant L as peerconn listener<br/>peer.go
    participant R as relayStats<br/>snapshot.go
    participant IPC as /peer/snapshot<br/>ipc/server.go
    participant App as lantern-core poller

    SB->>L: accept {Source, Destination, Bytes: *Counter}
    L->>L: isVerifierConn? drop (#688)
    L->>R: record(+1, source, counter) ⚠️
    Note over SB: wrapConn Read/Write<br/>add to the counter
    App->>IPC: GET every 1s
    IPC->>R: snapshot(): closed totals + live counters
    SB->>L: close {Source, Bytes}
    L->>R: record(-1): fold final count
Loading

Dependency bump

lantern-box goes from v0.0.154 to v0.0.166 (the release with #371), and the sing-box-minimal replace moves to the version that lantern-box main requires (mux.ContextWithSessionConn). This picks up lantern-box v0.0.155 through v0.0.165 as well. go mod tidy was run, and go.mod and go.sum are committed together. #371 is merged and released as v0.0.166, which this now pins.

Testing

  • relayStats tests: live and closed counts; per-IP sums across one client's connections; retention pruning; a new epoch per session; the totals surviving Stop; uncounted and unmatched events ignored.
  • TestClient_RelaySnapshot: end to end through a started client, with the verifier excluded and a poll after Stop still seeing the bytes.
  • go test -race ./peer/ ./ipc/ ./backend/ passes, and go test passes for every package except cmd/lantern, whose ipc.NewClient call is already broken on main.

Pre-PR review (local Codex gate)

  • Rounds: 3 — final: CODEX GATE: verdict=approve critical=0 important=0 minor=0 reviewed=23
  • Exit: approve
  • Fixed:
    • peer/snapshot.go:52 — open connections were keyed by ip:port, so a second multiplexed stream from one source overwrote the first one's counter (found while rebasing on peer: hide lantern-cloud's verify callback from connection events #688's fix). Now keyed by counter, with TestRelayStats_StreamsSharingASource.
    • peer/snapshot.go:130 (r1, important) — tallies were pruned only on snapshot, so an unpolled session grew with every client it saw. Now also pruned on accept, with TestRelayStats_PrunesWithoutSnapshots.
    • peer/peer.go:535 (r1, minor) — connections open at Stop were never retired. end() plus time-based retirement, with TestRelayStats_RetiresOpenConnectionsAfterStop.
    • peer/peer.go:536 (r2, important) — per-source verifier matching could hide an ordinary stream's close instead, leaving its counter held. Now matched by counter, with TestClient_OrdinaryCloseBeforeVerifierOnSharedSource (fails on the old code).
  • Dismissed: none

🤖 Generated with Claude Code

https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39

Summary by CodeRabbit

  • New Features
    • Added relay traffic snapshots showing cumulative data transferred, per-peer byte totals, and whether sharing is active.
    • Snapshots retain totals across sessions and provide a brief window to view statistics for recently stopped sessions or departed peers.

Copilot AI balanced review requested due to automatic review settings October 19, 2026 18:36
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e800c6b7-0042-4bdf-bfb9-6ebd8378e833
📥 Commits

Reviewing files that changed from the base of the PR and between 5043a9a and c050d89.

⛔ Files ignored due to path filters: 1
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing: 9
  • backend/peer_share.go
  • backend/radiance_test.go
  • go.mod
  • ipc/client.go
  • ipc/server.go
  • peer/peer.go
  • peer/peer_test.go
  • peer/snapshot.go
  • peer/snapshot_test.go
+------------------------------------------+
| 💣 Deploying bug fixes in 3... 2... 1... |
+------------------------------------------+
  \
   \     /)/)
        ( '')
        /  |
      o(___)
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Connection accounting and session lifecycle handling can produce incorrect totals and retain departed peers indefinitely.

3 open issues
What changed in this PR

Adds direct-sharing traffic snapshots so the app can display session and per-client byte totals alongside Unbounded statistics.

Changes:

  • Tracks live and closed connection counters by session.
  • Exposes snapshots through the backend and /peer/snapshot IPC endpoint.
  • Adds accounting tests and updates supporting dependencies.
File Description
peer/​snapshot.go Implements relay accounting and snapshots.
peer/​snapshot_test.go Tests totals, retention, epochs, and client integration.
peer/​peer.go Connects accounting to session and connection events.
ipc/​server.go Adds the snapshot endpoint.
ipc/​client.go Adds the snapshot client method.
go.sum Updates dependency checksums.
go.mod Updates counter-supporting dependencies.
backend/​radiance_test.go Extends peer controller test doubles.
backend/​peer_share.go Exposes snapshots through the backend.

Review effort: Balanced

Comment thread peer/peer.go Outdated
Comment thread peer/snapshot.go Outdated
Comment thread peer/snapshot.go Outdated
Adam Fisk and others added 4 commits October 19, 2026 13:10
Advanced stats can show speed, session data and per-country bytes for
Unbounded but not for direct sharing, because radiance had no byte counts for
peer connections. lantern-box now attaches a live peerconn.Counter to each
samizdat connection; the peer client adds them up per session and per client
IP, and serves the totals at /peer/snapshot (ipc.Client.PeerRelaySnapshot) in
the shape of unbounded.Snapshot, so the app can fold both with the same code.

Each session gets its own epoch and counts from zero. A departed client's
totals stay for 60s so the app's 1s poller sees its final count, and a
stopped session's totals stay readable until the next Start. Verifier
connections are already filtered before they reach the tally.

Bumps lantern-box to the commit carrying the counter (getlantern/lantern-box#371)
and sing-box-minimal to the version that lantern-box requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39
samizdat multiplexes streams over one TCP conn, so open streams can share a
source; keyed by source, a second stream overwrote the first's counter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39
Departed clients were pruned only when a snapshot was taken, so a session
no one polled kept every client IP it saw. They are now also pruned as new
connections arrive. Stop drops the close events of connections still open,
so a stopped session's are retired by time: read live for the retention
window, then folded into the totals and dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39
Counting verifier streams per source balances the per-IP totals but can hide
an ordinary stream's close in place of the verifier's, which left that
stream's counter in the relay tally for the rest of the session. Events now
carry the stream's counter, so a close is hidden exactly when its own accept
was; the per-source count remains for events without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39
@myleshorton
myleshorton force-pushed the fisk/peer-relay-snapshot branch from c6fc94d to 8c3aa4a Compare October 19, 2026 19:20
A listener callback can pass the drain check, stall, and resume after
Stop→Start has begun a new relay session, recording the old session's
counter into the new epoch. Each session now has a generation the listener
captures, and events from any other generation are ignored.

Addresses review on #689.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39
myleshorton pushed a commit to getlantern/lantern that referenced this pull request Oct 19, 2026
Advanced stats showed speed, session data and per-country bytes only for
Unbounded; direct sharing (SmC) got a note saying data was unavailable.
radiance now serves a direct-sharing relay snapshot at /peer/snapshot in
the shape of Unbounded's, so lantern-core polls it every second as
peer-snapshot and the share notifier folds it with the same relay code.

Each snapshot source gets its own counter tracker and sample clock: both
arrive every second with separate epochs, and a shared tracker would treat
every alternation as a restart. Peer snapshots are folded in every mode, so
bytes a session relays as it stops still count, but drive the speed only
while SmC is active, and an idle Unbounded snapshot no longer zeroes SmC's
speed. People still come from peer-connection events, so the snapshot's
arrivals are not counted again.

Bumps radiance to the snapshot commit (getlantern/radiance#689), with
lantern-box and sing-box-minimal to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39
@myleshorton
myleshorton requested a balanced review from Copilot October 19, 2026 19:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

An unresolved teardown race and stacked networking dependency upgrades require fixes and human integration review.

1 open issue
3 resolved since last review

Review effort: Balanced

Comment thread peer/peer.go
A Stop and Start between two of the app's polls lost the stopped session's
unpolled bytes, because begin reset the totals under a fresh epoch. Like
Unbounded's, the epoch is now fixed for the process and totals carry across
sessions; connections a session left open fold in at the next begin. And
end now advances the generation, so a verifier accept that resumes after
Stop, when isVerifierConn no longer recognizes it, is not counted.

Addresses review on #689 and #9181.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Restarting can discard counters still receiving bytes during teardown, permanently undercounting relayed traffic.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread peer/snapshot.go Outdated
closeBox can return before the old box finishes closing, so a stopped
session's connections can still move bytes after the next begin, which
folded their counters once and dropped them. They are now retired instead:
no longer listed as peers, read live until relayRetention, then folded.

Addresses review on #689.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Concurrent teardown and restart handling, prerequisite integration, and the planned dependency retagging require final human validation.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

The release carrying peerconn.Counter (getlantern/lantern-box#371).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39
Base automatically changed from fisk/peer-hide-verify-conns to main October 19, 2026 20:29
@myleshorton
myleshorton merged commit 79f0a5b into main Oct 19, 2026
2 of 3 checks passed
@myleshorton
myleshorton deleted the fisk/peer-relay-snapshot branch October 19, 2026 20:39
myleshorton pushed a commit to getlantern/lantern that referenced this pull request Oct 19, 2026
Advanced stats showed speed, session data and per-country bytes only for
Unbounded; direct sharing (SmC) got a note saying data was unavailable.
radiance now serves a direct-sharing relay snapshot at /peer/snapshot in
the shape of Unbounded's, so lantern-core polls it every second as
peer-snapshot and the share notifier folds it with the same relay code.

Each snapshot source gets its own counter tracker and sample clock: both
arrive every second with separate epochs, and a shared tracker would treat
every alternation as a restart. Peer snapshots are folded in every mode, so
bytes a session relays as it stops still count, but drive the speed only
while SmC is active, and an idle Unbounded snapshot no longer zeroes SmC's
speed. People still come from peer-connection events, so the snapshot's
arrivals are not counted again.

Bumps radiance to the snapshot commit (getlantern/radiance#689), with
lantern-box and sing-box-minimal to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39
myleshorton added a commit to getlantern/lantern that referenced this pull request Oct 19, 2026
* Action Mode: show relayed data for direct sharing

Advanced stats showed speed, session data and per-country bytes only for
Unbounded; direct sharing (SmC) got a note saying data was unavailable.
radiance now serves a direct-sharing relay snapshot at /peer/snapshot in
the shape of Unbounded's, so lantern-core polls it every second as
peer-snapshot and the share notifier folds it with the same relay code.

Each snapshot source gets its own counter tracker and sample clock: both
arrive every second with separate epochs, and a shared tracker would treat
every alternation as a restart. Peer snapshots are folded in every mode, so
bytes a session relays as it stops still count, but drive the speed only
while SmC is active, and an idle Unbounded snapshot no longer zeroes SmC's
speed. People still come from peer-connection events, so the snapshot's
arrivals are not counted again.

Bumps radiance to the snapshot commit (getlantern/radiance#689), with
lantern-box and sing-box-minimal to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39

* Action Mode: persist the direct-sharing relay watermark

Without it, a restarted app's first SmC snapshot only set a baseline, so
bytes relayed while the app was closed never reached the all-time total.
The peer feed now saves and restores its own epoch and counters alongside
Unbounded's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39

* Action Mode: zero SmC speed on outage, keep the save throttle

When the backend is unreachable peer snapshots stop, but only Unbounded's
outage reset the speed, so direct sharing kept showing its last figure.
And the SmC branch of the Unbounded snapshot forced a save every second,
which with SmC snapshots now marking the stats dirty bypassed the 30s
throttle; it now forces only when Unbounded's counters moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQG2dRhaocR2UY5RPamm39

* deps: pin lantern-box v0.0.166 and radiance 79f0a5bd8

lantern-box v0.0.166 carries peerconn.Counter (getlantern/lantern-box#371);
radiance 79f0a5bd8 is main with the relay snapshot merged
(getlantern/radiance#688, #689) and already contains a0b72470 from #9182.
sing-box-minimal moves to the version lantern-box v0.0.166 requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE

---------

Co-authored-by: Adam Fisk <a@lantern.io>
Co-authored-by: Claude Opus 5.5 <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.

2 participants