Skip to content

peer: serve helper pairings' short IDs on the samizdat inbound - #698

Merged
myleshorton merged 12 commits into
mainfrom
fisk/helper-direct-mode
Oct 22, 2026
Merged

myleshorton merged 12 commits into
mainfrom
fisk/helper-direct-mode

Conversation

@myleshorton

@myleshorton myleshorton commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

The volunteer side of Lantern's helper invites direct mode. A volunteer's samizdat inbound now accepts per-pairing short IDs from lantern-cloud, so that users who paired with this volunteer (e.g. in Iran) can connect to it. A volunteer can also choose to serve only its pairings.

Server side: getlantern/lantern-cloud#3675. Transport: getlantern/samizdat#18 and getlantern/lantern-box#376 (in v0.0.168+; main is on v0.0.169).

sequenceDiagram
    autonumber
    participant R as radiance<br/>peer/peer.go
    participant API as lantern-cloud
    participant B as samizdat inbound<br/>lantern-box v0.0.168
    R->>API: register {serve_pool omitted unless false}
    API-->>R: server_config + pairing_short_ids<br/>known, revision
    R->>B: build box, UpdateShortIDs(base ∪ pairing) before Start
    loop every 150 s
        R->>API: heartbeat
        API-->>R: pairing_short_ids, known, revision ⚠️
        Note over R: apply only if known AND revision > last applied
        R->>B: UpdateShortIDs(base ∪ pairing)<br/>(removed IDs: their sessions end)
    end
Loading

What it does

  • lantern-box: main already pins v0.0.169, which includes Inbound.UpdateShortIDs from fix: use the same value for the url test outbound and rule #376, so this PR doesn't bump it.
  • Peer API:
    • Register and heartbeat parse pairing_short_ids, pairing_short_ids_known and pairing_short_ids_revision. The revision is protojson's int64, which decodes from a string or a number.
    • A heartbeat body that won't decode never fails the heartbeat: it reads as "not known".
    • Register is strict again: an empty 2xx register body is an error.
  • peer_serve_pool setting, default true:
    • It's sent as serve_pool: false only when the setting is off; otherwise it's omitted.
    • A settings patch is persisted before its peer-share toggle runs, so "enable sharing, pairings only" registers correctly.
    • The owner can set it over IPC.
    • It takes effect at the next registration (Start or the hourly rotation). Re-registering immediately on a toggle comes with the app UI.
  • The pairing short-ID set on the running inbound:
    • The volunteer applies a set only when the server marks it authoritative (known) and its server-issued revision is newer than the last one applied. Responses that arrive out of order, such as a heartbeat in flight during a rotation, can't restore a revoked pairing.
    • known=false, a failed heartbeat or an unreadable heartbeat keeps the current set and never revokes.
    • If no authoritative set arrives for 48 h, the longest a pairing short ID is valid, the volunteer drops to its base ID, so a pairing ended while some API replicas lack the ticket key is still revoked.
    • New and rotated boxes get the set before Start, and the swap reconciles the new box to the current record, so paired users never hit a base-only box after a rotation.
    • A failed push is retried on later heartbeats.
    • Logs carry no short IDs and no pairing counts.

Known, not fixed here

  • Clock skew: if one API replica's clock runs hours ahead, its revisions make the client ignore correctly stamped sets until real time catches up. Paired users fall back to other routes meanwhile. This assumes NTP-synced servers; a bound against local time could follow.
  • Coverage gaps: there's no test for a failed post-swap push being retried onto the new box (the code handles it), and none for a Stop→Start resetting the revision.
  • cmd/lantern: it doesn't compile on main either (ipc.NewClient signature), and main's CI is green, so it's unrelated.

Tests

  • go test -race -tags with_clash_api ./peer/... ./backend/... ./ipc/... passes. The short-ID, revision, rotation, expiry and heartbeat tests passed -count=10 under -race. The CI command (go test -tags with_clash_api on every package except cmd/lantern) passes.
  • Each rule has a targeted test that fails when its guard is removed. That includes a real sing-box box with the samizdat inbound (TestSamizdatInbound_RealBox).

Pre-PR review (local Codex gate)

  • Rounds: 5. Rounds 4–5 ran past the 3-round cap with the human's approval. Final: round=5 CODEX GATE: verdict=findings critical=0 important=1 minor=0 reviewed=17.
  • Exit: the gate did not reach approve. Its last finding was fixed in dfb46f5 and verified by a separate state-machine review on the strongest model (all seven rules pass, no deadlocks), not by another Codex round.
  • Fixed:
    • peer/peer.go — a rotation could overwrite a newer heartbeat set (r1);
    • backend/radiance.go — peer_serve_pool was read before a same-patch persist (r1);
    • ipc/server.go — peer_serve_pool was missing from the owner allowlist (r1);
    • peer/peer.go — the generation was captured after Register (r2);
    • peer/peer.go — an in-flight heartbeat could apply after a rotation (r3, r4). Fixed at the root by the server-issued revision, which replaces client-side counters.
    • peer/peer.go — expiry during a rotation left stale IDs on the new box (r5).
  • Dismissed: none.

Per-task reviews (spec compliance plus code quality) and a branch review also ran on the strongest model before the gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE

Summary by CodeRabbit

  • New Features
    • Added a setting to control whether peer sharing serves the general pool.
    • Peer connections now receive and apply pairing short IDs, keeping them in sync as pairing information changes.
  • Bug Fixes
    • Peer-sharing settings are applied even when saving other settings fails, and peer-only changes no longer attempt to save an empty update.
    • Improved recovery from temporary pairing updates and credential changes, while preventing outdated pairing information from replacing newer updates.

Copilot AI balanced review requested due to automatic review settings October 21, 2026 23:30
@coderabbitai

coderabbitai Bot commented Oct 21, 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
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9d974704-0463-4a25-bf3b-ffaf6be233a8
📥 Commits

Reviewing files that changed from the base of the PR and between dfb46f5 and d9c9d6e.

📒 Files selected for processing: 2
  • peer/peer.go
  • peer/peer_test.go
+---------------------------------------------------------------------------+
| Your code and I are going to be best friends. Very critical best friends. |
+---------------------------------------------------------------------------+
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a8206510-5313-41b9-b5b9-6993a807e3d9
📥 Commits

Reviewing files that changed from the base of the PR and between 184a084 and dfb46f5.

⛔ Files ignored due to path filters: 1
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing: 10
  • backend/radiance.go
  • backend/radiance_test.go
  • common/settings/settings.go
  • go.mod
  • ipc/server.go
  • ipc/server_permissions_test.go
  • peer/api.go
  • peer/peer.go
  • peer/peer_test.go
  • peer/shortids_test.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

This change adds a setting for general peer-pool serving and carries pairing short IDs and revision metadata through peer registration and heartbeats. The peer client applies and reconciles pairing IDs during startup, heartbeats, and credential rotation, and expires unconfirmed sets after 48 hours.

Changes: Peer pairing and serving settings

Layer / File(s) Summary
Peer serving setting and settings updates
common/settings/settings.go, ipc/server.go, ipc/server_permissions_test.go, backend/radiance.go, backend/radiance_test.go
The non-admin settings allowlist includes peer_serve_pool. PatchSettings validates this setting, persists other settings before applying the peer toggle, and returns persistence errors after attempting the toggle. Tests check the setting observed at peer startup.
Peer registration and heartbeat metadata
peer/api.go, peer/peer_test.go
Registration accepts an optional serve_pool value. Registration and heartbeat responses carry pairing short IDs, known-state metadata, and revisions. Heartbeat decoding tolerates empty or otherwise undecodable successful responses. Tests cover metadata decoding and request serialization.
Pairing short-ID client lifecycle
peer/peer.go, peer/shortids_test.go, peer/peer_test.go, go.mod
The client validates and applies pairing IDs, tracks revisions and confirmation time, retries failed updates, and reconciles state during credential rotation. Unconfirmed sets expire after 48 hours. Tests cover update ordering, retries, rotation, expiry, and samizdat inbound behavior. The Lantern Box and samizdat dependencies are updated.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant PeerClient
  participant PeerAPI
  participant PeerServer
  participant SamizdatInbound
  PeerClient->>PeerAPI: Register with optional serve_pool
  PeerAPI->>PeerServer: Send registration
  PeerServer-->>PeerAPI: Return pairing IDs and revision
  PeerAPI-->>PeerClient: Return registration metadata
  PeerClient->>SamizdatInbound: Apply validated pairing IDs
  PeerClient->>PeerAPI: Send heartbeat
  PeerAPI->>PeerServer: Request heartbeat
  PeerServer-->>PeerAPI: Return pairing metadata
  PeerAPI-->>PeerClient: Return heartbeat response
  PeerClient->>SamizdatInbound: Reconcile pairing IDs
Loading

Suggested reviewers: garmr-ulfr, atavism

Merge Risk: ⚪ Minimal · up to dfb46

This change adds volunteer-side support for helper-invite pairing short IDs and a peer_serve_pool setting. No concrete merge-blocking defect was found in the supplied evidence. The one open uncertainty is how the server computes revisions, which could only matter for long-lived repeated revisions.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 42.59% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 54 functions across 9 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the main change: serving helper pairing short IDs on the samizdat inbound.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 42.59% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 54 functions across 9 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ 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.

🔵 Needs a closer look

Authentication-sensitive credential revocation across concurrent heartbeats, expiry, and box rotation warrants final human validation.

No open issues 🎉

What changed in this PR

Adds volunteer-side support for helper-invite direct connections by accepting per-pairing short IDs on the samizdat inbound, alongside an option to serve only paired users.

Changes:

  • Applies authoritative pairing sets with revision ordering, retries, rotation reconciliation, and 48-hour expiry.
  • Adds peer_serve_pool to settings and owner-authorized IPC updates, persisting it before sharing starts.
  • Updates transport dependencies and adds API, lifecycle, permissions, and integration tests.
File Description
peer/​shortids_test.go Tests pairing updates, revocation, expiry, rotation, and real inbound integration.
peer/​peer.go Manages pairing short IDs and sends the pool-serving preference.
peer/​peer_test.go Extends test doubles and covers API decoding and registration preferences.
peer/​api.go Adds pairing response fields, revision decoding, and tolerant heartbeat parsing.
ipc/​server.go Allows owners to update peer_serve_pool.
ipc/​server_permissions_test.go Tests owner permission to disable pool serving.
go.sum Updates transport dependency checksums.
go.mod Bumps lantern-box and samizdat dependencies.
common/​settings/​settings.go Defines the pool-serving preference with default-true semantics.
backend/​radiance.go Persists companion settings before applying the sharing toggle.
backend/​radiance_test.go Tests that sharing starts with the updated pool preference.

Review effort: Balanced

Adam Fisk and others added 12 commits October 22, 2026 09:45
…artbeat

Register and heartbeat responses now carry pairing_short_ids and
pairing_short_ids_known. The heartbeat used to answer with an empty
body; an empty body, {} or a missing flag all decode as not known.

serve_pool is sent only as false, when the new peer_serve_pool setting
is off. Unset or true leaves it out, so the server applies its default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE
A 2xx heartbeat has already been recorded by the server, so a body that
doesn't decode now yields a not-known response instead of an error. The
shared do is strict again, so an empty register body is still an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE
A volunteer now pushes its launch config's base short ID plus the
lantern-cloud-issued pairing short IDs onto the running samizdat inbound
through lantern-box's UpdateShortIDs: at Start, after each heartbeat and
after each credential rotation.

The set is replaced only when the server marks it authoritative
(pairing_short_ids_known). An authoritative empty list revokes every
pairing. A response that does not vouch for a set, or a failed
heartbeat, keeps the current one. A rotation without an authoritative
set re-pushes the last known one onto the new box, so pairings survive
it. Short IDs and their count are never logged.

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

A box built at Start or by a rotation now gets the known pairing set
(the register response's when the server vouches for it, otherwise the
last recorded one) before Start, so it never serves base-only while
paired users reconnect. The post-swap push is gone; the swap records
the set and whether the box took it.

The client also tracks whether the current box holds the recorded set.
When a push failed, every heartbeat, including one that does not vouch
for a set, pushes the recorded set again until the box takes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE
A heartbeat can record a newer known set while a rotation builds its box
from the older snapshot. When the rotation's own register isn't
authoritative, mark the set unapplied so the next heartbeat pushes the
record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE
A rotation snapshots the pairing set (its register response's, or the
last recorded one) before building the new box. A heartbeat that
recorded a newer authoritative set during the build was then
overwritten at the swap, briefly restoring a revoked pairing.

A generation counter now bumps whenever a known set is recorded. If it
moved since the rotation's snapshot, the swap keeps the newer record and
pushes it onto the new box while still holding shortIDsMu.

Also note that peer_serve_pool takes effect at the next registration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE
PatchSettings started sharing before persisting the rest of the same
patch, so a patch that turned sharing on with peer_serve_pool=false
registered into the general pool. The other keys are now persisted
first. The toggle still applies before every other handler, and a
failed persist is still reported alongside the toggle's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE
The "help others too" preference was missing from the settings the
non-admin owner may patch, so turning it off returned 403.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE
rotateCreds read pairingGen after Register and the old box's teardown.
A heartbeat that recorded a newer set (revoking a pairing) after the
server built the register response, but before that read, was folded
into the baseline, so the stale register set won and restored the
revoked pairing.

The generation is now read before Register. The rotation then takes
shortIDsMu before choosing the set to push onto the new box, and holds
it through Start and the swap. It pushes the register set only if no
heartbeat has recorded a set since the register began, and otherwise
the recorded one. No heartbeat can record a newer set until the new box
is the one heartbeats push to, so a started box never carries a pairing
a newer heartbeat revoked.

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

A heartbeat sent before a rotation could be answered after the swap and
overwrite the newer set the rotation installed, restoring a revoked
pairing or dropping a new one. Each rotation now bumps a pairing epoch,
taking shortIDsMu and c.mu at its start. A heartbeat carries the epoch
it was sent in, and a known set from an earlier epoch is discarded.

A set the server stops vouching for (every heartbeat answering
pairing_short_ids_known=false, say from replicas without the ticket key)
was kept forever, so a pairing ended on the server was never revoked.
The client now records when a known set was last confirmed, at register
or heartbeat. Once 48 hours pass without a confirmation, the longest a
short ID is valid, each heartbeat drops the set, leaving the box with
its base ID only. A rotation that reuses the record keeps its
confirmation time. Config.Now makes the clock injectable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE
Register and heartbeat responses now carry pairing_short_ids_revision,
the server's Unix ms when it computed the set, set only with
pairing_short_ids_known. lantern-cloud's protojson sends it as a JSON
string, so it decodes from a string or a number.

A known set is applied only if its revision is strictly greater than
the last one applied (0 before any). That fixes the whole class of
races where a heartbeat computed before a rotation's register is
answered after it, before or after the swap. A known set with revision
0 is never applied, since the server sends both fields together.

This replaces the client-side ordering: the pairing generation, the
rotation epoch that discarded in-flight heartbeats, and holding
shortIDsMu from the rotation's choice of set through its build, Start
and swap. shortIDsMu now covers only recording a set and its local
push. The rotation offers its register set like a heartbeat would,
pushes the newest record onto the new box before Start, and at the swap
pushes the record again if a newer set arrived in between. A failed
push is retried, known=false keeps the set, and the 48h expiry is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE
If a rotation's register did not vouch for a set, the new box got the
retained set before Start. A heartbeat could then expire that set on
the old box before the swap, and the swap marked the new box
synchronized while it still accepted the expired IDs.

The swap now compares what the pre-Start push left on the new box with
the current record (its set, or none when no set is known), and pushes
the record when they differ. With no set left, that push is base only.
pairingApplied comes from that push. A box that does not hold the
record is re-pushed on every heartbeat, base only when no set is known,
so a failed base-only push is retried too. Start counts a box without a
known set as synchronized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqE58aYaai3u8CFZe5nXdE
@myleshorton
myleshorton force-pushed the fisk/helper-direct-mode branch from dfb46f5 to d9c9d6e Compare October 22, 2026 15:48
@myleshorton
myleshorton merged commit be1d096 into main Oct 22, 2026
3 of 4 checks passed
@myleshorton
myleshorton deleted the fisk/helper-direct-mode branch October 22, 2026 15:51
myleshorton added a commit to getlantern/lantern that referenced this pull request Oct 22, 2026
radiance 5bff222 brings soar v0.2.4 and kindling 48c13b1
(getlantern/radiance#700). A round tripper that lost a race kept its
Soar stream open, so a client that kept losing short races eventually
failed every Soar attempt with "soar: too many streams" until restart.

The range also brings getlantern/radiance#698 (peer helper direct mode).


Claude-Session: https://claude.ai/code/session_01QQFpfCXs3JqrfyDuiGabF5

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