Add peer module + portforward for Share My Connection (PR 1/4)#458
Open
myleshorton wants to merge 6 commits into
Open
Add peer module + portforward for Share My Connection (PR 1/4)#458myleshorton wants to merge 6 commits into
myleshorton wants to merge 6 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces the new client-side peer-sharing module for Radiance: a UPnP-based port forwarder plus a peer.Client that registers a route with lantern-cloud, starts a sing-box service, renews the mapping, and maintains liveness via heartbeats.
Changes:
- Adds
portforwardto discover IGD gateways, create/remove TCP mappings, renew leases, and query the external IP. - Adds
peersession orchestration and a thin HTTP client for/v1/peer/{register,heartbeat,deregister}. - Adds focused unit tests for both packages and promotes
github.com/huin/goupnpto a direct dependency.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
portforward/portforward.go |
Implements UPnP discovery, mapping lifecycle, renewal loop, external IP lookup, and context-wrapping helpers. |
portforward/portforward_test.go |
Adds unit coverage for mapping, unmapping, renewal, context cancellation, and external IP behavior. |
peer/peer.go |
Implements the peer session lifecycle, heartbeat loop, status tracking, and sing-box startup/teardown. |
peer/peer_test.go |
Adds tests for happy path, failure unwinding, idempotent stop, and heartbeat-driven shutdown behavior. |
peer/api.go |
Adds the lantern-cloud peer API client and error wrapper types. |
go.mod |
Promotes goupnp to a direct dependency for the new port forwarding package. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
3 tasks
myleshorton
pushed a commit
that referenced
this pull request
May 5, 2026
Five Copilot comments on #458 flagged real lifecycle / concurrency bugs in the original PR 1 implementation. 1) peer.go:103 — Start checked c.active under the lock then released it before doing setup. Two concurrent Starts could both pass the check, both run MapPort/Register/box.Start, and the second's state would overwrite the first's, orphaning a registered route + open box that this Client could no longer Stop. Added a starting flag that's set under the lock alongside the active check, so any second Start while the first is in flight is rejected. 2) peer.go:127 / 145 — Rollback after MapPort, ExternalIP, Register, BuildBoxService, or box.Start failures all reused the caller's ctx. If the caller's ctx had already timed out or been cancelled, the Deregister and UnmapPort calls in the rollback would also abort immediately, leaking the registered route + router rule. Replaced the inline rollbacks with a single defer that runs against a fresh peerCleanupTimeout-bounded Background context, so cleanup always gets a live deadline. 3) portforward.go:234 — runWithCtx started fn even when the caller's ctx was already canceled, only stopping the wait. The goroutine would still run AddPortMapping or DeletePortMapping in the background, creating side effects after the caller had given up. Added a ctx.Err() check at the top so an already-canceled ctx returns immediately without spawning the goroutine. 4) portforward.go:128 — UnmapPort cleared f.mapping before DeletePortMapping succeeded. A failed delete (gateway momentarily unavailable, ctx expired, etc.) would leave the Forwarder "forgetting" about a router rule that was actually still live, so the caller couldn't retry the unmap and the user would have to wait for the UPnP lease to expire. Moved the f.mapping = nil to after the delete returns nil. Test coverage: * New TestClient_Start_ConcurrentStartsAreSerialized exercises the race fixed by issue 1: spawn two Starts, gate the first inside MapPort, release the second to observe the rejection, assert exactly one succeeds and exactly one returns "already active". * Existing tests for the rollback paths (PortForward / ExternalIP / Register / BoxStart failures) still pass — the cleanup defer takes the same shape as before but now uses a fresh ctx. go test -race ./peer/... ./portforward/... and golangci-lint --new-from-rev=origin/main both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
myleshorton
pushed a commit
that referenced
this pull request
May 5, 2026
PR 2 of 4 stacked on PR 1 (peer module + portforward, #458). * common/settings: add PeerShareEnabledKey bool. * peer.Client: emit StatusEvent on Start success and Stop completion so subscribers (the new IPC SSE handler) can drive UI without polling. * backend.LocalBackend: own a peerController (interface seam over peer.Client) constructed in NewLocalBackend with kindling's HTTP client + the lantern-cloud base URL + the device ID. * PatchSettings dispatch: PeerShareEnabledKey changes route to applyPeerShare(enabled). Toggle calls are serialized by peerToggleMu so a fast off→on→off can't see the second call's "already active" rollback racing the third call's Stop. Start runs against a 30s deadline so a slow router can't block the IPC response indefinitely. On Start failure the persisted setting is rolled back so reads of PeerShareEnabledKey reflect runtime state and the Dart toggle can surface the error. * Auto-resume: if PeerShareEnabledKey is true at LocalBackend.Start(), kick off Start in a goroutine tracked by peerWG. Close() waits for peerWG before tearing down ctx, so an in-flight resume can't leave a registered route + open box behind on shutdown. * Close: if peerClient.IsActive() after the WG settles, Stop with a fresh context so Deregister has a live HTTP deadline. * IPC: new GET /peer/status (snapshot) and GET /peer/status/events (SSE). The SSE handler replays the current snapshot on connect. Tests cover applyPeerShare's three branches (enable, disable, Start failure rolls back the setting), the resume-if-enabled path, the Close-waits-for-resume + Stop-active-peer race, and the PatchSettings dispatch wiring (a typo on the diff key would silently break the toggle without it). peer_test adds a Subscribe-and-assert test for StatusEvent emission on both edges. go test -race ./peer/... ./backend/... ./common/settings/... ./ipc/... golangci-lint run --new-from-rev=origin/main both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
myleshorton
pushed a commit
that referenced
this pull request
May 5, 2026
PR 2 of 4 stacked on PR 1 (peer module + portforward, #458). * common/settings: add PeerShareEnabledKey bool. * peer.Client: emit StatusEvent on Start success and Stop completion so subscribers (the new IPC SSE handler) can drive UI without polling. * backend.LocalBackend: own a peerController (interface seam over peer.Client) constructed in NewLocalBackend with kindling's HTTP client + the lantern-cloud base URL + the device ID. * PatchSettings dispatch: PeerShareEnabledKey changes route to applyPeerShare(enabled). Toggle calls are serialized by peerToggleMu so a fast off→on→off can't see the second call's "already active" rollback racing the third call's Stop. Start runs against a 30s deadline so a slow router can't block the IPC response indefinitely. On Start failure the persisted setting is rolled back so reads of PeerShareEnabledKey reflect runtime state and the Dart toggle can surface the error. * Auto-resume: if PeerShareEnabledKey is true at LocalBackend.Start(), kick off Start in a goroutine tracked by peerWG. Close() waits for peerWG before tearing down ctx, so an in-flight resume can't leave a registered route + open box behind on shutdown. * Close: if peerClient.IsActive() after the WG settles, Stop with a fresh context so Deregister has a live HTTP deadline. * IPC: new GET /peer/status (snapshot) and GET /peer/status/events (SSE). The SSE handler replays the current snapshot on connect. Tests cover applyPeerShare's three branches (enable, disable, Start failure rolls back the setting), the resume-if-enabled path, the Close-waits-for-resume + Stop-active-peer race, and the PatchSettings dispatch wiring (a typo on the diff key would silently break the toggle without it). peer_test adds a Subscribe-and-assert test for StatusEvent emission on both edges. go test -race ./peer/... ./backend/... ./common/settings/... ./ipc/... golangci-lint run --new-from-rev=origin/main both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This was referenced May 28, 2026
myleshorton
pushed a commit
that referenced
this pull request
May 28, 2026
Five Copilot comments on #458 flagged real lifecycle / concurrency bugs in the original PR 1 implementation. 1) peer.go:103 — Start checked c.active under the lock then released it before doing setup. Two concurrent Starts could both pass the check, both run MapPort/Register/box.Start, and the second's state would overwrite the first's, orphaning a registered route + open box that this Client could no longer Stop. Added a starting flag that's set under the lock alongside the active check, so any second Start while the first is in flight is rejected. 2) peer.go:127 / 145 — Rollback after MapPort, ExternalIP, Register, BuildBoxService, or box.Start failures all reused the caller's ctx. If the caller's ctx had already timed out or been cancelled, the Deregister and UnmapPort calls in the rollback would also abort immediately, leaking the registered route + router rule. Replaced the inline rollbacks with a single defer that runs against a fresh peerCleanupTimeout-bounded Background context, so cleanup always gets a live deadline. 3) portforward.go:234 — runWithCtx started fn even when the caller's ctx was already canceled, only stopping the wait. The goroutine would still run AddPortMapping or DeletePortMapping in the background, creating side effects after the caller had given up. Added a ctx.Err() check at the top so an already-canceled ctx returns immediately without spawning the goroutine. 4) portforward.go:128 — UnmapPort cleared f.mapping before DeletePortMapping succeeded. A failed delete (gateway momentarily unavailable, ctx expired, etc.) would leave the Forwarder "forgetting" about a router rule that was actually still live, so the caller couldn't retry the unmap and the user would have to wait for the UPnP lease to expire. Moved the f.mapping = nil to after the delete returns nil. Test coverage: * New TestClient_Start_ConcurrentStartsAreSerialized exercises the race fixed by issue 1: spawn two Starts, gate the first inside MapPort, release the second to observe the rejection, assert exactly one succeeds and exactly one returns "already active". * Existing tests for the rollback paths (PortForward / ExternalIP / Register / BoxStart failures) still pass — the cleanup defer takes the same shape as before but now uses a fresh ctx. go test -race ./peer/... ./portforward/... and golangci-lint --new-from-rev=origin/main both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
edaf357 to
c576b48
Compare
myleshorton
pushed a commit
that referenced
this pull request
May 28, 2026
PR 2 of 4 stacked on PR 1 (peer module + portforward, #458). * common/settings: add PeerShareEnabledKey bool. * peer.Client: emit StatusEvent on Start success and Stop completion so subscribers (the new IPC SSE handler) can drive UI without polling. * backend.LocalBackend: own a peerController (interface seam over peer.Client) constructed in NewLocalBackend with kindling's HTTP client + the lantern-cloud base URL + the device ID. * PatchSettings dispatch: PeerShareEnabledKey changes route to applyPeerShare(enabled). Toggle calls are serialized by peerToggleMu so a fast off→on→off can't see the second call's "already active" rollback racing the third call's Stop. Start runs against a 30s deadline so a slow router can't block the IPC response indefinitely. On Start failure the persisted setting is rolled back so reads of PeerShareEnabledKey reflect runtime state and the Dart toggle can surface the error. * Auto-resume: if PeerShareEnabledKey is true at LocalBackend.Start(), kick off Start in a goroutine tracked by peerWG. Close() waits for peerWG before tearing down ctx, so an in-flight resume can't leave a registered route + open box behind on shutdown. * Close: if peerClient.IsActive() after the WG settles, Stop with a fresh context so Deregister has a live HTTP deadline. * IPC: new GET /peer/status (snapshot) and GET /peer/status/events (SSE). The SSE handler replays the current snapshot on connect. Tests cover applyPeerShare's three branches (enable, disable, Start failure rolls back the setting), the resume-if-enabled path, the Close-waits-for-resume + Stop-active-peer race, and the PatchSettings dispatch wiring (a typo on the diff key would silently break the toggle without it). peer_test adds a Subscribe-and-assert test for StatusEvent emission on both edges. go test -race ./peer/... ./backend/... ./common/settings/... ./ipc/... golangci-lint run --new-from-rev=origin/main both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
myleshorton
added a commit
that referenced
this pull request
May 29, 2026
Five fixes from #458 (comment): 1. portforward.NewForwarder: when ctx is canceled/expired during discovery, propagate the ctx error instead of masking it as ErrNoPortForwarding. Callers can now distinguish 'this network can't host a peer' from 'we ran out of time, retry later'. 2. portforward.MapPort: when the gateway refuses a mapping (non-ctx error), wrap with errors.Join(ErrNoPortForwarding, err) so callers can detect the documented case via errors.Is while keeping the underlying router- specific error for diagnostics. 3. portforward.localIP: fall back to enumerating interfaces if the net.Dial("udp", "8.8.8.8:53") trick fails. Covers IPv6-only hosts and networks that block outbound to 8.8.8.8. 4. portforward.discoverIGDv1: also probe WANPPPConnection (PPPoE/DSL routers), not just WANIPConnection. Many consumer DSL CPEs only expose UPnP via WANPPPConnection. 5. peer.Stop: wait for any in-flight Start to finish before checking active. Without this, a Stop arriving while starting=true returns nil and the racing Start leaves the client active afterward — the exact orphaned-session shape Start's rollback path is designed to prevent. Wait honors ctx so a cancellable caller still has an exit door. Tests added: - TestForwarder_MapPort_GatewayErrorWrapsErrNoPortForwarding - TestLocalIPByInterfaceScan - TestClient_Stop_WaitsForInflightStart - TestClient_Stop_RespectsCtxWhileWaitingForStart All pre-existing tests pass under -race. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR 1 of 4 stacked PRs implementing the radiance side of "Share My
Connection" (peer-proxy). This PR introduces a self-contained peer
module — wiring into LocalBackend / settings / FFI lands in PRs 2-4.
* portforward: UPnP IGDv2 with IGDv1 fallback (huin/goupnp). Forwarder
exposes MapPort, UnmapPort, StartRenewal (50%-of-lease cadence,
1-min floor), and ExternalIP. Each goupnp call is wrapped in a
ctx-respecting helper so an unresponsive gateway can't block the
caller past its deadline.
* peer.Client: orchestrates one session — open UPnP port → fetch
external IP → register with lantern-cloud → start a second sing-box
instance with the server-supplied config → run the heartbeat loop.
Stop deregisters, closes the box, unmaps the port, and continues
past individual failures so partial state never lingers. The box's
lifetime ctx is derived from Background (not the Start caller's
ctx) so a short-lived Start ctx doesn't kill it.
* peer.API: thin HTTP client for /v1/peer/{register,heartbeat,
deregister}. X-Lantern-Device-Id is sent on every request so the
server can owner-gate.
* heartbeatLoop auto-stops on a 404 from the server (registration
reaped or wrong owner). Stop runs in a separate goroutine to avoid
the cyclic Stop → cancelRun → loop-exit deadlock.
Tests cover the happy path, every failure phase (port-forward,
external-IP, register, sing-box build, sing-box start), Stop
idempotency, Stop continuing past individual errors, the 404
auto-stop path, and the transient-error stays-running path. portforward
gets fake-IGD coverage including ctx-cancellation. go test -race and
golangci-lint are clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five Copilot comments on #458 flagged real lifecycle / concurrency bugs in the original PR 1 implementation. 1) peer.go:103 — Start checked c.active under the lock then released it before doing setup. Two concurrent Starts could both pass the check, both run MapPort/Register/box.Start, and the second's state would overwrite the first's, orphaning a registered route + open box that this Client could no longer Stop. Added a starting flag that's set under the lock alongside the active check, so any second Start while the first is in flight is rejected. 2) peer.go:127 / 145 — Rollback after MapPort, ExternalIP, Register, BuildBoxService, or box.Start failures all reused the caller's ctx. If the caller's ctx had already timed out or been cancelled, the Deregister and UnmapPort calls in the rollback would also abort immediately, leaking the registered route + router rule. Replaced the inline rollbacks with a single defer that runs against a fresh peerCleanupTimeout-bounded Background context, so cleanup always gets a live deadline. 3) portforward.go:234 — runWithCtx started fn even when the caller's ctx was already canceled, only stopping the wait. The goroutine would still run AddPortMapping or DeletePortMapping in the background, creating side effects after the caller had given up. Added a ctx.Err() check at the top so an already-canceled ctx returns immediately without spawning the goroutine. 4) portforward.go:128 — UnmapPort cleared f.mapping before DeletePortMapping succeeded. A failed delete (gateway momentarily unavailable, ctx expired, etc.) would leave the Forwarder "forgetting" about a router rule that was actually still live, so the caller couldn't retry the unmap and the user would have to wait for the UPnP lease to expire. Moved the f.mapping = nil to after the delete returns nil. Test coverage: * New TestClient_Start_ConcurrentStartsAreSerialized exercises the race fixed by issue 1: spawn two Starts, gate the first inside MapPort, release the second to observe the rejection, assert exactly one succeeds and exactly one returns "already active". * Existing tests for the rollback paths (PortForward / ExternalIP / Register / BoxStart failures) still pass — the cleanup defer takes the same shape as before but now uses a fresh ctx. go test -race ./peer/... ./portforward/... and golangci-lint --new-from-rev=origin/main both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
config/fetcher.go forwards FeatureOverridesKey (RADIANCE_FEATURE_OVERRIDES) as X-Lantern-Feature-Override on /config-new requests so QA can flip features on ahead of public rollout. peer.API.do only sent X-Lantern-Device-Id, so even with the override set the server-side gate rejected the peer register/heartbeat/deregister endpoints. Forward the same header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the peer-share user has Lantern VPN running, its sing-box installs a TUN (utun225) with auto_route=true that captures all outbound traffic on the host. Without intervention, the peer's sing-box (a separate libbox instance) would dial destination addresses through the OS routing table — which now points at utun225 — so the censored client's traffic would egress through the local user's Lantern proxy instead of their residential connection. That defeats the whole point of peer-sharing (use the user's home IP as a circumvention exit) and double-bills bandwidth through Lantern infra. Splice route.auto_detect_interface=true into the server-supplied sing-box options before handing them to libbox.NewServiceWithContext. sing-box's interface monitor picks the underlying physical iface (en0/wlan0) rather than any TUN, and binds outbound dials directly to it — bypassing the VPN TUN entirely. The bypass is applied client-side rather than server-side because it's a property of the client's environment (whether the user has a TUN VPN running), not the proxy track config. Setting it unconditionally is safe — when no TUN is present, auto_detect just picks the same default interface the OS would have chosen anyway. Tests cover the three branches: no route block in the input, an existing route block (other fields preserved), and malformed JSON. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five fixes from #458 (comment): 1. portforward.NewForwarder: when ctx is canceled/expired during discovery, propagate the ctx error instead of masking it as ErrNoPortForwarding. Callers can now distinguish 'this network can't host a peer' from 'we ran out of time, retry later'. 2. portforward.MapPort: when the gateway refuses a mapping (non-ctx error), wrap with errors.Join(ErrNoPortForwarding, err) so callers can detect the documented case via errors.Is while keeping the underlying router- specific error for diagnostics. 3. portforward.localIP: fall back to enumerating interfaces if the net.Dial("udp", "8.8.8.8:53") trick fails. Covers IPv6-only hosts and networks that block outbound to 8.8.8.8. 4. portforward.discoverIGDv1: also probe WANPPPConnection (PPPoE/DSL routers), not just WANIPConnection. Many consumer DSL CPEs only expose UPnP via WANPPPConnection. 5. peer.Stop: wait for any in-flight Start to finish before checking active. Without this, a Stop arriving while starting=true returns nil and the racing Start leaves the client active afterward — the exact orphaned-session shape Start's rollback path is designed to prevent. Wait honors ctx so a cancellable caller still has an exit door. Tests added: - TestForwarder_MapPort_GatewayErrorWrapsErrNoPortForwarding - TestLocalIPByInterfaceScan - TestClient_Stop_WaitsForInflightStart - TestClient_Stop_RespectsCtxWhileWaitingForStart All pre-existing tests pass under -race. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
99c9b51 to
b19243b
Compare
myleshorton
pushed a commit
that referenced
this pull request
May 29, 2026
PR 2 of 4 stacked on PR 1 (peer module + portforward, #458). * common/settings: add PeerShareEnabledKey bool. * peer.Client: emit StatusEvent on Start success and Stop completion so subscribers (the new IPC SSE handler) can drive UI without polling. * backend.LocalBackend: own a peerController (interface seam over peer.Client) constructed in NewLocalBackend with kindling's HTTP client + the lantern-cloud base URL + the device ID. * PatchSettings dispatch: PeerShareEnabledKey changes route to applyPeerShare(enabled). Toggle calls are serialized by peerToggleMu so a fast off→on→off can't see the second call's "already active" rollback racing the third call's Stop. Start runs against a 30s deadline so a slow router can't block the IPC response indefinitely. On Start failure the persisted setting is rolled back so reads of PeerShareEnabledKey reflect runtime state and the Dart toggle can surface the error. * Auto-resume: if PeerShareEnabledKey is true at LocalBackend.Start(), kick off Start in a goroutine tracked by peerWG. Close() waits for peerWG before tearing down ctx, so an in-flight resume can't leave a registered route + open box behind on shutdown. * Close: if peerClient.IsActive() after the WG settles, Stop with a fresh context so Deregister has a live HTTP deadline. * IPC: new GET /peer/status (snapshot) and GET /peer/status/events (SSE). The SSE handler replays the current snapshot on connect. Tests cover applyPeerShare's three branches (enable, disable, Start failure rolls back the setting), the resume-if-enabled path, the Close-waits-for-resume + Stop-active-peer race, and the PatchSettings dispatch wiring (a typo on the diff key would silently break the toggle without it). peer_test adds a Subscribe-and-assert test for StatusEvent emission on both edges. go test -race ./peer/... ./backend/... ./common/settings/... ./ipc/... golangci-lint run --new-from-rev=origin/main both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three honest-comment / honest-behavior fixes from #458 (comment) : 1. peer.go: heartbeat interval. The previous clamp bumped any server- supplied value below 1 minute up to 5 minutes, which would defeat the server's intent if it deliberately picked a short interval to reap stale registrations faster. Now: honor any positive value verbatim, only fall back to 5m when the field is non-positive (unset / older server / JSON omitted). 2. portforward.go: rewrote the StartRenewal comment. It used to claim the goroutine 'prevents routers from dropping the mapping when they silently assign a shorter TTL', but the renewal cadence is keyed off the *requested* lease (the only value we know) — UPnP IGD has no API to query the router-assigned lease. A router that silently shortens the TTL can still drop the mapping; the peer heartbeat path catches that and auto-Stops. The comment now describes what actually happens. 3. peer.go: rewrote the port-range comment. The old wording claimed '30000–50000 avoids well-known/registered ports and the OS ephemeral range' — but 30000–50000 overlaps both the IANA registered range (1024–49151) AND the Linux ephemeral range (default starts at 32768). The new comment is honest about that: the range minimizes collisions on the typical home network but doesn't guarantee zero, and the AddPortMapping conflict path is the safety net. No behavior change in #2 or #3 — only #1 actually changes runtime behavior, and only for short-interval server responses. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
myleshorton
pushed a commit
that referenced
this pull request
May 29, 2026
PR 2 of 4 stacked on PR 1 (peer module + portforward, #458). * common/settings: add PeerShareEnabledKey bool. * peer.Client: emit StatusEvent on Start success and Stop completion so subscribers (the new IPC SSE handler) can drive UI without polling. * backend.LocalBackend: own a peerController (interface seam over peer.Client) constructed in NewLocalBackend with kindling's HTTP client + the lantern-cloud base URL + the device ID. * PatchSettings dispatch: PeerShareEnabledKey changes route to applyPeerShare(enabled). Toggle calls are serialized by peerToggleMu so a fast off→on→off can't see the second call's "already active" rollback racing the third call's Stop. Start runs against a 30s deadline so a slow router can't block the IPC response indefinitely. On Start failure the persisted setting is rolled back so reads of PeerShareEnabledKey reflect runtime state and the Dart toggle can surface the error. * Auto-resume: if PeerShareEnabledKey is true at LocalBackend.Start(), kick off Start in a goroutine tracked by peerWG. Close() waits for peerWG before tearing down ctx, so an in-flight resume can't leave a registered route + open box behind on shutdown. * Close: if peerClient.IsActive() after the WG settles, Stop with a fresh context so Deregister has a live HTTP deadline. * IPC: new GET /peer/status (snapshot) and GET /peer/status/events (SSE). The SSE handler replays the current snapshot on connect. Tests cover applyPeerShare's three branches (enable, disable, Start failure rolls back the setting), the resume-if-enabled path, the Close-waits-for-resume + Stop-active-peer race, and the PatchSettings dispatch wiring (a typo on the diff key would silently break the toggle without it). peer_test adds a Subscribe-and-assert test for StatusEvent emission on both edges. go test -race ./peer/... ./backend/... ./common/settings/... ./ipc/... golangci-lint run --new-from-rev=origin/main both clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
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.
First of four stacked PRs implementing the radiance client side of Share My Connection (the peer-proxy feature). Server side is shipping in lantern-cloud (#2678 schema, #2679 endpoints, #2680 verifier+reaper, #2681 tests).
This PR is the self-contained peer module. No
LocalBackendwiring, no settings dispatch, no FFI yet — those land in PRs 2-4.What's in here
radiance/portforward/UPnP IGDv2 with IGDv1 fallback via
huin/goupnp. Public API:NewForwarder(ctx) (*Forwarder, error)— discovers the local gatewayMapPort(ctx, internalPort, description) (*Mapping, error)UnmapPort(ctx) errorStartRenewal(ctx)— re-issuesAddPortMappingat 50% lease (min 1 min)ExternalIP(ctx) (string, error)— queries the gateway directlyLocalIP() (string, error)— UDP-dial trickEvery blocking goupnp call is wrapped in a ctx-respecting helper so an unresponsive gateway cannot block the caller past its deadline.
radiance/peer/peer.Clientorchestrates one session:runCtxis derived fromcontext.Background()(not the Start caller's ctx) so a short-lived Start ctx doesn't kill the box.Stop()to avoid the cyclicStop → cancelRun → loop-exitdeadlock.peer.APIis the thin HTTP client for/v1/peer/{register,heartbeat,deregister}.X-Lantern-Device-Idis sent on every request so the server can owner-gate.Tests
go test -race ./peer/... ./portforward/...andgolangci-lint runare both clean.peerportforwardConfig.HeartbeatIntervalexists so the 404 auto-stop test can drive the loop at 50ms instead of waiting a full minute.Out of scope (PR 2-4)
peer.ClientintoLocalBackendvia a newPeerShareEnabledKeysetting and emit lifecycle events.setPeerProxyEnabledFFI export inlantern-core/ffi/.setPeerProxyrewrite inapp_setting_notifier.dart(mirrorsetBlockAdsrollback pattern) + optional status indicator.Test plan
go test -race ./peer/... ./portforward/...golangci-lint run --timeout 3m ./peer/... ./portforward/...go vet ./peer/... ./portforward/...🤖 Generated with Claude Code