Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
83be9ba
Add peer-share toggle to lantern-core (Share My Connection PR 3/4)
May 5, 2026
b742d9b
Wire Share My Connection toggle in Dart UI (PR 4/4)
May 5, 2026
2c44a3f
review: gate peer-proxy toggle to FFI-supported platforms
May 5, 2026
05473d6
peer-proxy: add macOS native handler
May 5, 2026
5561069
Prototype: unified Share My Connection screen with globe + SmC disclo…
May 7, 2026
ba77c8d
prototype: globe wasn't visible — restore MediaQuery override + ClipRect
May 7, 2026
e3bfff8
prototype: use SwitchButton to match the rest of the app's toggles
May 7, 2026
32402ef
prototype: nudge the globe up — alignment(0, 0.1) → (0, -0.1)
May 7, 2026
0ff7108
prototype: replace mock event timer with poll of radiance peer stats …
May 7, 2026
aa56a5e
Stream peer-connection events from radiance to Flutter via the existing
May 7, 2026
2f03640
Advanced section in Share My Connection: manual port forward setting
May 7, 2026
8f5ced9
Unbounded fully wired through to the SmC UI's "Basic mode"
May 8, 2026
566d789
macOS: wire setPeerManualPort + setUnboundedEnabled through MethodCha…
May 8, 2026
4cd8d9c
share-my-connection: toggle honors Advanced manual port before UPnP p…
May 8, 2026
0d36530
mobile: sanitize errors before returning to gomobile bridge
May 8, 2026
e51e416
share-my-connection: surface radiance peer phase events to the UI
May 11, 2026
a620404
core: instrument peer-connection subscriber to pair with radiance bre…
May 11, 2026
beb020d
core: log listenPeerConnectionEvents goroutine entry
May 11, 2026
71342de
core: consume peer events over IPC SSE instead of in-process events.S…
May 11, 2026
7dcf3d1
SmC: real per-peer geo, on-globe heart burst, arc reversal
May 12, 2026
a190859
SmC: lift heart-burst off the globe into a floating toast
May 12, 2026
50d9bf7
deps: bump radiance to #501 tip + lantern-box to #255 tip; drop local…
myleshorton May 29, 2026
ac77f75
smc: address Copilot review (5 of 7)
myleshorton May 31, 2026
281d5b7
smc: wire real UPnP / IGD probe via FFI + MethodChannel
myleshorton May 31, 2026
5258c7d
smc: i18n the 'On — tap to view' subtitle in VPN settings
myleshorton May 31, 2026
967c201
macos: wire probeUPnP MethodChannel handler
myleshorton May 31, 2026
015cc1f
mobile: wire SmC + Unbounded MethodChannel handlers for Android & iOS
myleshorton May 31, 2026
0631608
smc: address Copilot review on #8819 round-N
myleshorton Jun 1, 2026
4bb5b52
smc: tighten _extractIP for bare IPv6 + check Unbounded enable Either
myleshorton Jun 1, 2026
f7adec5
smc: render off-with-error + check SmC enable Either + cache peer geo
myleshorton Jun 1, 2026
0bcd729
smc: list-spread + dispose guards + terminal-phase reset + docs
myleshorton Jun 1, 2026
24abae2
smc: MediaQuery copyWith + symmetric wire format + drop stale fromJson
myleshorton Jun 1, 2026
690494b
smc: gate arc draws on origin coords + accurate event type doc
myleshorton Jun 1, 2026
7d03f48
smc: defer subscription cleanup + strict arg validation across handlers
myleshorton Jun 1, 2026
95c1b5a
smc: _resolveAndEmit isClosed guard + honest 'session' stat label
myleshorton Jun 1, 2026
2b553c7
smc: keep-alive ShareNotifier + reflect Unbounded in VPN tile
myleshorton Jun 1, 2026
34e64f1
smc: revise defer Unsubscribe comment to match actual lifecycle
myleshorton Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ enum class Methods(val method: String) {
SetRoutingMode("setRoutingMode"),
IsSmartRoutingEnabled("isSmartRoutingEnabled"),

// Share My Connection (samizdat) + Unbounded (broflake) peer-share
SetPeerProxyEnabled("setPeerProxyEnabled"),
IsPeerProxyEnabled("isPeerProxyEnabled"),
SetPeerManualPort("setPeerManualPort"),
GetPeerManualPort("getPeerManualPort"),
SetUnboundedEnabled("setUnboundedEnabled"),
IsUnboundedEnabled("isUnboundedEnabled"),
ProbeUPnP("probeUPnP"),

// Telemetry
IsTelemetryEnabled("isTelemetryEnabled"),

Expand Down Expand Up @@ -1221,6 +1230,66 @@ class MethodHandler : FlutterPlugin,
}
}

// Share My Connection (samizdat) + Unbounded (broflake)
// peer-share. Phones on mobile data won't have UPnP, but
// home WiFi does — manual port forwarding works there too,
// so the whole stack is exposed on Android rather than
// desktop-only.
Methods.SetPeerProxyEnabled.method -> {
scope.handleResult(result, "set_peer_proxy_enabled") {
val enabled = call.argument<Boolean>("enabled")
?: error("Missing enabled")
Mobile.setPeerShareEnabled(enabled)
}
}

Methods.IsPeerProxyEnabled.method -> {
scope.handleValue(result, "is_peer_proxy_enabled") {
Mobile.isPeerShareEnabled()
}
}

Methods.SetPeerManualPort.method -> {
scope.handleResult(result, "set_peer_manual_port") {
// error() surfaces the failure rather than silently
// defaulting to 0 — which would clear the user's
// manual port override on caller bugs. Matches the
// SetPeerProxyEnabled pattern above.
val port = call.argument<Int>("port") ?: error("Missing port")
Mobile.setPeerManualPort(port.toLong())
}
}
Comment thread
myleshorton marked this conversation as resolved.

Methods.GetPeerManualPort.method -> {
scope.handleValue(result, "get_peer_manual_port") {
Mobile.getPeerManualPort().toInt()
}
}

Methods.SetUnboundedEnabled.method -> {
scope.handleResult(result, "set_unbounded_enabled") {
val enabled = call.argument<Boolean>("enabled")
?: error("Missing enabled")
Mobile.setUnboundedEnabled(enabled)
}
}

Methods.IsUnboundedEnabled.method -> {
scope.handleValue(result, "is_unbounded_enabled") {
Mobile.isUnboundedEnabled()
}
}

Methods.ProbeUPnP.method -> {
// UPnP M-SEARCH waits up to ~6s on multicast replies;
// handleValue is already coroutine-backed (Dispatchers.IO
// via scope), so the wait runs off the main thread and
// doesn't block the Flutter UI isolate.
scope.handleValue(result, "probe_upnp") {
Mobile.probeUPnP()
}
}

Methods.IsTelemetryEnabled.method -> {
scope.handleValue(result, "is_telemetry_enabled") {
Mobile.isTelemetryEnabled()
Expand Down
123 changes: 123 additions & 0 deletions assets/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,129 @@ msgstr "Block Ads"
msgid "only_active"
msgstr "Only active when VPN is connected"

msgid "share_my_connection"
msgstr "Share My Connection"

msgid "share_my_connection_subtitle"
msgstr "Let other Lantern users route through your connection to bypass censorship."

msgid "share_my_connection_on_tap_to_view"
msgstr "On — tap to view"

# Share My Connection screen — body / hero copy
msgid "smc_intro"
msgstr "Help others bypass censorship by sharing a small portion of your home internet connection. While sharing is on, traffic from users in censored regions will egress through your IP."

# Status card — phase labels
msgid "smc_status_label"
msgstr "Status"

msgid "smc_status_off"
msgstr "Off"

msgid "smc_status_probing"
msgstr "Probing your network…"

msgid "smc_status_active_unbounded"
msgstr "Active — sharing via Unbounded (WebRTC)"

msgid "smc_status_active_smc"
msgstr "Active — sharing via Share My Connection (residential proxy)"

msgid "smc_status_mapping_port"
msgstr "Opening port on your router…"

msgid "smc_status_detecting_ip"
msgstr "Detecting your public IP…"

msgid "smc_status_registering"
msgstr "Registering with Lantern…"

msgid "smc_status_starting_proxy"
msgstr "Starting local proxy…"

msgid "smc_status_verifying"
msgstr "Verifying connectivity…"

msgid "smc_status_serving"
msgstr "Sharing — ready to serve users in censored regions"

msgid "smc_status_stopping"
msgstr "Stopping…"

msgid "smc_status_error_with_message"
msgstr "Couldn't share: %s"

msgid "smc_status_error_generic"
msgstr "Couldn't share — try toggling again"

# Status card — stats + tooltip
msgid "smc_stat_active_now"
msgstr "Active now"

msgid "smc_stat_total_today"
msgstr "Total this session"

Comment thread
myleshorton marked this conversation as resolved.
msgid "smc_connections_tooltip"
msgstr "Most connections are short liveness probes — Lantern clients periodically check that this peer is reachable before sending real traffic. A quick burst from many locations is normal; an arc that lingers represents an actual user session."

# Arrival toast — "New connection from {country}"
msgid "smc_arrival_toast"
msgstr "New connection from %s"

# Advanced section / manual port forward
msgid "smc_advanced"
msgstr "Advanced"

msgid "smc_advanced_subtitle"
msgstr "For users whose router doesn't support UPnP"

msgid "smc_manual_port"
msgstr "Manual port forward"

msgid "smc_manual_port_description"
msgstr "If your router doesn't support UPnP, configure a port forward on your router and enter the port number here. Lantern will use it as the external port instead of probing UPnP. Leave blank to use UPnP (default)."

msgid "smc_manual_port_label"
msgstr "Port"

msgid "smc_manual_port_hint"
msgstr "e.g. 5698"

msgid "smc_manual_port_save"
msgstr "Save"

msgid "smc_manual_port_currently_set"
msgstr "Currently set to port %d. Toggle Share My Connection off and back on for the change to take effect."

msgid "smc_manual_port_out_of_range"
msgstr "Port must be between 1 and 65535"

msgid "smc_manual_port_cleared"
msgstr "Manual port cleared — using UPnP"

msgid "smc_manual_port_saved"
msgstr "Manual port set to %d"

# SmC disclosure dialog (shown before opting into the residential-proxy mode)
msgid "smc_disclosure_title"
msgstr "Use full Share My Connection?"

msgid "smc_disclosure_body_capability"
msgstr "Your network supports the higher-bandwidth, more block-resistant mode. In this mode, your home internet connection routes traffic for users in censored countries."

msgid "smc_disclosure_body_safety"
msgstr "Lantern blocks abuse destinations, rotates credentials, and operates as a \"mere conduit\" under DMCA § 512(a) — but your IP address will appear in the destination's logs while you're sharing."

msgid "smc_disclosure_body_alternative"
msgstr "You can still help by selecting \"Basic mode\" instead, which uses ephemeral WebRTC connections that are not tied to your IP in the same way."

msgid "smc_disclosure_basic"
msgstr "Basic mode (Unbounded)"

msgid "smc_disclosure_full"
msgstr "Full mode (SmC)"

msgid "vpn_connected"
msgstr "Lantern is now connected."

Expand Down
1 change: 1 addition & 0 deletions assets/unbounded/explosion.json

Large diffs are not rendered by default.

Binary file added assets/unbounded/uv-map-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/unbounded/uv-map.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ module github.com/getlantern/lantern

go 1.26.2

// replace github.com/getlantern/radiance => ../radiance

// replace github.com/getlantern/lantern-server-provisioner => ../lantern-server-provisioner

// replace github.com/sagernet/sing-box => ../sing-box-minimal
Expand All @@ -25,7 +23,7 @@ replace github.com/quic-go/qpack => github.com/quic-go/qpack v0.5.1
require (
github.com/alecthomas/assert/v2 v2.3.0
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9
github.com/getlantern/radiance v0.0.0-20260529193818-b9df7d613b1a
github.com/getlantern/radiance v0.0.0-20260531221356-11aa55a6ff16
github.com/sagernet/sing-box v1.12.22
golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0
golang.org/x/sys v0.41.0
Expand Down Expand Up @@ -172,7 +170,7 @@ require (
github.com/getlantern/domainfront v0.0.0-20260419161617-0bff0b2169f4 // indirect
github.com/getlantern/keepcurrent v0.0.0-20260422161259-54a4d9a93694 // indirect
github.com/getlantern/kindling v0.0.0-20260529141244-21f8b144afab // indirect
github.com/getlantern/lantern-box v0.0.86 // indirect
github.com/getlantern/lantern-box v0.0.87-0.20260529195337-0b63c0f42962 // indirect
github.com/getlantern/lantern-water v0.0.0-20260520145825-958775d51395 // indirect
github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175 // indirect
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,8 @@ github.com/getlantern/keepcurrent v0.0.0-20260422161259-54a4d9a93694 h1:iLWm6S/4
github.com/getlantern/keepcurrent v0.0.0-20260422161259-54a4d9a93694/go.mod h1:ag5g9aWUw2FJcX5RVRpJ9EBQBy5yJuy2WXDouIn/m4w=
github.com/getlantern/kindling v0.0.0-20260529141244-21f8b144afab h1:PitYhTvo3oHRKYl4pVAoOIN8bhM+Bw+JBWncMglvHSg=
github.com/getlantern/kindling v0.0.0-20260529141244-21f8b144afab/go.mod h1:TGTxpoNVwc8Be4qkBNtf5oj2psJaEIZEq47GOPS7zkA=
github.com/getlantern/lantern-box v0.0.86 h1:myJa+Crg/oMgqSFhX7DOox4XcVIx8VFiPnkel8x8YT4=
github.com/getlantern/lantern-box v0.0.86/go.mod h1:BVXPyEicSu7m4nQY1OHPkOZNj87M7sYrzmY9AgyiPkc=
github.com/getlantern/lantern-box v0.0.87-0.20260529195337-0b63c0f42962 h1:VSSC7BIn42+tQmhoYg7Wc+ilkXC4SdoJ0LQ6+4kvtC0=
github.com/getlantern/lantern-box v0.0.87-0.20260529195337-0b63c0f42962/go.mod h1:BVXPyEicSu7m4nQY1OHPkOZNj87M7sYrzmY9AgyiPkc=
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9 h1:6seyD2f9tz2am0YQd/Qn+q7LFiiQgnmxgwWFnVceGZw=
github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9/go.mod h1:s0VKrlJf/z+M0U8IKHFL2hfuflocRw3SINmMacrTlMA=
github.com/getlantern/lantern-water v0.0.0-20260520145825-958775d51395 h1:grfGavAUp2E9w9ZoJuM3FyWyQ0sCJ64V4ZMKtZKRqTc=
Expand All @@ -261,8 +261,8 @@ github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YL
github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q=
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b h1:gMYJzEhLrmIqQ+JnjiYNm+UyUDalK3WUmVyecFwmV5g=
github.com/getlantern/publicip v0.0.0-20260328175246-2c460fe80c6b/go.mod h1:NpfXdK4ldEKkjQ4P1R+DBF4ua5VFOlxmgHROTnYrApg=
github.com/getlantern/radiance v0.0.0-20260529193818-b9df7d613b1a h1:BUnZxaaUJbDmILeIOj+/4qlUQrgD8hdV1+67fkLvdqY=
github.com/getlantern/radiance v0.0.0-20260529193818-b9df7d613b1a/go.mod h1:SOXeNVsbdP1v9yYRbuJLB9tV9DpPI77HN+iSecuC9kM=
github.com/getlantern/radiance v0.0.0-20260531221356-11aa55a6ff16 h1:CpsYjT3sBimvg/GNYO5IKvRjWDc4BCeDjDQxUNsx8gA=
github.com/getlantern/radiance v0.0.0-20260531221356-11aa55a6ff16/go.mod h1:wemClXaug4hwPdsUEm8g1bCa8tkjk3UjDM+6PfWJwMI=
github.com/getlantern/samizdat v0.0.3-0.20260529191731-5ea8ae61ddbf h1:KxiMF+oG0rTtuBi7GiIaHfccYOf69rLJ/VnO5myoYc4=
github.com/getlantern/samizdat v0.0.3-0.20260529191731-5ea8ae61ddbf/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0=
github.com/getlantern/semconv v0.0.0-20260327040646-21845dda05cb h1:c5YM7b3a4r2J8Eh89KkI6M/iTFe6Bi+b8AJlfkKdFq4=
Expand Down
98 changes: 98 additions & 0 deletions ios/Runner/Handlers/MethodHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,56 @@ class MethodHandler {
await MainActor.run { result(MobileIsSmartRoutingEnabled()) }
}

// Share My Connection (samizdat) + Unbounded (broflake) peer-share.
// Phones on cellular data won't have UPnP, but home WiFi does — and
// manual port forwarding works on any network where the user owns
// the router. The whole stack is exposed on iOS rather than
// desktop-only.
case "setPeerProxyEnabled":
// requireArg surfaces a FlutterError on missing/invalid
// argument shape instead of silently defaulting to false
// (which would disable sharing on caller bugs).
guard let enabled: Bool = requireArg(call: call, name: "enabled", result: result) else { return }
self.setPeerProxyEnabled(result: result, enabled: enabled)

case "isPeerProxyEnabled":
Task {
await MainActor.run { result(MobileIsPeerShareEnabled()) }
}

case "setPeerManualPort":
// requireArg surfaces a FlutterError on missing/invalid
// argument shape instead of silently defaulting to 0
// (which has the real semantic of clearing the manual port
// override — caller bugs would silently wipe the user's
// setting).
guard let port: Int = requireArg(call: call, name: "port", result: result) else { return }
self.setPeerManualPort(result: result, port: port)

Comment thread
myleshorton marked this conversation as resolved.
case "getPeerManualPort":
Task {
await MainActor.run { result(Int(MobileGetPeerManualPort())) }
}

case "setUnboundedEnabled":
guard let enabled: Bool = requireArg(call: call, name: "enabled", result: result) else { return }
self.setUnboundedEnabled(result: result, enabled: enabled)

case "isUnboundedEnabled":
Task {
await MainActor.run { result(MobileIsUnboundedEnabled()) }
}

case "probeUPnP":
// UPnP M-SEARCH multicast wait — up to ~6s in MobileProbeUPnP.
// Hop off the main actor so the wait doesn't stall the UI, then
// deliver the bool back on MainActor for the Flutter result
// callback.
Task.detached {
let available = MobileProbeUPnP()
await MainActor.run { result(available) }
}

case "isTelemetryEnabled":
Task {
await MainActor.run { result(MobileIsTelemetryEnabled()) }
Expand Down Expand Up @@ -1127,6 +1177,54 @@ class MethodHandler {
}
}

// Share My Connection (samizdat) toggle. Mirrors the macOS handler's
// pattern: blocking Mobile call goes onto a Task; result and error
// delivery hop to MainActor. Plain Task (not Task.detached) is fine
// for these PatchSettings calls — they finish in milliseconds, so
// inheriting the current actor's executor is cheap. probeUPnP is
// the one exception that uses Task.detached because its M-SEARCH
// wait is multi-second.
func setPeerProxyEnabled(result: @escaping FlutterResult, enabled: Bool) {
Task {
var error: NSError?
MobileSetPeerShareEnabled(enabled, &error)
if let error {
await self.handleFlutterError(error, result: result, code: "SET_PEER_PROXY_ENABLED_ERROR")
return
}
await MainActor.run { result("ok") }
}
}

// Manual port forward setting for the SmC residential-proxy path.
// Pass 0 to clear and revert to UPnP-discovered port behavior.
func setPeerManualPort(result: @escaping FlutterResult, port: Int) {
Task {
var error: NSError?
MobileSetPeerManualPort(port, &error)
if let error {
await self.handleFlutterError(error, result: result, code: "SET_PEER_MANUAL_PORT_ERROR")
return
}
await MainActor.run { result("ok") }
}
}

// Local opt-in for the broflake / Unbounded widget proxy ("Basic mode"
// in the SmC UI). Actual run state also depends on the server's
// unbounded feature flag and supplied UnboundedConfig.
func setUnboundedEnabled(result: @escaping FlutterResult, enabled: Bool) {
Task {
var error: NSError?
MobileSetUnboundedEnabled(enabled, &error)
if let error {
await self.handleFlutterError(error, result: result, code: "SET_UNBOUNDED_ENABLED_ERROR")
return
}
await MainActor.run { result("ok") }
}
}

func updateTelemetryEvents(consent: Bool, result: @escaping FlutterResult) {
Task {
var error: NSError?
Expand Down
Loading
Loading