Repository navigation
peer: serve helper pairings' short IDs on the samizdat inbound - #698
Merged
Merged
Conversation
Contributor
Contributor
There was a problem hiding this comment.
🔵 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_poolto 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
…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
force-pushed
the
fisk/helper-direct-mode
branch
from
October 22, 2026 15:48
dfb46f5 to
d9c9d6e
Compare
This was referenced Oct 22, 2026
Merged
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>
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.
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) endWhat it does
Inbound.UpdateShortIDsfrom fix: use the same value for the url test outbound and rule #376, so this PR doesn't bump it.pairing_short_ids,pairing_short_ids_knownandpairing_short_ids_revision. The revision is protojson's int64, which decodes from a string or a number.peer_serve_poolsetting, default true:serve_pool: falseonly when the setting is off; otherwise it's omitted.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, not fixed here
cmd/lantern: it doesn't compile onmaineither (ipc.NewClientsignature), 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=10under-race. The CI command (go test -tags with_clash_apion every package exceptcmd/lantern) passes.TestSamizdatInbound_RealBox).Pre-PR review (local Codex gate)
round=5 CODEX GATE: verdict=findings critical=0 important=1 minor=0 reviewed=17.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.peer/peer.go— a rotation could overwrite a newer heartbeat set (r1);backend/radiance.go—peer_serve_poolwas read before a same-patch persist (r1);ipc/server.go—peer_serve_poolwas 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).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