Skip to content

apps + lanterncore: fix empty Windows apps list + restore UI-process logging#8706

Closed
myleshorton wants to merge 24 commits intogarmr/radiance-daemon-refactorfrom
fisk/apps-windows-filter-fix
Closed

apps + lanterncore: fix empty Windows apps list + restore UI-process logging#8706
myleshorton wants to merge 24 commits intogarmr/radiance-daemon-refactorfrom
fisk/apps-windows-filter-fix

Conversation

@myleshorton
Copy link
Copy Markdown
Contributor

@myleshorton myleshorton commented Apr 24, 2026

Started as a fix for Freshdesk #173774 (Derek's empty apps list in Windows split tunneling, resolves getlantern/engineering#3335) and grew to fix the underlying reason we had no diagnostics to work with.

Net changes

1. apps_windows.go — empty apps list

  • Drop the ParentKeyName != "" filter in isNonUserFacingUninstallEntry. ParentKeyName only indicates the entry is a sub-component of another installer package, and plenty of legitimate end-user apps set it (Squirrel installs — Slack, Discord, VS Code Insiders; winget packages; MSI bundle children). SystemComponent=1 and NoDisplay=1 are the documented signals; those stay. Introduced by Filter system apps from Windows split tunneling #8641.
  • Also drops the now-unused parentKeyName field from uninstallEntryMetadata and the GetStringValue("ParentKeyName") read.
  • Promote the Start Menu scanner's COM-failure logs from Debug → Warn (CoInitializeEx, WScript.Shell CreateObject, QueryInterface). These paths silently abandoned the whole Start Menu scan; now an empty apps list tells us which call failed.
  • Per-filter tally slog.Info summaries at the end of each scanner (Start Menu + Uninstall registry): scanned counts, kept, per-drop-reason counts, and a sampleKept slice with the first 20 discovered apps. A single log line now pinpoints which filter is zeroing out the list.

2. lanterncore — restore UI-process Go logging

Derek's first round of test logs showed none of the new scanner summaries because the UI process never called common.Init on the refactor branch. Without it, slog writes to stderr (= nowhere on a GUI host), settings are uninitialized, and no lantern.log gets written outside the daemon. Patrick pointed out the refactor branch just forgot to call common.Init — one-line fix.

  • Add common.Init(opts.DataDir, opts.LogDir, opts.LogLevel) at the top of LanternCore.initialize. Wires up slog → <LogDir>/lantern.log, seeds settings (DataPathKey / LogPathKey), and installs the crash reporter. Idempotent via common.Init's own sync.Once so Android (where the daemon already ran it in-process) no-ops safely.
  • Auto-attach UI-process *.log to Report Issue. LanternCore.ReportIssue now globs *.log from settings.LogPathKey (set by common.Init) and appends to the IPC attachments. The daemon's archive builder only sees its own logDir, so without this pass-through, UI-process lantern.log and flutter.log would never reach the issue bundle.
  • Documented cross-user caveat (Windows daemon = LocalSystem, macOS System Extension = root-equivalent) — we pass paths over IPC, so the UI logDir has to live in a location the daemon's user can read. %PUBLIC%\Lantern\logs and /Users/Shared/Lantern/Logs are chosen for that reason; if we ever move to per-user paths we'll need an in-memory attachment protocol.

Compatibility

Core interface signature unchanged, so stays compatible with #8704 (screenshot attachments, open against main). That PR's attachmentsJSON layers on top of the same additional-attachments primitive.

Test plan

  • go test ./lantern-core/apps/...
  • GOOS=windows go build ./lantern-core/...
  • Next nightly: Derek hits Report Issue on Windows — bundle should include lantern.log + flutter.log from UI logDir, and the scanner summaries pinpoint the actual filter emptying his list

🤖 Generated with Claude Code

isNonUserFacingUninstallEntry was treating any Uninstall-registry entry
with a non-empty ParentKeyName as non-user-facing. That's too strong —
ParentKeyName just indicates the entry is a sub-component of another
installer package, and plenty of legitimate end-user apps set it:

- Squirrel-installed apps: Slack, Discord, GitHub Desktop, VS Code
  Insiders, Atom, many Electron-based installers
- winget-managed packages
- MSI bundles that chain sub-components

On machines where most installed apps come through these installers,
this filter empties the apps list in Settings → Split Tunneling → Apps.
Reported in Freshdesk #173774 against v9.0.30 on Windows 10.

Keep the SystemComponent=1 and NoDisplay=1 checks (Windows-documented
signals for non-user-facing entries) and the update/hotfix/security
releaseType check. Drop the standalone ParentKeyName check.

Also flip the matching test case — "has parent key name alone" now
expects false.

Fixes getlantern/engineering#3335. Introduced by #8641.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 24, 2026 21:21
When CoInitializeEx or WScript.Shell fail in
collectAppsFromStartMenuShortcuts, the function silently returns an
empty list — the CoInitializeEx path logs nothing at all, and the
WScript.Shell path logs only at Debug level. Combined with the
Uninstall-registry path returning zero (see previous commit) the user
gets an empty apps list with no user-visible signal that the Start
Menu scanner failed.

Promote all three early-return paths to Warn with a consistent
"skipping Start Menu app scan" message plus the underlying err, so
when the apps list is empty the UI log (slog output from the Flutter
UI process) shows exactly which path failed.

Part of engineering#3335. Stacked on the previous commit in this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adjusts Windows uninstall-registry filtering so legitimate user-facing apps (notably Squirrel/winget-managed entries) aren’t incorrectly excluded from the Split Tunneling apps list.

Changes:

  • Removes the ParentKeyName-only heuristic from isNonUserFacingUninstallEntry.
  • Updates the corresponding unit test to expect user-facing behavior when only ParentKeyName is present.
  • Adds clarifying inline comments explaining why ParentKeyName is not a reliable signal.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
lantern-core/apps/apps_windows.go Stops treating ParentKeyName as sufficient for filtering; keeps documented flags (SystemComponent, NoDisplay) and ReleaseType filtering.
lantern-core/apps/apps_windows_test.go Updates the ParentKeyName test case expectation and documents rationale.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lantern-core/apps/apps_windows.go Outdated
myleshorton and others added 5 commits April 24, 2026 15:25
Adds an Info-level summary log at the end of each Windows app scanner
plus a rollup in loadInstalledAppsPlatform, so diagnosing an empty
apps list only needs one log read instead of a round-trip.

Fields captured:

- Uninstall registry scanner: scanned total, kept, and per-filter
  drop counts (droppedNonUserFacing / NoDisplayName / NoExe / System
  / Utility / Excluded / Duplicate).
- Start Menu scanner: roots scanned vs missing, total shortcuts,
  kept, and per-filter drops (Unresolved / System / UtilityOrExcluded
  / Duplicate). Also captures how many apps were added via
  collectAppsFromPackageCacheHints recovery.
- Top-level summary: startMenuCount, registryCount, total (fires
  even on success, unlike the existing empty-fallback warnings).

Part of engineering#3335. Stacked on the previous two commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Copilot review on #8706. With ParentKeyName no longer affecting
filtering, the field and its registry read are dead weight. Remove:

- parentKeyName from uninstallEntryMetadata
- GetStringValue("ParentKeyName") from readUninstallEntryMetadata
  (replaced with a short comment pointing to
  isNonUserFacingUninstallEntry for the rationale, so future readers
  don't reintroduce it)
- The now-invalid "has parent key name alone" test case

The rationale comment stays on isNonUserFacingUninstallEntry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The slog.Info summaries added in the previous commit don't help
diagnose real tickets on Windows — the FFI library runs in the
Flutter UI process, whose default slog handler writes to stderr
(the daemon lantern.log only captures the daemon's own logs).
Freshdesk #173778 (Derek on v9.0.30 with the earlier fixes) shipped
a daemon log with zero apps-scan activity, confirming the problem.

Write a parallel dump file to %PUBLIC%\Lantern\data\apps-scan-debug.log
that the user can grab directly. Captures, per run:

- APPDATA / ProgramData env vars (so we can tell if they're empty)
- Start Menu roots scanned vs missing, shortcut counts, per-filter drops
- Uninstall registry totals, per-filter drops
- First 20 kept apps from each scanner
- Early-bailout reasons (CoInitializeEx / WScript.Shell / QueryInterface
  failures) when the Start Menu scanner can't run at all

File is append-only; multiple scans accumulate so we can see changes
across sessions. All I/O errors are swallowed — this is pure
diagnostic, never block the real scan.

Part of engineering#3335. Stacked on the previous commits in this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generalize the FFI-specific dump file from the previous commit. The
underlying problem isn't FFI-specific — it's that any process loading
lanterncore as a library alongside a separate radiance daemon (today
just the Windows/Linux desktop FFI; tomorrow possibly macOS via a
sidecar bridge) gets the default slog handler, which writes to stderr
and is silently dropped on GUI hosts. flutter.log only catches
Dart-side logs; the daemon's lantern.log only catches the daemon
process. Go code running in the UI process — apps scanner, vpn
helpers, anything lanterncore transitively calls — has nowhere to
land.

Add lanterncore.SetupLogging(logDir, level) — writes to
<logDir>/lantern-core.log via rlog.NewLogger (rotation + standard
format), idempotent via sync.Once. Documented to NOT be called from
processes that run the daemon in-process (mobile, macOS in-process)
where common.Init already handles logging via lantern.log.

The desktop FFI's setup() now calls it before lanterncore.New so
LanternCore initialization logging also lands in the file.

Also auto-attach all *.log files from the UI logDir to Report Issue
via a new App.ReportIssueWithAttachments method on the Core interface
(LanternCore.ReportIssue stays as a thin wrapper for back-compat).
The daemon-side issue archive globs only its own logDir, so without
this lantern-core.log and flutter.log would never reach the bundle.

The Windows-specific apps-scan-debug.log file from the previous
commit is removed — slog summaries written to lantern-core.log via
the new generic mechanism cover the same diagnostics, with env vars
folded into the slog attrs and a sampleKept slice instead of a
separate text dump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#8704 (Add screenshot attachments to issue reports — open against
main) changes LanternCore.ReportIssue in a different direction: it
adds an attachmentsJSON string param with a ReportIssueAttachment
struct (name/path/mimeType/sizeBytes), whereas this PR had added a
separate ReportIssueWithAttachments method taking []string.

Two conflicting APIs for the same concept would collide hard when
refactor eventually absorbs main. Defer the UI log auto-attach to a
follow-up that adopts #8704's mechanism once it lands.

Immediate impact for Derek: lantern-core.log still gets written (the
general SetupLogging fix is unchanged), but he'll have to grab it
manually from C:\Users\Public\Lantern\logs until #8704 propagates
here and we wire auto-attach through its attachmentsJSON.

Reverts just the ReportIssueWithAttachments plumbing from the
previous commit. SetupLogging + apps_windows slog summaries stay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
myleshorton and others added 4 commits April 24, 2026 16:56
Two corrections:

1) SetupLogging doc was wrong about which platforms run the daemon
   in-process. Only Android does (UI + VPN service share a process).
   iOS and macOS talk to Network/System Extensions over XPC; Windows
   and Linux talk to lanternd over a named pipe. On all four of those,
   the UI process loads lanterncore as a library without the daemon's
   common.Init ever running, so slog goes to stderr = nowhere.
   Updated the doc accordingly — the fix is useful everywhere except
   Android.

2) Report Issue already globs *.log from the daemon's logDir via
   buildIssueArchive, but the UI process writes flutter.log and (now)
   lantern-core.log to a DIFFERENT dir — the daemon's glob never sees
   them. Added lanterncore.LogDir() which returns the logDir registered
   by SetupLogging, and have LanternCore.ReportIssue glob *.log from
   it and append to the additional attachments before handing off to
   the IPC client.

   No Core interface change — LogDir() is a package-level accessor
   that just reads the dir SetupLogging stashed. On Android (where
   SetupLogging is never called) LogDir() returns "" and ReportIssue
   behaves exactly as before.

Also leaves Core.ReportIssue signature untouched so this stays
compatible with #8704 (screenshot attachments); that
PR's attachmentsJSON is layered on top of this same additional-path
mechanism, not a replacement for it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer point: the daemon and UI run as DIFFERENT users on desktop
(Windows daemon runs as LocalSystem — confirmed in
radiance/cmd/lanternd/lanternd_windows.go:73 — while the UI runs as
the logged-in user; macOS System Extension runs with root-equivalent
privs). Passing file PATHS over IPC and having the daemon read them
on its side only works if the file's ACL grants the daemon's user
read access. We're currently dodging that because the UI deliberately
writes to %PUBLIC%\Lantern\logs and /Users/Shared/Lantern/Logs —
both have default ACLs that grant SYSTEM / root read access
regardless of the file's owning user.

Document that explicitly in ReportIssue so a future change to a
per-user path (e.g. %LOCALAPPDATA% or ~/Library/Logs) forces us to
swap to an in-memory protocol.

Also stat files on the UI side in collectUILogs and drop any we
can't read ourselves — they won't survive the daemon-side read
either and excluding them keeps the report from ballooning with
entries that'll just warn in readExtraFiles on the daemon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ger setup

Per Patrick: refactor branch just never added the common.Init call
that the old architecture relied on. Calling it is a one-liner and
sets up everything I'd been reinventing — slog → <logDir>/lantern.log,
settings (DataPathKey / LogPathKey), crash reporter, reporting, env
resolution. All of it idempotent via common.Init's own sync.Once,
so platforms where the daemon already ran it in-process (Android)
safely no-op the second call.

Changes:

- LanternCore.initialize now calls common.Init(DataDir, LogDir,
  LogLevel) before anything else. Captures Go-side logs from any
  code running in the UI process (apps scanner, ipc.Client helpers,
  anything transitively reachable).

- Dropped lanterncore.SetupLogging / LogDir() — common.Init covers
  both. ReportIssue now reads settings.LogPathKey (set by
  common.Init) instead of a custom accessor, and globs *.log from
  that dir to auto-attach UI-process logs to the issue bundle.

- FFI setup no longer calls SetupLogging; common.Init runs inside
  lanterncore.New.

Cross-user caveat from the previous commit stays in a doc comment on
ReportIssue — %PUBLIC%\Lantern\logs and /Users/Shared/Lantern/Logs
are chosen specifically because SYSTEM / root can read them
regardless of the creating user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trim the comments added across this PR — most were restating the
code or explaining things the commit messages already cover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@myleshorton myleshorton changed the title apps: stop filtering uninstall entries by ParentKeyName alone apps + lanterncore: fix empty Windows apps list + restore UI-process logging Apr 25, 2026
Comment thread lantern-core/core.go Outdated
myleshorton and others added 10 commits April 24, 2026 18:57
Per Patrick's review: mobile + macOS already have UI and daemon
sharing the same directories, so the daemon-side archive glob
already covers those logs. Only Windows and Linux have distinct
dirs that need the pass-through.
Per Patrick: on iOS/macOS the tunnel extension already calls
common.Init with the same logDir as the main app would. With my
unconditional call, both processes would point lumberjack at the
same lantern.log — when one hits the 25MB threshold and renames
the file, the other keeps writing to the renamed backup until it
rotates, at which point their rotated backups collide. Slow-burn
but real.

Restrict the call to runtime.GOOS == "windows" || "linux" — the
only platforms where the UI process has no other path to common.Init
and no shared logDir with the daemon. Android (single process) and
iOS/macOS (shared logDir, daemon handles it) keep their existing
behavior.
Main app and tunnel extension share /Users/Shared/Lantern/Logs, so
calling common.Init from both processes would race on lantern.log's
lumberjack rotation. Instead, on darwin/ios set up a dedicated
<logDir>/lantern-app.log from the main app. Extension keeps owning
lantern.log; each lumberjack handles its own file, no cross-process
rename races.

Skipped settings.InitSettings and the crash reporter since the
extension's common.Init already owns both of those through the
shared dataDir. This is just the slog half of common.Init.
Actual root cause of Derek's empty Windows split-tunneling list across
Freshdesk #173774 / #173778 / #173826.

The Windows app scanner correctly finds 19 apps (12 from Start Menu,
7 from Uninstall registry — the new diagnostic logs in this PR
confirm), so the apps list was reaching the Flutter side from
loadInstalledApps. The empty UI was caused by getEnabledApps:

    var enabledApps []string   // nil on a fresh install (no
                               // ProcessPath / ProcessPathRegex /
                               // PackageName items)
    ...
    json.Marshal(enabledApps)  // → "null" (nil slice, not [])

then the FFI layer in lantern_ffi_service.dart:287:

    (jsonDecode(enabledJson) as List)
    // jsonDecode("null") = null
    // null as List → "type 'Null' is not a subtype of List<dynamic>"

The thrown cast error landed in the catch block which yielded an
empty []AppData to the riverpod stream — UI rendered nothing.

Two-line fix: initialize as []string{} so json.Marshal emits "[]"
instead of "null". The Dart side then casts cleanly to an empty
List<dynamic> and the apps actually render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups for Freshdesk #173827 (Derek's report on the
just-working-now Windows split-tunneling list):

1. Lantern showing in its own apps list. Both scanners pick up
   Lantern: Start Menu finds the UI exe (C:\Program Files\Lantern\
   lantern.exe), Uninstall registry finds the daemon
   (C:\Program Files\Lantern\LanternSvc.exe with display name
   "Lantern version 9.0.30+501"). Add isLanternSelfApp matching by
   exe path under any Program Files\Lantern\ dir, by basename for
   lantern / lanternsvc / lanternd, and by name for "Lantern" /
   "Lantern desktop" / "Lantern vpn" / "Lantern version *" so
   portable installs and the verbose registry name both get caught.
   Wire into both scanners alongside isWindowsSystemApp so the
   counters get a new droppedSelf field.

2. Apps missing (Claude in this report). Without per-shortcut /
   per-entry visibility we can't tell *which* shortcuts get dropped
   as unresolved or which registry entries fall through with no
   .exe. Add sampleDroppedUnresolved (.lnk paths + display names
   from Start Menu) and sampleDroppedNoExe (display name + DisplayIcon
   + InstallLocation from registry) slice attrs on the existing
   slog.Info summaries. Capped at 20 each. Next round of Derek's
   logs should show whether Claude lands in either list and what
   resolveShortcutExecutable / pickExePath saw for it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Last set of logs (Freshdesk #173840) showed Lantern self-filter
working but Claude still missing — and not in any of the 20 sampled
unresolveds (those were all Microsoft system shortcuts: Accessibility,
System Tools, etc.). Two likely hiding spots:

- Start Menu droppedUtilityOrExcluded (count=2). Add
  sampleDroppedUtilityOrExcluded so we see exactly what the 2
  filtered shortcuts are and why (utility match vs excluded path).

- Registry droppedNoDisplayName (count=27). Big bucket where
  Squirrel apps often land if they don't set DisplayName under
  the standard subkey. Add sampleDroppedNoDisplayName showing
  the registry subkey name + DisplayIcon + InstallLocation —
  enough fingerprint to identify Claude.

Also bump the unresolved cap from 20 → 50 (Derek had 37) so we
capture all of them next time and don't miss Claude in the tail.
Three new sources to catch apps that aren't in Start Menu or
Uninstall registry — primarily Squirrel/Electron apps (Slack,
Discord, Claude, GitHub Desktop, VS Code Insiders, Atom-likes)
that often only register for auto-startup or live entirely under
%LOCALAPPDATA%. See Freshdesk #173842.

1. collectAppsFromAppPaths
   HKLM\Software\Microsoft\Windows\CurrentVersion\App Paths.
   Each subkey name is the exe filename and (Default) is the full
   path. Apps register here to be runnable via Win+R / shellexecute
   — catches browsers, IDEs, Office, third-party utilities. Plain
   key enumeration; same dedup/system/self/utility filters as the
   other scanners.

2. collectAppsFromRunRegistry
   HKLM/HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
   Each value is a command line; Squirrel apps register
   "com.squirrel.<App>.<App>" pointing at Update.exe with
   --processStart "<App>.exe". parseRunEntry splits the command
   line via the existing parseWindowsCommandTokens helper and
   reuses resolveWrappedExecutableWithContext (which already
   handles the --processStart fallback when the head exe is
   excluded). deriveRunDisplayName strips the
   "com.squirrel.<App>." prefix.

3. collectAppsFromSquirrelLocalAppData
   Walks %LOCALAPPDATA% one level deep looking for <Dir>\Update.exe.
   Backstop for Squirrel apps that appear in neither Start Menu
   nor Run (e.g. user disabled auto-start). findSquirrelAppExe
   tries <AppName>.exe at the dir root, then any app-X.Y.Z /
   current subdir's <AppName>.exe, then any non-excluded .exe in
   those locations as a last resort.

Each scanner emits its own slog.Info summary with kept/dropped
breakdown + sampleKept, mirroring the existing Start Menu and
Uninstall registry summaries. The top-level windows app scan
summary now reports per-source counts.

Tests added for deriveRunDisplayName (Squirrel prefix stripping,
plain values, empty fallback) and findSquirrelAppExe (sibling
exe, app-X.Y.Z layout, fallback when dir name and app name
diverge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Derek's diagnostic logs (Freshdesk #173890) showed App Paths added
30 entries to the apps list, but most were Microsoft system internals
or UWP helper exes — only 1Password / ms-teams / mspaint / notepad /
olk are real user-facing apps. Specific noise observed:

  - IEDIAG, IEXPLORE under \Program Files\Internet Explorer\
  - msoadfsb, msoasb, msoxmled, sdxhelper, SKYPESERVER under
    \Microsoft Office\Root\Office16\
  - TabTip under \Common Files\microsoft shared\ink\
  - wab, wabmig under \Program Files\Windows Mail\
  - 1Password-BrowserSupport, 1Password-LastPass-Exporter,
    op-ssh-sign-wsl (UWP package helpers)
  - ms-teamsupdate (Teams updater helper)
  - GetHelp (Microsoft Get Help)

Add isAppPathsNoise() applied only in the App Paths scanner so we can
be aggressive without affecting Start Menu / Uninstall scans:

  1. Path-based: drop entries under \Internet Explorer\, \Windows
     Mail\, \Windows NT\, \Windows Defender\, \Common Files\microsoft
     shared\, \Common Files\microsoft.net\.
  2. Office Root: drop everything under \Microsoft Office\ except a
     fixed allowlist of primary product exes (winword/excel/powerpnt/
     outlook/msaccess/mspub/onenote/lync/groove/visio/winproj). Office
     primaries also flow in via Start Menu so dedup handles them.
  3. Helper-named basenames: substring-match against helperHints
     (browsersupport, lastpassexporter, sshsign, sshagent, updater,
     helper, diagnostic, diagcmd) on the normalized basename — catches
     "ms-teamsupdate" → "msteamsupdate" matching "update" via the
     suffix branch, and "1Password-BrowserSupport" →
     "1passwordbrowsersupport" matching "browsersupport".
  4. Suffix-only: drop basenames ending in update/service/agent/sync/
     broker (but not the bare word, so a legit app called "Service"
     wouldn't be dropped).

Also wire isLikelySystemDisplayName into App Paths so display names
like "GetHelp", "Tips", "Xbox" get caught via the existing curated
list rather than via path-pattern.

17 new test cases covering positive (all the observed noise drops) and
negative (Word/Excel kept under Office Root, ms-teams/mspaint/notepad/
olk kept under WindowsApps, Chrome kept under \Google\).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lantern-core/core.go Outdated
Comment thread lantern-core/core.go Outdated
Comment thread lantern-core/apps/apps_windows.go Outdated
Comment thread lantern-core/apps/apps_windows.go Outdated
myleshorton and others added 2 commits April 27, 2026 16:41
Two more App Paths noise patterns surfaced after the first filter pass
in derek's logs (ticket #173931, kept=11 still included plumbing exes):

- \windowsapps\microsoft.desktopappinstaller_ — drops winget and
  WindowsPackageManagerServer (CLI / background server, not GUI apps).
- \dotnet\ subdir under any UWP package — .NET helper assemblies
  (e.g. Power Automate Desktop's PAD.BrowserNativeMessageHost,
  PAD.ChildSession.Service.Host) live there. The user-facing exe of a
  UWP package always sits at the package root, never under \dotnet\.
…ist constants

Four Copilot review threads, all legitimate:

- core.go:195 — switch comment was misleading. It claimed "Android is the
  only platform where the daemon shares the process," but darwin/ios also
  embed the backend in-process (init_mobile.go is build-tagged
  android||ios||darwin). Rewrite to describe the actual three-way split:
  windows/linux talk to a daemon over IPC, darwin/ios share logDir with
  the tunnel extension, android relies on Mobile.SetupRadiance having
  already called common.Init.

- core.go:633 — collectLocalLogs's doc claimed "absolute paths" but
  doesn't call filepath.Abs; the result shape mirrors whatever
  filepath.Glob returns. Update the doc to match (the caller in
  ReportIssue already passes the absolute settings.LogPathKey, so the
  practical behavior is unchanged). Also soften the os.Stat readability
  claim — that check screens the UI process's view, not the daemon's, so
  describe it as best-effort and note the daemon's own readability check
  is authoritative.

- apps_windows.go:537 — sampleAppNames was logging full executable paths
  (typically C:\Users\<username>\...) into scan-summary slog lines that
  end up in "Report Issue" bundles. PII risk. Redact to filepath.Base —
  enough signal for diagnostics ("did Slack get included?") without
  leaking user filesystem layout.

- apps_windows.go:912 — isAppPathsNoise allocated four collection
  literals on every call (systemPaths slice, primaryOfficeExes map,
  helperHints slice, generic suffixes slice), and runs hundreds-to-
  thousands of times per scan. Hoist all four to package-level vars so
  the per-entry hot path is allocation-free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@myleshorton myleshorton requested a review from atavism April 27, 2026 23:48
Require \windowsapps\ alongside \dotnet\ before treating an entry as
noise, so a third-party installer that happens to ship its bundled
.NET runtime under a \dotnet\ subdir isn't dropped.
@myleshorton
Copy link
Copy Markdown
Contributor Author

Head's up @atavism and @garmr-ulfr I'm going to split this up into an essential, no-brainer PR and a separate one for the fancier app searching on windows.

@myleshorton
Copy link
Copy Markdown
Contributor Author

Superseded by two split PRs:

The original PR conflated two unrelated fixes (the Dart-cast crash on null vs [], and the under-covered app-discovery scanner). Splitting so each piece can be evaluated on its own merits and #8709 can land fast.

myleshorton added a commit that referenced this pull request Apr 28, 2026
…ogging (#8709)

Two narrow fixes that together resolve Freshdesk #173774 / #173778 /
#173826 (Derek's "Failed to fetch installed apps" empty list on Windows
split tunneling). Split out from #8706 so they can land independently
of the broader app-discovery rework that PR also contained.

1. **GetEnabledApps returns []string{} instead of nil.**
   When no apps are split-tunneled, the previous code returned nil,
   which json.Marshal serialized as "null". Dart's jsonDecode("null")
   returns null; the receiving code does `as List`, which throws and
   the UI shows "Failed to fetch installed apps". Initializing as an
   empty slice serializes to "[]" — Dart parses that as an empty list,
   no exception, no error UI. THIS is the actual root cause of the
   empty-list reports we've been chasing; the apps-discovery scanner
   work was investigating a different (also-real but secondary) issue.

2. **UI-process slog wired up via common.Init.**
   On the refactor branch, the UI process never called common.Init.
   slog wrote to stderr (= nowhere on a GUI host), settings were
   uninitialized, no lantern.log was produced outside the daemon.
   Patrick caught this — it was a one-line miss in the refactor.

   Platform-aware so we don't double-init on platforms where the
   backend embeds in-process:
     - windows/linux: full common.Init (separate UI + daemon procs)
     - darwin/ios:    setupAppLogging into a distinct lantern-app.log
                      so the main-app slog doesn't race the tunnel
                      extension's lantern.log on lumberjack rotation
     - android:       Mobile.SetupRadiance already ran common.Init
                      upstream — fall through

3. **Auto-attach UI-process *.log to ReportIssue (windows/linux only).**
   Without it the daemon's archive glob only sees the daemon's logDir;
   UI-side lantern.log + flutter.log never reach the issue bundle. The
   daemon runs as SYSTEM on Windows; we keep UI logDir at
   %PUBLIC%\Lantern\logs so SYSTEM can read it.

The broader Windows app-discovery work from #8706 (App Paths scan, Run
keys, Squirrel pattern, isAppPathsNoise heuristic filters) is being
held in a separate PR for independent review.

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread lantern-core/core.go
myleshorton added a commit that referenced this pull request Apr 28, 2026
* migrate to new ipc.Client api, first-pass

* pullin a couple fixes, update linux vpn status poller

* start ipc server ios/macos

* update radiance, fix linux daemon build

* start ipc server windows service

* fix datacap stream

* decode user response data to json

* gofmt

* update ipc request path check for linux smoke test

* Fixed issue with user apis

* redo linux packaging changes undone by merge

* move RunOffCgoStack from radiance to here, small cleanup

* fetch radiance-owned settings on demand instead of caching locally

* add missing smart-routing, ad-block, oauth calls

* clean up

* fix ref async issue for IPC calls

* gofmt

* fix test, linux package verification

* update radiance, remove server groups

* fix: return added server tags from AddServersByURL

Server tags are determined by URL content, not caller-supplied names.
addServerBasedOnURLs now returns the tags of added servers so callers
can connect using the actual tag. Also sends VPN status updates from
connectToServer on Linux so the UI reflects connection state changes.

* wrap ffi calls in runOnGoStack, update win service

* add explicit not linux build tag

* update radiance

* use RADIANCE_REPO in lanternd src

* flatten server model to match radiance, fix tests

* use loopback ipc client for mobile

* update radiance, log service install error in smoke test

* retrieve selected server from radiance instead of cacheing

* stop lantern before unintall, revert accidental service name change

* remove allow override

* fix name reference and misplaced stop call

* fix several issues

* code review

* fix toggles not registering and fetching plans

* always refetch server list when view opens

* fix crash in server select screen

* fix split tunnel website view not loading websites

* sync vpn status from system on launch

* fix stale onboarding marker persisting reinstall

* Revert "fix stale onboarding marker persisting reinstall"

This reverts commit a21a218eac7df90d678ce5d35d27892bbe893da2.

* fix vpn prompt displaying when quiting

* Macos system extension updates #2 (#8637)

* if system extension is in uninstall state do not block new installtion.

* update macos system extension test

* do not cache dart_tool

* Set the default status as unknown.

* code review updates

* Filter system apps from Windows split tunneling (#8641)

* Add split tunneling e2e test

* Fix split tunneling website smoke assertion

* Fix split tunneling smoke navigation

* code review updates

* code review updates

* code review updates

* Filter Windows system apps in split tunneling list

* code review updates

* code review updates

* Update system apps filter

* code review updates

* fix: upload and notify for nightlies even when some platforms fail (#8649)

The upload-s3 and upload-release-artifacts jobs required ALL platform
builds to succeed or be skipped. When a matrix entry failed (e.g.,
Linux arm64), the entire build-linux job reported as 'failure', which
caused both upload jobs to skip entirely — even though macOS, Android,
iOS, and Linux amd64 all succeeded.

Simplify the condition: run uploads if at least one platform build
succeeded. The upload steps already handle missing artifacts gracefully
(upload_if_exists checks for file existence).

This ensures the Slack notification goes out with download links for
whatever platforms did build successfully.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add arch to releases (#8652)

* feat: add arch to releases

* Update linux/packaging/usr/lib/systemd/system/lanternd.service

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: remove committed lanternd.service file

Agent-Logs-Url: https://github.com/getlantern/lantern/sessions/15085485-3c6a-4e1e-93ea-6e9bf0623d09

Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

* fix issues from 3173

* Refactor and fixed multiple bugs

* cache selected server location locally to avoid UI flash

* Fix tunnel issue in android

* App event issue and auto server location fixes

* added logs

* mobile: return string instead of []byte + update Swift callers (#8663)

* mobile: return string instead of []byte from gomobile-exported funcs

The gomobile wrapper copies Go pointer-containing return values to the C
thread stack using runtime.wbMove. When a GC cycle runs during the copy,
bulkBarrierPreWrite panics because the destination isn't GC-tracked.
Returning string avoids this — gomobile marshals strings via C heap
allocation rather than leaving them as Go slice headers.

See getlantern/engineering#3175 for the full crash analysis (from
Freshdesk #172640 — Derek reporting "Lantern Crash" on macOS 26.3.1).

Go changes:
  AvailableFeatures, UserData, FetchUserData, GetAvailableServers,
  GetSelectedServerJSON, OAuthLoginCallback, AcknowledgeGooglePurchase,
  AcknowledgeApplePurchase, Login, Logout, DeleteAccount

Swift changes (macos + ios): preserve Flutter contract by converting
the string back to Data for methods whose Dart side reads `bytes` via
utf8.decode (getUserData, fetchUserData, oauthLoginCallback, login,
logout, deleteAccount, acknowledgeInAppPurchase). For methods whose Dart
side expects String (featureFlags, getLanternAvailableServers,
getSelectedServerJSON), just pass the gomobile string directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* android: update MethodHandler for string-returning gomobile bindings

The gomobile-exported funcs in lantern-core/mobile/mobile.go now return
string instead of []byte. The generated Android binding will therefore
return String where it used to return ByteArray.

For each affected method, match what the iOS handler does so the Flutter
platform-channel contract stays stable:

  * Methods whose Dart callers expect bytes (Uint8List) — login,
    logout, deleteAccount, userData, fetchUserData, oauthLoginCallback,
    acknowledgeGooglePurchase — convert the String result via
    `.toByteArray(Charsets.UTF_8)` before calling success() (mirrors
    Swift's `.data(using: .utf8)`).

  * Methods whose Dart callers expect a String — availableFeatures,
    getAvailableServers, getSelectedServerJSON — drop the
    `String(byteArray)` constructor and use the return value directly,
    with the same "{}" / "[]" empty-default that iOS uses.

Addresses Copilot review on PR #8663.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* encapsulate ipc.Client behind LanternCore interface

Route all IPC operations through LanternCore methods instead of
exposing Client() to callers. Add GetSelectedServerTag,
GetAutoLocationJSON, CheckDaemonReachable, PatchSettings, and
VPNStatusEvents to the Core interface. Update FFI and mobile layers
to use them, and remove now-unused vpn_tunnel helper functions.

Also includes Flutter-side fixes: device-removal sign-in race
condition, plans fetch retry logic, and private server setup
improvements.

* ios/macos: drop invalid optional-chaining on non-optional String (#8671)

The gomobile-exported functions in lantern-core/mobile/mobile.go were
migrated from ([]byte, error) to (string, error). gomobile renders the
new signatures with a non-optional Swift String return (Data was
optional; String is not), so `json?.data(using: .utf8)` and
`payload?.data(using: .utf8)` now fail to compile:

    error: cannot use optional chaining on non-optional value of type
    'String'

Drop the `?` on all 14 call sites (7 each in ios/ and macos/). The
resulting `json.data(using: .utf8)` returns Data? anyway — an empty
Go string still produces a non-nil empty Data, which preserves the
Flutter contract the comment on these lines describes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* add android-test + android-reproduce for emulator testing and ticket reproduction (#8672)

* add android-test script for quick emulator testing with env overrides

Usage:
  scripts/android/android-test <apk> [ENV_KEY=VALUE ...]

Example:
  scripts/android/android-test lantern.apk RADIANCE_COUNTRY=BG RADIANCE_FEATURE_OVERRIDES=dns_ruleset_host_bypass

Starts an emulator, installs the APK, pushes a .env file with overrides
to the app's data dir (via adb root on Google APIs images, run-as on
debug APKs, or su on rooted devices), restarts the app, and streams
filtered logcat.

Prefers the "lantern-test" AVD if it exists (create with Google APIs
image for root access):
  sdkmanager "system-images;android-35;google_apis;arm64-v8a"
  avdmanager create avd -n lantern-test -k "system-images;android-35;google_apis;arm64-v8a"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* address review: serial targeting, su quoting, trap cleanup, fix comment

- Use -s <serial> throughout so multiple devices don't break adb
- Fix su -c quoting so $(stat ...) expands on-device
- Add trap to clean up temp .env on EXIT/INT/TERM
- Fix header comment (no /sdcard/ fallback)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-test: push .env to .lantern data dir (not app root)

The Go env package reads .env from the data directory (via
env.LoadFromDir called from common.Init), not from the app's root
data dir. Push to /data/data/$PKG/.lantern/.env so radiance finds it.

Companion: getlantern/radiance#421 (env.LoadFromDir)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-test: auto-install system image and create AVD if none exists

If no AVDs are found, the script now automatically:
1. Detects host arch (arm64 vs x86_64)
2. Installs the Google APIs system image via sdkmanager
3. Creates a "lantern-test" AVD via avdmanager

This means running android-test on a fresh machine with just the
Android SDK installed works out of the box — no manual AVD setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* address review: array for ADB_CMD, timeouts, remove unused PID

- Use bash array for ADB_CMD so paths with spaces work correctly
- Add configurable timeouts for emulator appear (120s) and boot (300s)
- Remove unused EMULATOR_PID — emulator intentionally left running
  between invocations so subsequent runs don't pay boot cost

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* add android-reproduce: reproduce Freshdesk tickets on emulator

Usage:
  android-reproduce /tmp/ticket-172722              # auto-downloads APK
  android-reproduce /tmp/ticket-172722 lantern.apk  # uses provided APK

After running /analyze-ticket, this script:
1. Extracts country + version from the ticket's config/logs
2. Downloads the matching APK from GitHub releases (gh CLI)
3. Pushes the user's exact config.json, servers.json, split-tunnel.json
   to the emulator so it gets the same proxies, DNS rules, rule sets
4. Sets RADIANCE_COUNTRY to match the user's region
5. Installs, restarts, and streams filtered logcat

This gives near-exact reproduction of Android-specific issues by
replicating the user's proxy assignments, country routing, and
sing-box config on a local emulator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-reproduce: match user's Android API level from ticket logs

Extracts sdkInt, osVersion, and model from flutter.log's "Device info"
line. Creates an AVD with the matching API level (e.g. "lantern-api36"
for a user on Android 16/SDK 36). Falls back to API 35 if the target
image isn't available.

Example for ticket #172722 (Android 16, SM-A556B):
  Creates lantern-api35 (API 36 clamped to 35), installs matching APK,
  pushes user's exact config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-reproduce: dynamically find closest available API image

Instead of hardcoding a fallback to API 35, step down from the user's
sdkInt until we find an installable Google APIs image. Each API level
gets its own AVD (lantern-api29, lantern-api34, etc.) that persists
across runs, building up a catalog over time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* address review: install-before-push, fix eval injection, f-string, file search

- Install APK + launch once before pushing configs (so data dir exists)
- Replace eval with mapfile for device info extraction (no shell injection)
- Fix f-string syntax error in locations display
- Search both ticket-dir and config-dir for servers.json/split-tunnel.json
- Remove unused SCRIPT_DIR
- Update android-test header to document auto-AVD-creation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix private server navigation issue

* deps: bump sing-box-minimal to v1.12.21-lantern on refactor (#8679)

Companion to #8678. The refactor branch still pins v1.12.19-lantern,
which is missing the non-fatal-rule-set-fetch fix (sing-box-minimal
9c79c311, shipped in v1.12.21-lantern). Without it, Android builds
from this branch hit the same bootstrap deadlock.

* Add IPC starter in android

1

* macos, ios and android cleanup

* lantern-core: wire config events through IPC (#8673)

* lantern-core: subscribe to config events over IPC (/config/events)

The refactor branch removed listenConfigEvents when it was discovered
that the in-process events.SubscribeContext no longer worked — the
extension's radiance process is where config.NewConfigEvent is emitted,
and the host's subscription never fires across processes.

Now that the companion radiance PR adds a /config/events SSE endpoint,
restore the listener using lc.client.ConfigEvents with the same
reconnect-with-backoff pattern listenAutoSelectedEvents uses. Each
frame fires notifyFlutter(EventTypeConfig, "") so Flutter's
app_event_notifier "config" case resumes driving
availableServersProvider.forceFetchAvailableServers() and
homeProvider.fetchUserDataIfNeeded() on every config change.

Also bumps the radiance pin to the commit that adds the endpoint.

Addresses the config-events half of getlantern/engineering#3182.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* lantern-core: update StartBackgroundListeners comment to include config

Reflects that listenConfigEvents also starts automatically from
initialize, addressing Copilot review on PR #8673.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* add android-reproduce: reproduce Freshdesk tickets on emulator

Usage:
  android-reproduce /tmp/ticket-172722              # auto-downloads APK
  android-reproduce /tmp/ticket-172722 lantern.apk  # uses provided APK

After running /analyze-ticket, this script:
1. Extracts country + version from the ticket's config/logs
2. Downloads the matching APK from GitHub releases (gh CLI)
3. Pushes the user's exact config.json, servers.json, split-tunnel.json
   to the emulator so it gets the same proxies, DNS rules, rule sets
4. Sets RADIANCE_COUNTRY to match the user's region
5. Installs, restarts, and streams filtered logcat

This gives near-exact reproduction of Android-specific issues by
replicating the user's proxy assignments, country routing, and
sing-box config on a local emulator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Adam Fisk <afisk@mini.local>

* windows ffi cleanup

* Update bindings

* point to radiance refactor branch

* feat(dev-mode): hidden 5-tap unlock on support view + expanded dev screen

Show Build number alongside Lantern version on the support view. Tapping
the Build row 5× within 3s toggles developer mode (gated to nightly/debug
builds for enabling; disabling works anywhere). The developer entry in
settings now hides unless dev mode is enabled.

Developer screen adds radiance env-var overrides (country, version,
feature overrides), a log-level dropdown, a config-fetch toggle, and
buttons to send a config request, run URL tests, show live settings/env,
and disable dev mode. Pins qpack to v0.5.1 via replace directive to match
radiance's own pin so sing-box-minimal's quic-go HTTP/3 continues to
build.

Wires radiance ipc.Client.PatchSettings / PatchEnvVars / RunOfflineURLTests
/ UpdateConfig through lantern-core and exports them via FFI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bump radiance - limit config fetch to 1 at a time

* feat(dev-mode): show spinner on in-flight action tiles

Tapping Send config request / Run URL tests / Show settings & env vars
now disables the tile and shows a spinner until the IPC call returns, so
users don't assume the button is broken during the latency before the
snackbar appears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Developer mode refactor

* Sync garmr/radiance-daemon-refactor with origin/main (#8684)

* deps: update radiance to fix outbound removal breaking config refresh (#8639)

Picks up radiance PR #405 which fixes removeOutbounds failing when
extra outbounds (non-smart Pro locations) aren't in the URL test group.
This was causing every config refresh IPC to return 500, preventing
SetURLOverrides and CheckOutbounds from running — resulting in ~50%
of bandit probe callbacks never firing.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Smart location country fix (#8638)

* Do not reset a smart location.

* code review updates

* Fix website split-tunneling reliability and CI validation (#8640)

* Add split tunneling e2e test

* Fix split tunneling website smoke assertion

* Fix split tunneling smoke navigation

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* Macos system extension updates #2 (#8637)

* if system extension is in uninstall state do not block new installtion.

* update macos system extension test

* do not cache dart_tool

* Set the default status as unknown.

* code review updates

* Filter system apps from Windows split tunneling (#8641)

* Add split tunneling e2e test

* Fix split tunneling website smoke assertion

* Fix split tunneling smoke navigation

* code review updates

* code review updates

* code review updates

* Filter Windows system apps in split tunneling list

* code review updates

* code review updates

* Update system apps filter

* code review updates

* deps: update radiance + lantern-box to fix ~20% callback failure (#8642)

Picks up:
- radiance PR #406 → lantern-box PR #231: clear URL test history
  when SetURLOverrides is called so outbounds are re-tested with
  new callback URLs
- radiance PR #405: best-effort URL test group removal (already in
  previous update, carried forward)
- lantern-box v0.0.61: includes CA cert install + history fix

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update radiance + lantern-box for callback-all-outbounds (#8644)

- radiance: removes URL test filtering, all outbounds tested (PR #407)
- lantern-box v0.0.62: 6-worker URL test pool + client delay reporting (PR #232)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Hide system apps without dropping user apps on Windows (#8643)

* code review updates

* code review updates

* code review updates

* chore: update radiance for async IPC outbound handlers (#8645)

Picks up getlantern/radiance#410: IPC outbound update/add/remove
handlers return 202 immediately and process asynchronously, fixing
the EOF errors on every config refresh.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update radiance for split tunnel persistence fix (#8646)

Picks up getlantern/radiance#411: fixes split tunnel filters silently
not persisting due to dangling slice pointers in initRuleMap.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: upload and notify for nightlies even when some platforms fail (#8649)

The upload-s3 and upload-release-artifacts jobs required ALL platform
builds to succeed or be skipped. When a matrix entry failed (e.g.,
Linux arm64), the entire build-linux job reported as 'failure', which
caused both upload jobs to skip entirely — even though macOS, Android,
iOS, and Linux amd64 all succeeded.

Simplify the condition: run uploads if at least one platform build
succeeded. The upload steps already handle missing artifacts gracefully
(upload_if_exists checks for file existence).

This ensures the Slack notification goes out with download links for
whatever platforms did build successfully.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Stabilize nightly smoke checks and platform release publishing (#8651)

* Stabilize nightly smoke checks and platform release publishing

* code review updates

* code review updates

* chore: bump radiance to latest main (lantern-box v0.0.65) (#8654)

Picks up:
- Reflex active-probe resistance: silence-timeout + masquerade
  fallback (getlantern/lantern-box#237 via radiance#413)
- TLS 1.3 minimum enforcement for Reflex
  (getlantern/lantern-box#236)
- radiance split-tunnel filter persistence fix (#411)

No Flutter / client-side behavior changes required — the Reflex
hardening is server-side.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add arch to releases (#8652)

* feat: add arch to releases

* Update linux/packaging/usr/lib/systemd/system/lanternd.service

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: remove committed lanternd.service file

Agent-Logs-Url: https://github.com/getlantern/lantern/sessions/15085485-3c6a-4e1e-93ea-6e9bf0623d09

Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

* ran go mod tidy

* Improve Windows app discovery for shortcut wrappers (#8653)

* code review updates

* Improve Windows app discovery for shortcut wrappers

* code review updates

* code review updates

* code review updates

* The radiance-to-device limit is flow fix. (#8659)

* only use permalinks (#8658)

Co-authored-by: atavism <paul@getlantern.org>

* Add auth E2E tests and wire Linux/Windows CI (#8607)

* auth flow test updates

* auth flow test updates

* auth flow test updates

* code review updates

* code review updates

* code review updates

* code review updates

* deps: update sing-box-minimal to v1.12.21-lantern (#8660)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Show vpn conflict dialog on smart location (#8661)

* Show vpn conflict dialog on smart location

* code review updates

* chore: bump radiance and lantern-box to latest (#8664)

- radiance: f1c425231e41 → 4241e6c5a9c6 (main HEAD)
- lantern-box: v0.0.65 → v0.0.67

Ran go mod tidy.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Windows installer cleanup, improve app discovery and icon loading (#8666)

* code review updates

* Add comment

* code review updates

* remove sentry (#8665)

* Save last server location (#8655)

* save server location

* update radiance.

* Forbid AutoConnect if connect fails.

* update radiance

* code review updates

* update radiance

* code review updates (#8675)

* deps: restore sing-box-minimal v1.12.21-lantern (#8678)

PR #8655 ("Save last server location") accidentally downgraded
sing-box-minimal from v1.12.21-lantern back to v1.12.19-lantern in
go.mod during review churn. v1.12.21-lantern contains commit 9c79c311
("fix: make initial remote rule-set fetch non-fatal"), which turns the
Android bootstrap deadlock ("no available network interface" during
initial rule-set fetch) from a fatal libbox startup error into a
WARN + retry-after-start. Without it, nightly builds from main fail
to connect on any smart-routing country (Macao, Bulgaria, etc.).

Confirmed by comparing Freshdesk #172722 (broken, rule_set_remote.go:235,
v1.12.19-lantern) with #172795 (working, rule_set_remote.go:113,
v1.12.21-lantern). Same user, same device, same 9.0.25 version, same
smart-routing-bg-common-direct fetch failure — only the sing-box-minimal
version differs. The v9.0.25-beta-android tag was cut before #8655
merged, which is why Alexander's beta works while the nightly doesn't.

`go mod tidy` also dropped stale go.sum entries for superseded radiance
and lantern-box pseudo-versions and removed the unused getsentry/sentry-go
indirect (left behind after #8665).

* Makefile: fix empty common.Version on Windows CI (missing app version 400) (#8677)

* Makefile: use env-provided APP_VERSION so Windows CI populates version ldflag

common.Version in radiance was being linked as an empty string on Windows
CI builds. The `-X .../common.Version=$(APP_VERSION_PUBSPEC)` ldflag
depended on `$(shell grep ... | sed ...)` or a PowerShell fallback, and
the Windows path was producing an empty value. With common.Version empty,
backend.NewRequestWithHeaders sets X-Lantern-App-Version to "", and
lantern-cloud's /v1/config-new handler rejects the request with
400 "missing app version" — no config is returned, so the client falls
back to the embedded server list with no bandit tracks. Observed on
Freshdesk #172794 (Windows 9.0.26 nightly, radiance 400s on every retry).

Use the APP_VERSION already exported to GITHUB_ENV by build-windows.yml's
"Read app version from pubspec.yaml" step, and compute APP_VERSION_PUBSPEC
with Make built-ins ($(firstword $(subst +, ,...))) so no shell tools are
required. Drops the Windows_NT branch; local dev on Mac/Linux still uses
the grep/sed fallback (APP_VERSION ?=).

* Makefile: restore Windows local-dev fallback for APP_VERSION

The previous commit removed the Windows_NT branch under the assumption
that APP_VERSION would always come from the environment. That's true on
CI (build-windows.yml exports it to GITHUB_ENV), but local Windows
developers running `make windows-release` directly don't set the env
var, and the grep/sed fallback runs under cmd.exe where Unix-style
quoting fails silently.

Add back the Windows PowerShell branch, but only as the fallback when
APP_VERSION isn't in the environment (`?=` on both branches). CI keeps
working via the env override; local Mac/Linux uses grep/sed; local
Windows uses PowerShell Select-String. The `+`-splitting stays in
Make built-ins so it works no matter which branch produced APP_VERSION.

* Makefile: fail the build when APP_VERSION_PUBSPEC ends up empty

Adds a parse-time guard so an unresolvable version fails loudly rather
than producing a binary with empty common.Version — which is what caused
this whole bug in the first place. Addresses Copilot review feedback on
PR #8677.

$ APP_VERSION="" make
Makefile:36: *** APP_VERSION_PUBSPEC is empty; export APP_VERSION ...

* Roll in #8676: PowerShell quoting + Windows service startup log

Incorporates the non-overlapping pieces of @atavism's PR #8676 so we
can close it in favor of this PR:

- Swap the Windows APP_VERSION fallback's PowerShell invocation to
  outer-single / inner-double quoting. The previous outer-double /
  inner-single form gets mangled when Make expands $$ and cmd.exe
  passes the resulting string to powershell, even in the local-dev
  fallback path.
- Same fix for GO_VERSION's PowerShell shell-out further down in the
  Makefile (separate variable, same root cause).
- Log the Windows service startup (name, version, mode) so it's
  visible when triaging issues. Matches the log line from #8676.

* Fix data cap issue (#8668)

* Report an Issue screen fixes (#8670)

* updates to report issue screen

* updates to report issue screen

* rename report issue

* rename report issue

* code review updates

* ffi: add missing base64 import for app icon encoding

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

---------

Co-authored-by: Myles Horton <afisk@getlantern.org>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: jigar-f <132374182+jigar-f@users.noreply.github.com>
Co-authored-by: Ilya Yakelzon <reflog@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>
Co-authored-by: Jay <110402935+jay-418@users.noreply.github.com>

* code review updates

* bump radiance

* bump go to v1.26.2

Go v1.26.2 includes a patch to CGo that addresses some of
the bulkBarrierPreWrite panics.

* bump radiance - fix event streams on mobile

* fix sign up issue to point new radiance.

* split tunneling: treat FFI "ok" response as success, not error (#8691)

* split tunneling: treat FFI "ok" response as success, not error

_runSplitTunnelCall was checking `result != nullptr` and treating any
non-null return as an error message. But the Go FFI
(lantern-core/ffi/ffi.go) returns C.CString("ok") on success for both
addSplitTunnelItem and removeSplitTunnelItem — a non-null C string.

As a result, every successful add/remove was being reported to the UI as
a failure with message "ok". Symptoms:

- Adding a website in split tunneling showed an unstyled default
  snackbar reading "OK" (the default Material SnackBar rendering
  failure.localizedErrorMessage).
- The website appeared to not be saved — but it actually was; the
  provider's `reloaded` flag was never set, so the on-screen list never
  re-fetched from the backend.
- Re-clicking "Add" with the same domain created a duplicate entry on
  disk (visible as repeated items in split-tunnel.json) because the
  provider's local "already-added" check worked against a stale copy
  that had never been refreshed.

Fix: mirror the checkAPIError convention — treat literal "ok" as
success, parse JSON {"error": "..."} bodies for the error message, and
fall back to the raw string otherwise.

Reported in getlantern/engineering#3291 against Windows 9.0.29 build 481
(Freshdesk #173656).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* split tunneling: reuse _ffiOkResults for success-string check

Rather than hardcoding 'ok', use the existing _ffiOkResults set
({'ok', 'true'}) defined at the top of this file so the split-tunnel
path stays in sync with the other FFI success checks (e.g.
_setupRadiance at line 201).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* split tunneling: use design-system error snackbar on add (#8692)

The local showSnackbar helper in website_domain_input was using
Material's default ScaffoldMessenger.showSnackBar(SnackBar(content:
Text(message))) — producing an unstyled grey/dark snackbar that the rest
of the app doesn't use. Every call site in this file is an error path
(empty input, invalid domain, already-added, backend failure), so route
them through context.showSnackBarError which applies the app's rounded,
floating, red-background error style.

Follow-up to #8691. Addresses the "unstyled snackbar" symptom in
getlantern/engineering#3291 issue 3 for any remaining error surface
after the FFI "ok" fix.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(vpn): use SelectServer when switching servers on a live tunnel

 connectToServer previously always called ConnectVPN, which radiance
 rejects with ErrTunnelAlreadyConnected when the tunnel is up. Check
 VPNStatus first and route to SelectServer when Connected, falling
 back to ConnectVPN otherwise.

* android: detach connect() scope so withTimeout actually unblocks the UI (#8689)

* review: detach connect() scope so timeout actually unblocks the UI

Copilot flagged on #8689 that the existing coroutineScope { ... } still
hangs in exactly the scenario this change is meant to protect against.
Structured coroutineScope cancels its children on exception but then
waits for them to complete — so when withTimeout fires, we cancel the
deferred (which the JNI call ignores, since it has no suspension
points) and then block on it finishing anyway. Net effect: the UI is
still frozen, which is the symptom we're trying to prevent.

Switch to a DETACHED CoroutineScope(SupervisorJob() + Dispatchers.IO).
Its Job is not a child of the enclosing coroutine, so cancelling it
doesn't join — the orphan coroutine keeps running the JNI call in the
background until Go returns or the process exits, but the caller is
unblocked and the runCatching.onFailure path fires the timeout error
state for the UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: add single-flight gate to prevent orphan accumulation

Copilot correctly pointed out on #8689 that the detached-scope approach
can accumulate orphan coroutines if the user retries while a previous
connect() is still stuck in JNI. Each orphan pins a Dispatchers.IO
thread; enough retries against a truly deadlocked Go side could
pressure the IO pool.

Their suggested fix (Dispatchers.IO.limitedParallelism(1)) would
serialize retries behind the orphan, turning the 2nd retry into
another 60s hang. A simple single-flight AtomicBoolean gate with fast
rejection is the cleaner mitigation:

- compareAndSet rejects concurrent attempts with IllegalStateException
  (surfaces via the existing runCatching.onFailure → error state).
- The flag clears in a try/finally inside the async block, which runs
  when the JNI call eventually returns — cancellation alone can't
  break it out, but once Go completes the finally runs and a future
  retry is admitted.
- Process death (reboot, force-stop) resets the flag naturally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Show the fastest location on smart location.

* android: make restartService block until restart completes (#8697)

* android: make restartService block until restart completes

Two bugs in the platformIfce restart path that together let the tunnel
wedge in Restarting forever on Android, triggering the "Error in VPN
operation" on every subsequent Connect attempt
(getlantern/engineering#3297, Freshdesk #173681).

1. restartService() used serviceScope.launch { ... } and returned
   immediately. Radiance's Restart() treats the sync return as "restart
   succeeded" and leaves the tunnel at status=Restarting, expecting the
   platform coroutine to drive it through stopVPN → startVPN and
   transition status via Mobile.* side-effects. If the service is torn
   down before the coroutine completes (onDestroy, process pressure),
   nothing ever transitions the tunnel out of Restarting.

   Switch to runBlocking(Dispatchers.IO) so the return actually
   reflects completion. c.mu is released on the Go side before
   RestartService is invoked, so synchronous Mobile.* callbacks on
   this thread don't deadlock.

2. stopVPNTunnel() skipped Mobile.stopVPN() when Mobile.isVPNConnected()
   returned false. isVPNConnected is status == Connected — but at the
   point stopVPNTunnel is called from restartService, radiance has
   already set status=Restarting, so the guard always skips and the
   tunnel is never actually closed.

   Swap the guard for Mobile.isRadianceConnected() — i.e. only skip
   when the IPC server itself isn't up. Mobile.stopVPN() is a no-op
   when c.tunnel is nil on the Go side, so the original guard was
   redundant even for the Connected == true case.

Evidence from Freshdesk #173681 logs for the broken path:
- 15:17:34.826 Restart → 15:17:34.828 "Tunnel restarted successfully"
  (2ms total — consistent with fire-and-forget, not real teardown)
- No subsequent tunnel.init / Tunnel connection established
- 15:19:10 onDestroy logs "Skipping stopVPN — VPN tunnel was never
  started" (same isVPNConnected() check)
- 15:21:48 next Connect fails within 2ms of the IPC request with
  "tunnel is currently Restarting"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* android: drop isVPNConnected guard in onDestroy too

Same shape as the restart-path fix: if c.tunnel is non-nil on the Go
side but the tunnel status is anything other than Connected (Restarting
after a failed restart, Connecting mid-startup, Error from a prior
failure), isVPNConnected() returns false and the old guard skipped
Mobile.stopVPN(). That left the radiance tunnel state dangling across
service destroy.

Observed in Freshdesk #173681: "onDestroy — radianceConnected=true
vpnConnected=false, Skipping stopVPN — VPN tunnel was never started"
while the tunnel was actually alive at status=Restarting.

Swap the second guard for an unconditional call. Mobile.stopVPN() is a
no-op when c.tunnel is nil, so the guard was always redundant — it just
happened to also hide the non-Connected-but-non-nil case that's
load-bearing during restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* android: verify restart postcondition before returning to Go

launchVPN wraps its body in runCatching { ... }.onFailure { ... } and
returns normally regardless of whether Mobile.startVPN() threw — so a
nil return from startVPN() does not mean the restart succeeded. Without
a postcondition check, restartService would log "completed" and return
to radiance as if everything worked, even though the tunnel is still
stuck in Restarting, which defeats the whole point of making this
function block.

Check Mobile.isVPNConnected() at the end of the runBlocking block and
throw IllegalStateException if false. The exception propagates through
runBlocking → restartService → radiance's platformIfce.RestartService()
as a non-nil error, so Restart() hits the ErrorStatus branch and the
caller sees the failure.

Addresses Copilot review feedback on PR #8697.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Adam Fisk <afisk@mini.local>

* fix(vpn): don't cancel tunnel when restart's start phase fails

The PacketTunnelExtension hosts the IPC server, so cancelTunnelWithError
tears down the daemon along with the tunnel. Inline MobileStartVPN in
restartService so a failed restart leaves the extension (and IPC socket)
alive; radiance's status events surface the failure for retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix copilot issue (#8696) (#8698)

Co-authored-by: atavism <atavism@users.noreply.github.com>

* main: don't block first paint on Updater.init() (#8699)

* main: don't block first paint on Updater.init()

Moving Updater.init() off the critical path to runApp. Investigating a
one-shot black-screen-on-startup report on a local macOS dev build
(9.0.29 build 487): flutter.log stopped at the last pre-runApp log line
with no Dart exception and no crash, while the Go side kept running
normally. The only awaited call between that last log and runApp is
Updater.init().

Inside init(), the actual update check is already deferred 45 s via
Future.delayed + unawaited. But setFeedURL and setScheduledCheckInterval
are awaited — both bridge into Sparkle via the auto_updater Flutter
plugin, and both can stall on first launch: feed URL resolution,
keychain access, or a previous launch's background worker still holding
a lock. Any of those becomes a main-isolate hang that prevents runApp,
which exactly matches the observed symptom.

Fix: drop the await so Updater.init() runs concurrently with the rest
of startup. All errors are already handled inside init() itself, so
unawaited is safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: guard sl<Updater>() lookup against failed service injection

Copilot flagged that if injectServices() throws above (caught at
main.dart:45), Updater is never registered (it's registered at
injection_container.dart:40, after storage init), and sl<Updater>()
throws synchronously. unawaited() doesn't help — the throw happens
before the Future is constructed, so it propagates out of main and
prevents runApp.

Wrap the call in try/catch + sl.isRegistered<Updater>() so any failure
to look up or start Updater.init logs and continues to runApp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(logs): stream diagnostic logs via ipc TailLogs on desktop

Wires the FFI path to radiance's ipc.Client.TailLogs and merges in-app
flutter.log records so the diagnostic logs view shows both sources.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deps: bump radiance to refactor tip (9703bcf) (#8700)

Picks up:
- refactor(vpn): own VPN status on the client so restarts span tunnels
- vpn: instrument tunnel.start phases + VPNClient.Restart (#443)

The VPN-status-ownership refactor moves setStatus calls out of
tunnel and onto VPNClient so a restart transitions Restarting →
Disconnecting → Disconnected → Connecting → Connected cleanly.

The instrumentation PR adds child spans around libbox.Setup,
libbox.NewServiceWithContext, libbox.BoxService.Start, and
newMutableGroupManager so SigNoz can attribute the 10s+ tail
on /service/start observed in Freshdesk #173696.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix server auto issue

* More fix to server selection.

* server selection changes for IOS/Macos

* Use select sever if vpn is active.

* bump radiance - pull in empty tag fix

* lantern-core: dispatch ConnectVPN/StartVPN to SelectServer on live tunnel (#8702)

* lantern-core: dispatch ConnectVPN to SelectServer on live tunnel

When the Flutter UI triggers an auto-select on a live tunnel — most
visibly Jigar's rewrite of onSmartLocation (server_selection.dart), which
routes "switch back to Smart" through startVPN(force: true) → Dart
lantern.startVPN() → ffi.go:startVPN → c.ConnectVPN("") — radiance's
/vpn/connect endpoint rejects the request with ErrTunnelAlreadyConnected
(radiance/vpn/vpn.go:126 in VPNClient.Connect). The error is returned to
the Dart UI as a snackbar, the tunnel stays pinned to the previously
selected manual server, and lantern.log is silent because neither
LocalBackend.ConnectVPN nor VPNClient.Connect slog the ErrTunnelAlready
Connected path.

Observed on 9.0.30 beta (internal tester, Freshdesk #173763, build from
commit 405468954 which includes Jigar's 289507280). After manually
picking Bogotá, clicking "Smart" at the top of the server-selection
screen surfaces the snackbar and the tunnel keeps routing traffic
through the Bogotá samizdat outbound.

Fix: when Status() == Connected, LanternCore.ConnectVPN dispatches the
request to /server/selected (the live-tunnel outbound swap) instead of
/vpn/connect. Empty tag normalizes to vpn.AutoSelectTag — Dart sends ""
for Smart, radiance recognizes only the literal "auto" and otherwise
falls into the manual-outbound branch of SelectServer, stranding Clash
in manual mode with an empty selector. The mapping is centralized in a
small normalizeAutoTag helper used by both ConnectVPN and SelectServer.

This puts the same dispatch logic that lives in ffi.go:connectToServer
onto every caller of LanternCore.ConnectVPN — including ffi.go:startVPN
(which Jigar's rewrite now funnels through) and any future FFI/mobile
entry point.

getlantern/engineering#3291 issue 3. Supersedes earlier work on
fisk/connect-dispatch-select-when-connected (485bf5a00), which was
scoped to this same dispatch but predated the current refactor branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* vpn_tunnel: dispatch StartVPN to SelectServer on live tunnel (mobile path)

Mobile.StartVPN (the gomobile entry point for Android MainActivity and
iOS VPNManager) routes through vpn_tunnel.StartVPN(client), which calls
client.ConnectVPN(ctx, vpn.AutoSelectTag) directly — bypassing
lanterncore.Core. Jigar's onSmartLocation rewrite dispatches "switch
back to Smart" through startVPN(force: true), which on Android/iOS
lands here. Same ErrTunnelAlreadyConnected bug as the FFI path fixed in
the previous commit.

Mirror the VPNStatus dispatch pattern garmr already added to
vpn_tunnel.ConnectToServer in 405468954: when Status() == Connected,
swap outbound via /server/selected; otherwise fall through to the
existing /vpn/connect start.

Together with the LanternCore.ConnectVPN dispatch, this closes the
Smart-from-connected bug on every platform (Windows FFI, Android/iOS
gomobile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ffi: drop now-redundant VPNStatus dispatch in connectToServer

LanternCore.ConnectVPN already routes to /server/selected when the
tunnel is live (added earlier in this PR), so ffi.go:connectToServer's
own VPNStatus check is duplicate work. Collapse to a single c.ConnectVPN
call — both the live-tunnel-swap and fresh-connect paths flow through
the dispatch one layer down.

Behavior unchanged. The "start service failed" error wrapper is kept
for Dart-side snackbar stability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* lantern-core: collapse dispatch to a single implementation in vpn_tunnel

Three functions had independent VPNStatus → SelectServer-vs-ConnectVPN
dispatches after the earlier commits: LanternCore.ConnectVPN,
vpn_tunnel.StartVPN (both added in this PR), and vpn_tunnel.ConnectToServer
(pre-existing from 405468954). Consolidate so vpn_tunnel.ConnectToServer
is the authoritative dispatch and the other two delegate.

- LanternCore.ConnectVPN → vpn_tunnel.ConnectToServer(lc.client, tag)
- vpn_tunnel.StartVPN → ConnectToServer(client, vpn.AutoSelectTag)

LanternCore.SelectServer keeps its own empty-tag normalization since its
scope is the one-shot SelectServer IPC, not the dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* lantern-core: drop client-side empty-tag normalization (radiance fac9089) (#8703)

Patrick's radiance fac9089 ("fix(vpn): treat the empty string as
AutoSelect in SelectServer") is now pinned on this branch via
72a6c6282. Radiance normalizes tag == "" → AutoSelectTag on both
ConnectVPN and SelectServer, so the client-side normalizations we
added earlier (normalizeAutoTag helper in core.go, `if tag == ""` in
vpn_tunnel.ConnectToServer) are redundant — radiance handles the Dart
"" convention uniformly.

Remove:
- LanternCore.normalizeAutoTag helper + its use in SelectServer
- `if tag == "" { tag = vpn.AutoSelectTag }` branch in
  vpn_tunnel.ConnectToServer
- lantern-core/core_test.go (only tested the removed helper)

Behavior unchanged end-to-end: empty tag still means auto-select on
every path (FFI, gomobile, connectToServer, startVPN).

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bump radiance to refactor tip (d5a1872) — pull in LocalBackend.SelectServer empty-tag fix (#8705)

radiance@d5a1872 completes fac9089's empty-string → AutoSelectTag
normalization by extending it to LocalBackend.SelectServer, which
previously only matched the literal "auto" and fell through to the
srvManager lookup for tag == "" — producing "no server found with tag"
(HTTP 500, snackbar) on Smart-from-connected flows after the client-
side normalization was removed in this branch's 6de3c9aa9.

Reported on Lantern 9.0.30 beta via Freshdesk #173773.

go.mod + go.sum bump only; no lantern code changes. Pinned commit:
getlantern/radiance@d5a18726afbc (#444).

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Garmr/refactor mobile logstream (#8701)

* feat(logs): stream diagnostic logs via ipc TailLogs on mobile

Adds a mobile gomobile binding for ipc.Client.TailLogs (TailLogs +
LogSubscription) and switches Android and iOS to consume it, replacing
the per-platform log-file tailers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(logs): stream diagnostic logs via ipc TailLogs on macos

Switches the macOS log stream to MobileTailLogs, matching iOS. Removes
the file-watching LogTailer (no remaining callers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(logs): harden TailLogs against nil, panics, and listener leaks

- Reject nil listener in mobile.TailLogs; recover from panics crossing
  the gomobile bridge so the stream survives unexpected bridge errors.
- Retain the Kotlin LogListener in a field so the Go side's reference
  stays strongly rooted on the JVM.
- On iOS/macOS, cancel any pre-existing subscription before starting a
  new one and clear the stored listener when MobileTailLogs errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(logs): share TailLogs plumbing across mobile and ffi

Adds lantern-core/logs.Subscribe wrapping ipc.Client.TailLogs so the
mobile and desktop integrations go through one helper. Drops the iOS
LogTailer dead code and the unused lantern-core/logging package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update log formatting

* Fix issue with ios

* Fix macos logs issue

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jigar-f <jigar@getlantern.org>

* ffi + lantern-core: drop non-Linux preflight; bound IPC calls with per-operation timeouts (#8707)

* ffi: skip the daemon-reachability preflight on Windows / macOS / mobile

The 300 ms preflight in lantern-core/core.go's CheckDaemonReachable
was originally tuned for the Linux flow (PR #8494 by atavism, commit
bf054f4ea), where the failure path falls back to `systemctl is-active
lanternd.service` for a rich diagnostic error. The 300 ms cap made
sense as "fast probe → systemd-rich-error", with the systemd query
adding the actual user-facing context.

Subsequent refactors (commit bd89bea7e Apr 7, then PR #8578 commit
4d4e06d9d Apr 16) generalized that preflight to all platforms but
the systemd fallback only survived in ffi_linux.go. On Windows /
macOS / mobile, ffi_nonlinux.go ended up running the same 300 ms
probe with no fallback — just an artificial guillotine in front of
ConnectVPN, which has its own "lanternd not reachable" error path
with equivalent precision.

Cold-start IPC on Windows regularly exceeds 300 ms (named-pipe dial
+ winio impersonation token dance + H2c connection preface +
goroutine scheduling on a 96-second-idle daemon), so the first VPN
toggle after launch reliably trips the timeout and shows the user a
"lanternd not reachable" error. Clicking again 10 seconds later
silently succeeds. Reproduced on the same Windows machine across
9.0.29 (Freshdesk #173696) and 9.0.30 (#173932).

Make the preflight a no-op on non-Linux. Linux keeps the original
fast-probe-then-systemdDiag flow unchanged. If we add Windows
(`sc query LanternSvc`) or macOS (`launchctl list`) diagnostics
later, restore the preflight and call them from here.

See getlantern/engineering#3382 for the full archaeology + design
discussion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ffi + lantern-core: bound IPC calls with per-operation timeouts

Companion to dropping the non-Linux daemon-reachability preflight in this
same PR. The preflight (ffi_nonlinux.go's `checkDaemonReachable`) was
introduced in commit bd89bea7e along with the *removal* of per-call
timeouts that used to live on the FFI layer:

    -    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    -    if err := c.Client().DisconnectVPN(ctx); err != nil { ... }
    +    if err := c.DisconnectVPN(); err != nil { ... }

After that change, the only IPC call with any deadline at all was the
300 ms preflight. Every other operation flowed lc.ctx (
context.WithCancel(context.Background())) straight through, meaning a
hung lanternd would freeze the UI indefinitely. Dropping the preflight
without restoring per-call timeouts removes the only line of defense.

Restore them at the LanternCore layer where they belong, with values
sized for the inherent work each operation does (state changes can run
into multi-second territory; status queries should be near-instant):

    ipcConnectTimeout     = 60 * time.Second   // ConnectVPN
    ipcStateChangeTimeout = 30 * time.Second   // SelectServer, DisconnectVPN
    ipcStatusTimeout      = 10 * time.Second   // VPNStatus, IsVPNRunning

These bound the worst case (hung daemon → user sees a clear error within
a minute, no indefinite spinner) without firing during normal slow paths.
The dialer's 10 s connect timeout (radiance/ipc/conn_windows.go) already
covers the lanternd-crashed case; these guard the lanternd-hung case.

vpn_tunnel.{StartVPN, StopVPN, ConnectToServer} take the ctx through
their signatures instead of building their own context.Background()
internally, so callers stay in charge of their own deadlines. mobile/
mobile.go updated to set 60 s / 30 s / 60 s contexts on its three
gomobile entry points.

CheckDaemonReachable's 300 ms timeout is kept untouched — Linux still
calls it from ffi_linux.go for the systemctl is-active fallback that's
the whole point of the fast probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bump radiance

* lantern-core: fix empty Windows split-tunnel apps list + UI-process logging (#8709)

Two narrow fixes that together resolve Freshdesk #173774 / #173778 /
#173826 (Derek's "Failed to fetch installed apps" empty list on Windows
split tunneling). Split out from #8706 so they can land independently
of the broader app-discovery rework that PR also contained.

1. **GetEnabledApps returns []string{} instead of nil.**
   When no apps are split-tunneled, the previous code returned nil,
   which json.Marshal serialized as "null". Dart's jsonDecode("null")
   returns null; the receiving code does `as List`, which throws and
   the UI shows "Failed to fetch installed apps". Initializing as an
   empty slice serializes to "[]" — Dart parses that as an empty list,
   no exception, no error UI. THIS is the actual root cause of the
   empty-list reports we've been chasing; the apps-discovery scanner
   work was investigating a different (also-real but secondary) issue.

2. **UI-process slog wired up via common.Init.**
   On the refactor branch, the UI process never called common.Init.
   slog wrote to stderr (= nowhere on a GUI host), settings were
   uninitialized, no lantern.log was produced outside the daemon.
   Patrick caught this — it was a one-line miss in the refactor.

   Platform-aware so we don't double-init on platforms where the
   backend embeds in-process:
     - windows/linux: full common.Init (separate UI + daemon procs)
     - darwin/ios:    setupAppLogging into a distinct lantern-app.log
                      so the main-app slog doesn't race the tunnel
                      extension's lantern.log on lumberjack rotation
     - android:       Mobile.SetupRadiance already ran common.Init
                      upstream — fall through

3. **Auto-attach UI-process *.log to ReportIssue (windows/linux only).**
   Without it the daemon's archive glob only sees the daemon's logDir;
   UI-side lantern.log + flutter.log never reach the issue bundle. The
   daemon runs as SYSTEM on Windows; we keep UI logDir at
   %PUBLIC%\Lantern\logs so SYSTEM can read it.

The broader Windows app-discovery work from #8706 (App Paths scan, Run
keys, Squirrel pattern, isAppPathsNoise heuristic filters) is being
held in a separate PR for independent review.

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* android: init Go logging early so lantern-core/radiance debug logs aren't lost (#8711)

On Android the entire app runs in a single process, so once common.Init
runs slog.SetDefault covers everything. But common.Init only runs deep
inside SetupRadiance / StartIPCServer, which LanternVpnService launches
asynchronously from an intent fired by MainActivity.startLanternService.
Any slog call emitted in the gap — including any of the wide MethodHandler
surface that Flutter can reach before the VPN service is up — falls
through to the stdlib default (text → stderr → logcat at INFO), so DEBUG
logs vanish and the format diverges from what we use everywhere else.

Add Mobile.InitLogging as a thin gomobile-exposed wrapper around
common.Init, and call it from MainActivity.configureFlutterEngine before
startLanternService. common.Init is guarded by an atomic.Bool, so the
later call from backend.NewLocalBackend is a no-op.

Mirrors PR #8709 (Windows). Reported on Slack by Jigar.

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* FetchUserData at startup.

* Map the correct type to avoid a crash.

* bump radiance to main post refactor merge

PR getlantern/radiance#370 merged the long-lived refactor branch into
radiance main. This branch was previously pinned at the refactor tip
(5643163d8d70); repinning to main (e312570c7aea) so all downstream
work consumes the merged code.

Transitive: getlantern/kindling auto-bumps to 6143132aaf40 to match
the kindling.TransportName + domainfront API radiance now uses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Jigar-f <jigar@getlantern.org>
Co-authored-by: jigar-f <132374182+jigar-f@users.noreply.github.com>
Co-authored-by: atavism <atavism@users.noreply.github.com>
Co-authored-by: Myles Horton <afisk@getlantern.org>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ilya Yakelzon <reflog@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>
Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Jay <110402935+jay-418@users.noreply.github.com>
Co-authored-by: atavism <paul@getlantern.org>
atavism added a commit that referenced this pull request Apr 29, 2026
…ise filter (#8710)

* migrate to new ipc.Client api, first-pass

* pullin a couple fixes, update linux vpn status poller

* start ipc server ios/macos

* update radiance, fix linux daemon build

* start ipc server windows service

* fix datacap stream

* decode user response data to json

* gofmt

* update ipc request path check for linux smoke test

* Fixed issue with user apis

* redo linux packaging changes undone by merge

* move RunOffCgoStack from radiance to here, small cleanup

* fetch radiance-owned settings on demand instead of caching locally

* add missing smart-routing, ad-block, oauth calls

* clean up

* fix ref async issue for IPC calls

* gofmt

* fix test, linux package verification

* update radiance, remove server groups

* fix: return added server tags from AddServersByURL

Server tags are determined by URL content, not caller-supplied names.
addServerBasedOnURLs now returns the tags of added servers so callers
can connect using the actual tag. Also sends VPN status updates from
connectToServer on Linux so the UI reflects connection state changes.

* wrap ffi calls in runOnGoStack, update win service

* add explicit not linux build tag

* update radiance

* use RADIANCE_REPO in lanternd src

* flatten server model to match radiance, fix tests

* use loopback ipc client for mobile

* update radiance, log service install error in smoke test

* retrieve selected server from radiance instead of cacheing

* stop lantern before unintall, revert accidental service name change

* remove allow override

* fix name reference and misplaced stop call

* fix several issues

* code review

* fix toggles not registering and fetching plans

* always refetch server list when view opens

* fix crash in server select screen

* fix split tunnel website view not loading websites

* sync vpn status from system on launch

* fix stale onboarding marker persisting reinstall

* Revert "fix stale onboarding marker persisting reinstall"

This reverts commit a21a218eac7df90d678ce5d35d27892bbe893da2.

* fix vpn prompt displaying when quiting

* Macos system extension updates #2 (#8637)

* if system extension is in uninstall state do not block new installtion.

* update macos system extension test

* do not cache dart_tool

* Set the default status as unknown.

* code review updates

* Filter system apps from Windows split tunneling (#8641)

* Add split tunneling e2e test

* Fix split tunneling website smoke assertion

* Fix split tunneling smoke navigation

* code review updates

* code review updates

* code review updates

* Filter Windows system apps in split tunneling list

* code review updates

* code review updates

* Update system apps filter

* code review updates

* fix: upload and notify for nightlies even when some platforms fail (#8649)

The upload-s3 and upload-release-artifacts jobs required ALL platform
builds to succeed or be skipped. When a matrix entry failed (e.g.,
Linux arm64), the entire build-linux job reported as 'failure', which
caused both upload jobs to skip entirely — even though macOS, Android,
iOS, and Linux amd64 all succeeded.

Simplify the condition: run uploads if at least one platform build
succeeded. The upload steps already handle missing artifacts gracefully
(upload_if_exists checks for file existence).

This ensures the Slack notification goes out with download links for
whatever platforms did build successfully.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add arch to releases (#8652)

* feat: add arch to releases

* Update linux/packaging/usr/lib/systemd/system/lanternd.service

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: remove committed lanternd.service file

Agent-Logs-Url: https://github.com/getlantern/lantern/sessions/15085485-3c6a-4e1e-93ea-6e9bf0623d09

Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

* fix issues from 3173

* Refactor and fixed multiple bugs

* cache selected server location locally to avoid UI flash

* Fix tunnel issue in android

* App event issue and auto server location fixes

* added logs

* mobile: return string instead of []byte + update Swift callers (#8663)

* mobile: return string instead of []byte from gomobile-exported funcs

The gomobile wrapper copies Go pointer-containing return values to the C
thread stack using runtime.wbMove. When a GC cycle runs during the copy,
bulkBarrierPreWrite panics because the destination isn't GC-tracked.
Returning string avoids this — gomobile marshals strings via C heap
allocation rather than leaving them as Go slice headers.

See getlantern/engineering#3175 for the full crash analysis (from
Freshdesk #172640 — Derek reporting "Lantern Crash" on macOS 26.3.1).

Go changes:
  AvailableFeatures, UserData, FetchUserData, GetAvailableServers,
  GetSelectedServerJSON, OAuthLoginCallback, AcknowledgeGooglePurchase,
  AcknowledgeApplePurchase, Login, Logout, DeleteAccount

Swift changes (macos + ios): preserve Flutter contract by converting
the string back to Data for methods whose Dart side reads `bytes` via
utf8.decode (getUserData, fetchUserData, oauthLoginCallback, login,
logout, deleteAccount, acknowledgeInAppPurchase). For methods whose Dart
side expects String (featureFlags, getLanternAvailableServers,
getSelectedServerJSON), just pass the gomobile string directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* android: update MethodHandler for string-returning gomobile bindings

The gomobile-exported funcs in lantern-core/mobile/mobile.go now return
string instead of []byte. The generated Android binding will therefore
return String where it used to return ByteArray.

For each affected method, match what the iOS handler does so the Flutter
platform-channel contract stays stable:

  * Methods whose Dart callers expect bytes (Uint8List) — login,
    logout, deleteAccount, userData, fetchUserData, oauthLoginCallback,
    acknowledgeGooglePurchase — convert the String result via
    `.toByteArray(Charsets.UTF_8)` before calling success() (mirrors
    Swift's `.data(using: .utf8)`).

  * Methods whose Dart callers expect a String — availableFeatures,
    getAvailableServers, getSelectedServerJSON — drop the
    `String(byteArray)` constructor and use the return value directly,
    with the same "{}" / "[]" empty-default that iOS uses.

Addresses Copilot review on PR #8663.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* encapsulate ipc.Client behind LanternCore interface

Route all IPC operations through LanternCore methods instead of
exposing Client() to callers. Add GetSelectedServerTag,
GetAutoLocationJSON, CheckDaemonReachable, PatchSettings, and
VPNStatusEvents to the Core interface. Update FFI and mobile layers
to use them, and remove now-unused vpn_tunnel helper functions.

Also includes Flutter-side fixes: device-removal sign-in race
condition, plans fetch retry logic, and private server setup
improvements.

* ios/macos: drop invalid optional-chaining on non-optional String (#8671)

The gomobile-exported functions in lantern-core/mobile/mobile.go were
migrated from ([]byte, error) to (string, error). gomobile renders the
new signatures with a non-optional Swift String return (Data was
optional; String is not), so `json?.data(using: .utf8)` and
`payload?.data(using: .utf8)` now fail to compile:

    error: cannot use optional chaining on non-optional value of type
    'String'

Drop the `?` on all 14 call sites (7 each in ios/ and macos/). The
resulting `json.data(using: .utf8)` returns Data? anyway — an empty
Go string still produces a non-nil empty Data, which preserves the
Flutter contract the comment on these lines describes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* add android-test + android-reproduce for emulator testing and ticket reproduction (#8672)

* add android-test script for quick emulator testing with env overrides

Usage:
  scripts/android/android-test <apk> [ENV_KEY=VALUE ...]

Example:
  scripts/android/android-test lantern.apk RADIANCE_COUNTRY=BG RADIANCE_FEATURE_OVERRIDES=dns_ruleset_host_bypass

Starts an emulator, installs the APK, pushes a .env file with overrides
to the app's data dir (via adb root on Google APIs images, run-as on
debug APKs, or su on rooted devices), restarts the app, and streams
filtered logcat.

Prefers the "lantern-test" AVD if it exists (create with Google APIs
image for root access):
  sdkmanager "system-images;android-35;google_apis;arm64-v8a"
  avdmanager create avd -n lantern-test -k "system-images;android-35;google_apis;arm64-v8a"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* address review: serial targeting, su quoting, trap cleanup, fix comment

- Use -s <serial> throughout so multiple devices don't break adb
- Fix su -c quoting so $(stat ...) expands on-device
- Add trap to clean up temp .env on EXIT/INT/TERM
- Fix header comment (no /sdcard/ fallback)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-test: push .env to .lantern data dir (not app root)

The Go env package reads .env from the data directory (via
env.LoadFromDir called from common.Init), not from the app's root
data dir. Push to /data/data/$PKG/.lantern/.env so radiance finds it.

Companion: getlantern/radiance#421 (env.LoadFromDir)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-test: auto-install system image and create AVD if none exists

If no AVDs are found, the script now automatically:
1. Detects host arch (arm64 vs x86_64)
2. Installs the Google APIs system image via sdkmanager
3. Creates a "lantern-test" AVD via avdmanager

This means running android-test on a fresh machine with just the
Android SDK installed works out of the box — no manual AVD setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* address review: array for ADB_CMD, timeouts, remove unused PID

- Use bash array for ADB_CMD so paths with spaces work correctly
- Add configurable timeouts for emulator appear (120s) and boot (300s)
- Remove unused EMULATOR_PID — emulator intentionally left running
  between invocations so subsequent runs don't pay boot cost

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* add android-reproduce: reproduce Freshdesk tickets on emulator

Usage:
  android-reproduce /tmp/ticket-172722              # auto-downloads APK
  android-reproduce /tmp/ticket-172722 lantern.apk  # uses provided APK

After running /analyze-ticket, this script:
1. Extracts country + version from the ticket's config/logs
2. Downloads the matching APK from GitHub releases (gh CLI)
3. Pushes the user's exact config.json, servers.json, split-tunnel.json
   to the emulator so it gets the same proxies, DNS rules, rule sets
4. Sets RADIANCE_COUNTRY to match the user's region
5. Installs, restarts, and streams filtered logcat

This gives near-exact reproduction of Android-specific issues by
replicating the user's proxy assignments, country routing, and
sing-box config on a local emulator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-reproduce: match user's Android API level from ticket logs

Extracts sdkInt, osVersion, and model from flutter.log's "Device info"
line. Creates an AVD with the matching API level (e.g. "lantern-api36"
for a user on Android 16/SDK 36). Falls back to API 35 if the target
image isn't available.

Example for ticket #172722 (Android 16, SM-A556B):
  Creates lantern-api35 (API 36 clamped to 35), installs matching APK,
  pushes user's exact config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-reproduce: dynamically find closest available API image

Instead of hardcoding a fallback to API 35, step down from the user's
sdkInt until we find an installable Google APIs image. Each API level
gets its own AVD (lantern-api29, lantern-api34, etc.) that persists
across runs, building up a catalog over time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* address review: install-before-push, fix eval injection, f-string, file search

- Install APK + launch once before pushing configs (so data dir exists)
- Replace eval with mapfile for device info extraction (no shell injection)
- Fix f-string syntax error in locations display
- Search both ticket-dir and config-dir for servers.json/split-tunnel.json
- Remove unused SCRIPT_DIR
- Update android-test header to document auto-AVD-creation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix private server navigation issue

* deps: bump sing-box-minimal to v1.12.21-lantern on refactor (#8679)

Companion to #8678. The refactor branch still pins v1.12.19-lantern,
which is missing the non-fatal-rule-set-fetch fix (sing-box-minimal
9c79c311, shipped in v1.12.21-lantern). Without it, Android builds
from this branch hit the same bootstrap deadlock.

* Add IPC starter in android

1

* macos, ios and android cleanup

* lantern-core: wire config events through IPC (#8673)

* lantern-core: subscribe to config events over IPC (/config/events)

The refactor branch removed listenConfigEvents when it was discovered
that the in-process events.SubscribeContext no longer worked — the
extension's radiance process is where config.NewConfigEvent is emitted,
and the host's subscription never fires across processes.

Now that the companion radiance PR adds a /config/events SSE endpoint,
restore the listener using lc.client.ConfigEvents with the same
reconnect-with-backoff pattern listenAutoSelectedEvents uses. Each
frame fires notifyFlutter(EventTypeConfig, "") so Flutter's
app_event_notifier "config" case resumes driving
availableServersProvider.forceFetchAvailableServers() and
homeProvider.fetchUserDataIfNeeded() on every config change.

Also bumps the radiance pin to the commit that adds the endpoint.

Addresses the config-events half of getlantern/engineering#3182.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* lantern-core: update StartBackgroundListeners comment to include config

Reflects that listenConfigEvents also starts automatically from
initialize, addressing Copilot review on PR #8673.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* add android-reproduce: reproduce Freshdesk tickets on emulator

Usage:
  android-reproduce /tmp/ticket-172722              # auto-downloads APK
  android-reproduce /tmp/ticket-172722 lantern.apk  # uses provided APK

After running /analyze-ticket, this script:
1. Extracts country + version from the ticket's config/logs
2. Downloads the matching APK from GitHub releases (gh CLI)
3. Pushes the user's exact config.json, servers.json, split-tunnel.json
   to the emulator so it gets the same proxies, DNS rules, rule sets
4. Sets RADIANCE_COUNTRY to match the user's region
5. Installs, restarts, and streams filtered logcat

This gives near-exact reproduction of Android-specific issues by
replicating the user's proxy assignments, country routing, and
sing-box config on a local emulator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Adam Fisk <afisk@mini.local>

* windows ffi cleanup

* Update bindings

* point to radiance refactor branch

* feat(dev-mode): hidden 5-tap unlock on support view + expanded dev screen

Show Build number alongside Lantern version on the support view. Tapping
the Build row 5× within 3s toggles developer mode (gated to nightly/debug
builds for enabling; disabling works anywhere). The developer entry in
settings now hides unless dev mode is enabled.

Developer screen adds radiance env-var overrides (country, version,
feature overrides), a log-level dropdown, a config-fetch toggle, and
buttons to send a config request, run URL tests, show live settings/env,
and disable dev mode. Pins qpack to v0.5.1 via replace directive to match
radiance's own pin so sing-box-minimal's quic-go HTTP/3 continues to
build.

Wires radiance ipc.Client.PatchSettings / PatchEnvVars / RunOfflineURLTests
/ UpdateConfig through lantern-core and exports them via FFI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bump radiance - limit config fetch to 1 at a time

* feat(dev-mode): show spinner on in-flight action tiles

Tapping Send config request / Run URL tests / Show settings & env vars
now disables the tile and shows a spinner until the IPC call returns, so
users don't assume the button is broken during the latency before the
snackbar appears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Developer mode refactor

* Sync garmr/radiance-daemon-refactor with origin/main (#8684)

* deps: update radiance to fix outbound removal breaking config refresh (#8639)

Picks up radiance PR #405 which fixes removeOutbounds failing when
extra outbounds (non-smart Pro locations) aren't in the URL test group.
This was causing every config refresh IPC to return 500, preventing
SetURLOverrides and CheckOutbounds from running — resulting in ~50%
of bandit probe callbacks never firing.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Smart location country fix (#8638)

* Do not reset a smart location.

* code review updates

* Fix website split-tunneling reliability and CI validation (#8640)

* Add split tunneling e2e test

* Fix split tunneling website smoke assertion

* Fix split tunneling smoke navigation

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* Macos system extension updates #2 (#8637)

* if system extension is in uninstall state do not block new installtion.

* update macos system extension test

* do not cache dart_tool

* Set the default status as unknown.

* code review updates

* Filter system apps from Windows split tunneling (#8641)

* Add split tunneling e2e test

* Fix split tunneling website smoke assertion

* Fix split tunneling smoke navigation

* code review updates

* code review updates

* code review updates

* Filter Windows system apps in split tunneling list

* code review updates

* code review updates

* Update system apps filter

* code review updates

* deps: update radiance + lantern-box to fix ~20% callback failure (#8642)

Picks up:
- radiance PR #406 → lantern-box PR #231: clear URL test history
  when SetURLOverrides is called so outbounds are re-tested with
  new callback URLs
- radiance PR #405: best-effort URL test group removal (already in
  previous update, carried forward)
- lantern-box v0.0.61: includes CA cert install + history fix

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update radiance + lantern-box for callback-all-outbounds (#8644)

- radiance: removes URL test filtering, all outbounds tested (PR #407)
- lantern-box v0.0.62: 6-worker URL test pool + client delay reporting (PR #232)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Hide system apps without dropping user apps on Windows (#8643)

* code review updates

* code review updates

* code review updates

* chore: update radiance for async IPC outbound handlers (#8645)

Picks up getlantern/radiance#410: IPC outbound update/add/remove
handlers return 202 immediately and process asynchronously, fixing
the EOF errors on every config refresh.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update radiance for split tunnel persistence fix (#8646)

Picks up getlantern/radiance#411: fixes split tunnel filters silently
not persisting due to dangling slice pointers in initRuleMap.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: upload and notify for nightlies even when some platforms fail (#8649)

The upload-s3 and upload-release-artifacts jobs required ALL platform
builds to succeed or be skipped. When a matrix entry failed (e.g.,
Linux arm64), the entire build-linux job reported as 'failure', which
caused both upload jobs to skip entirely — even though macOS, Android,
iOS, and Linux amd64 all succeeded.

Simplify the condition: run uploads if at least one platform build
succeeded. The upload steps already handle missing artifacts gracefully
(upload_if_exists checks for file existence).

This ensures the Slack notification goes out with download links for
whatever platforms did build successfully.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Stabilize nightly smoke checks and platform release publishing (#8651)

* Stabilize nightly smoke checks and platform release publishing

* code review updates

* code review updates

* chore: bump radiance to latest main (lantern-box v0.0.65) (#8654)

Picks up:
- Reflex active-probe resistance: silence-timeout + masquerade
  fallback (getlantern/lantern-box#237 via radiance#413)
- TLS 1.3 minimum enforcement for Reflex
  (getlantern/lantern-box#236)
- radiance split-tunnel filter persistence fix (#411)

No Flutter / client-side behavior changes required — the Reflex
hardening is server-side.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add arch to releases (#8652)

* feat: add arch to releases

* Update linux/packaging/usr/lib/systemd/system/lanternd.service

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: remove committed lanternd.service file

Agent-Logs-Url: https://github.com/getlantern/lantern/sessions/15085485-3c6a-4e1e-93ea-6e9bf0623d09

Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

* ran go mod tidy

* Improve Windows app discovery for shortcut wrappers (#8653)

* code review updates

* Improve Windows app discovery for shortcut wrappers

* code review updates

* code review updates

* code review updates

* The radiance-to-device limit is flow fix. (#8659)

* only use permalinks (#8658)

Co-authored-by: atavism <paul@getlantern.org>

* Add auth E2E tests and wire Linux/Windows CI (#8607)

* auth flow test updates

* auth flow test updates

* auth flow test updates

* code review updates

* code review updates

* code review updates

* code review updates

* deps: update sing-box-minimal to v1.12.21-lantern (#8660)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Show vpn conflict dialog on smart location (#8661)

* Show vpn conflict dialog on smart location

* code review updates

* chore: bump radiance and lantern-box to latest (#8664)

- radiance: f1c425231e41 → 4241e6c5a9c6 (main HEAD)
- lantern-box: v0.0.65 → v0.0.67

Ran go mod tidy.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Windows installer cleanup, improve app discovery and icon loading (#8666)

* code review updates

* Add comment

* code review updates

* remove sentry (#8665)

* Save last server location (#8655)

* save server location

* update radiance.

* Forbid AutoConnect if connect fails.

* update radiance

* code review updates

* update radiance

* code review updates (#8675)

* deps: restore sing-box-minimal v1.12.21-lantern (#8678)

PR #8655 ("Save last server location") accidentally downgraded
sing-box-minimal from v1.12.21-lantern back to v1.12.19-lantern in
go.mod during review churn. v1.12.21-lantern contains commit 9c79c311
("fix: make initial remote rule-set fetch non-fatal"), which turns the
Android bootstrap deadlock ("no available network interface" during
initial rule-set fetch) from a fatal libbox startup error into a
WARN + retry-after-start. Without it, nightly builds from main fail
to connect on any smart-routing country (Macao, Bulgaria, etc.).

Confirmed by comparing Freshdesk #172722 (broken, rule_set_remote.go:235,
v1.12.19-lantern) with #172795 (working, rule_set_remote.go:113,
v1.12.21-lantern). Same user, same device, same 9.0.25 version, same
smart-routing-bg-common-direct fetch failure — only the sing-box-minimal
version differs. The v9.0.25-beta-android tag was cut before #8655
merged, which is why Alexander's beta works while the nightly doesn't.

`go mod tidy` also dropped stale go.sum entries for superseded radiance
and lantern-box pseudo-versions and removed the unused getsentry/sentry-go
indirect (left behind after #8665).

* Makefile: fix empty common.Version on Windows CI (missing app version 400) (#8677)

* Makefile: use env-provided APP_VERSION so Windows CI populates version ldflag

common.Version in radiance was being linked as an empty string on Windows
CI builds. The `-X .../common.Version=$(APP_VERSION_PUBSPEC)` ldflag
depended on `$(shell grep ... | sed ...)` or a PowerShell fallback, and
the Windows path was producing an empty value. With common.Version empty,
backend.NewRequestWithHeaders sets X-Lantern-App-Version to "", and
lantern-cloud's /v1/config-new handler rejects the request with
400 "missing app version" — no config is returned, so the client falls
back to the embedded server list with no bandit tracks. Observed on
Freshdesk #172794 (Windows 9.0.26 nightly, radiance 400s on every retry).

Use the APP_VERSION already exported to GITHUB_ENV by build-windows.yml's
"Read app version from pubspec.yaml" step, and compute APP_VERSION_PUBSPEC
with Make built-ins ($(firstword $(subst +, ,...))) so no shell tools are
required. Drops the Windows_NT branch; local dev on Mac/Linux still uses
the grep/sed fallback (APP_VERSION ?=).

* Makefile: restore Windows local-dev fallback for APP_VERSION

The previous commit removed the Windows_NT branch under the assumption
that APP_VERSION would always come from the environment. That's true on
CI (build-windows.yml exports it to GITHUB_ENV), but local Windows
developers running `make windows-release` directly don't set the env
var, and the grep/sed fallback runs under cmd.exe where Unix-style
quoting fails silently.

Add back the Windows PowerShell branch, but only as the fallback when
APP_VERSION isn't in the environment (`?=` on both branches). CI keeps
working via the env override; local Mac/Linux uses grep/sed; local
Windows uses PowerShell Select-String. The `+`-splitting stays in
Make built-ins so it works no matter which branch produced APP_VERSION.

* Makefile: fail the build when APP_VERSION_PUBSPEC ends up empty

Adds a parse-time guard so an unresolvable version fails loudly rather
than producing a binary with empty common.Version — which is what caused
this whole bug in the first place. Addresses Copilot review feedback on
PR #8677.

$ APP_VERSION="" make
Makefile:36: *** APP_VERSION_PUBSPEC is empty; export APP_VERSION ...

* Roll in #8676: PowerShell quoting + Windows service startup log

Incorporates the non-overlapping pieces of @atavism's PR #8676 so we
can close it in favor of this PR:

- Swap the Windows APP_VERSION fallback's PowerShell invocation to
  outer-single / inner-double quoting. The previous outer-double /
  inner-single form gets mangled when Make expands $$ and cmd.exe
  passes the resulting string to powershell, even in the local-dev
  fallback path.
- Same fix for GO_VERSION's PowerShell shell-out further down in the
  Makefile (separate variable, same root cause).
- Log the Windows service startup (name, version, mode) so it's
  visible when triaging issues. Matches the log line from #8676.

* Fix data cap issue (#8668)

* Report an Issue screen fixes (#8670)

* updates to report issue screen

* updates to report issue screen

* rename report issue

* rename report issue

* code review updates

* ffi: add missing base64 import for app icon encoding

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

---------

Co-authored-by: Myles Horton <afisk@getlantern.org>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: jigar-f <132374182+jigar-f@users.noreply.github.com>
Co-authored-by: Ilya Yakelzon <reflog@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>
Co-authored-by: Jay <110402935+jay-418@users.noreply.github.com>

* code review updates

* bump radiance

* bump go to v1.26.2

Go v1.26.2 includes a patch to CGo that addresses some of
the bulkBarrierPreWrite panics.

* bump radiance - fix event streams on mobile

* fix sign up issue to point new radiance.

* split tunneling: treat FFI "ok" response as success, not error (#8691)

* split tunneling: treat FFI "ok" response as success, not error

_runSplitTunnelCall was checking `result != nullptr` and treating any
non-null return as an error message. But the Go FFI
(lantern-core/ffi/ffi.go) returns C.CString("ok") on success for both
addSplitTunnelItem and removeSplitTunnelItem — a non-null C string.

As a result, every successful add/remove was being reported to the UI as
a failure with message "ok". Symptoms:

- Adding a website in split tunneling showed an unstyled default
  snackbar reading "OK" (the default Material SnackBar rendering
  failure.localizedErrorMessage).
- The website appeared to not be saved — but it actually was; the
  provider's `reloaded` flag was never set, so the on-screen list never
  re-fetched from the backend.
- Re-clicking "Add" with the same domain created a duplicate entry on
  disk (visible as repeated items in split-tunnel.json) because the
  provider's local "already-added" check worked against a stale copy
  that had never been refreshed.

Fix: mirror the checkAPIError convention — treat literal "ok" as
success, parse JSON {"error": "..."} bodies for the error message, and
fall back to the raw string otherwise.

Reported in getlantern/engineering#3291 against Windows 9.0.29 build 481
(Freshdesk #173656).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* split tunneling: reuse _ffiOkResults for success-string check

Rather than hardcoding 'ok', use the existing _ffiOkResults set
({'ok', 'true'}) defined at the top of this file so the split-tunnel
path stays in sync with the other FFI success checks (e.g.
_setupRadiance at line 201).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* split tunneling: use design-system error snackbar on add (#8692)

The local showSnackbar helper in website_domain_input was using
Material's default ScaffoldMessenger.showSnackBar(SnackBar(content:
Text(message))) — producing an unstyled grey/dark snackbar that the rest
of the app doesn't use. Every call site in this file is an error path
(empty input, invalid domain, already-added, backend failure), so route
them through context.showSnackBarError which applies the app's rounded,
floating, red-background error style.

Follow-up to #8691. Addresses the "unstyled snackbar" symptom in
getlantern/engineering#3291 issue 3 for any remaining error surface
after the FFI "ok" fix.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(vpn): use SelectServer when switching servers on a live tunnel

 connectToServer previously always called ConnectVPN, which radiance
 rejects with ErrTunnelAlreadyConnected when the tunnel is up. Check
 VPNStatus first and route to SelectServer when Connected, falling
 back to ConnectVPN otherwise.

* android: detach connect() scope so withTimeout actually unblocks the UI (#8689)

* review: detach connect() scope so timeout actually unblocks the UI

Copilot flagged on #8689 that the existing coroutineScope { ... } still
hangs in exactly the scenario this change is meant to protect against.
Structured coroutineScope cancels its children on exception but then
waits for them to complete — so when withTimeout fires, we cancel the
deferred (which the JNI call ignores, since it has no suspension
points) and then block on it finishing anyway. Net effect: the UI is
still frozen, which is the symptom we're trying to prevent.

Switch to a DETACHED CoroutineScope(SupervisorJob() + Dispatchers.IO).
Its Job is not a child of the enclosing coroutine, so cancelling it
doesn't join — the orphan coroutine keeps running the JNI call in the
background until Go returns or the process exits, but the caller is
unblocked and the runCatching.onFailure path fires the timeout error
state for the UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: add single-flight gate to prevent orphan accumulation

Copilot correctly pointed out on #8689 that the detached-scope approach
can accumulate orphan coroutines if the user retries while a previous
connect() is still stuck in JNI. Each orphan pins a Dispatchers.IO
thread; enough retries against a truly deadlocked Go side could
pressure the IO pool.

Their suggested fix (Dispatchers.IO.limitedParallelism(1)) would
serialize retries behind the orphan, turning the 2nd retry into
another 60s hang. A simple single-flight AtomicBoolean gate with fast
rejection is the cleaner mitigation:

- compareAndSet rejects concurrent attempts with IllegalStateException
  (surfaces via the existing runCatching.onFailure → error state).
- The flag clears in a try/finally inside the async block, which runs
  when the JNI call eventually returns — cancellation alone can't
  break it out, but once Go completes the finally runs and a future
  retry is admitted.
- Process death (reboot, force-stop) resets the flag naturally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Show the fastest location on smart location.

* android: make restartService block until restart completes (#8697)

* android: make restartService block until restart completes

Two bugs in the platformIfce restart path that together let the tunnel
wedge in Restarting forever on Android, triggering the "Error in VPN
operation" on every subsequent Connect attempt
(getlantern/engineering#3297, Freshdesk #173681).

1. restartService() used serviceScope.launch { ... } and returned
   immediately. Radiance's Restart() treats the sync return as "restart
   succeeded" and leaves the tunnel at status=Restarting, expecting the
   platform coroutine to drive it through stopVPN → startVPN and
   transition status via Mobile.* side-effects. If the service is torn
   down before the coroutine completes (onDestroy, process pressure),
   nothing ever transitions the tunnel out of Restarting.

   Switch to runBlocking(Dispatchers.IO) so the return actually
   reflects completion. c.mu is released on the Go side before
   RestartService is invoked, so synchronous Mobile.* callbacks on
   this thread don't deadlock.

2. stopVPNTunnel() skipped Mobile.stopVPN() when Mobile.isVPNConnected()
   returned false. isVPNConnected is status == Connected — but at the
   point stopVPNTunnel is called from restartService, radiance has
   already set status=Restarting, so the guard always skips and the
   tunnel is never actually closed.

   Swap the guard for Mobile.isRadianceConnected() — i.e. only skip
   when the IPC server itself isn't up. Mobile.stopVPN() is a no-op
   when c.tunnel is nil on the Go side, so the original guard was
   redundant even for the Connected == true case.

Evidence from Freshdesk #173681 logs for the broken path:
- 15:17:34.826 Restart → 15:17:34.828 "Tunnel restarted successfully"
  (2ms total — consistent with fire-and-forget, not real teardown)
- No subsequent tunnel.init / Tunnel connection established
- 15:19:10 onDestroy logs "Skipping stopVPN — VPN tunnel was never
  started" (same isVPNConnected() check)
- 15:21:48 next Connect fails within 2ms of the IPC request with
  "tunnel is currently Restarting"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* android: drop isVPNConnected guard in onDestroy too

Same shape as the restart-path fix: if c.tunnel is non-nil on the Go
side but the tunnel status is anything other than Connected (Restarting
after a failed restart, Connecting mid-startup, Error from a prior
failure), isVPNConnected() returns false and the old guard skipped
Mobile.stopVPN(). That left the radiance tunnel state dangling across
service destroy.

Observed in Freshdesk #173681: "onDestroy — radianceConnected=true
vpnConnected=false, Skipping stopVPN — VPN tunnel was never started"
while the tunnel was actually alive at status=Restarting.

Swap the second guard for an unconditional call. Mobile.stopVPN() is a
no-op when c.tunnel is nil, so the guard was always redundant — it just
happened to also hide the non-Connected-but-non-nil case that's
load-bearing during restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* android: verify restart postcondition before returning to Go

launchVPN wraps its body in runCatching { ... }.onFailure { ... } and
returns normally regardless of whether Mobile.startVPN() threw — so a
nil return from startVPN() does not mean the restart succeeded. Without
a postcondition check, restartService would log "completed" and return
to radiance as if everything worked, even though the tunnel is still
stuck in Restarting, which defeats the whole point of making this
function block.

Check Mobile.isVPNConnected() at the end of the runBlocking block and
throw IllegalStateException if false. The exception propagates through
runBlocking → restartService → radiance's platformIfce.RestartService()
as a non-nil error, so Restart() hits the ErrorStatus branch and the
caller sees the failure.

Addresses Copilot review feedback on PR #8697.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Adam Fisk <afisk@mini.local>

* fix(vpn): don't cancel tunnel when restart's start phase fails

The PacketTunnelExtension hosts the IPC server, so cancelTunnelWithError
tears down the daemon along with the tunnel. Inline MobileStartVPN in
restartService so a failed restart leaves the extension (and IPC socket)
alive; radiance's status events surface the failure for retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix copilot issue (#8696) (#8698)

Co-authored-by: atavism <atavism@users.noreply.github.com>

* main: don't block first paint on Updater.init() (#8699)

* main: don't block first paint on Updater.init()

Moving Updater.init() off the critical path to runApp. Investigating a
one-shot black-screen-on-startup report on a local macOS dev build
(9.0.29 build 487): flutter.log stopped at the last pre-runApp log line
with no Dart exception and no crash, while the Go side kept running
normally. The only awaited call between that last log and runApp is
Updater.init().

Inside init(), the actual update check is already deferred 45 s via
Future.delayed + unawaited. But setFeedURL and setScheduledCheckInterval
are awaited — both bridge into Sparkle via the auto_updater Flutter
plugin, and both can stall on first launch: feed URL resolution,
keychain access, or a previous launch's background worker still holding
a lock. Any of those becomes a main-isolate hang that prevents runApp,
which exactly matches the observed symptom.

Fix: drop the await so Updater.init() runs concurrently with the rest
of startup. All errors are already handled inside init() itself, so
unawaited is safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: guard sl<Updater>() lookup against failed service injection

Copilot flagged that if injectServices() throws above (caught at
main.dart:45), Updater is never registered (it's registered at
injection_container.dart:40, after storage init), and sl<Updater>()
throws synchronously. unawaited() doesn't help — the throw happens
before the Future is constructed, so it propagates out of main and
prevents runApp.

Wrap the call in try/catch + sl.isRegistered<Updater>() so any failure
to look up or start Updater.init logs and continues to runApp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(logs): stream diagnostic logs via ipc TailLogs on desktop

Wires the FFI path to radiance's ipc.Client.TailLogs and merges in-app
flutter.log records so the diagnostic logs view shows both sources.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deps: bump radiance to refactor tip (9703bcf) (#8700)

Picks up:
- refactor(vpn): own VPN status on the client so restarts span tunnels
- vpn: instrument tunnel.start phases + VPNClient.Restart (#443)

The VPN-status-ownership refactor moves setStatus calls out of
tunnel and onto VPNClient so a restart transitions Restarting →
Disconnecting → Disconnected → Connecting → Connected cleanly.

The instrumentation PR adds child spans around libbox.Setup,
libbox.NewServiceWithContext, libbox.BoxService.Start, and
newMutableGroupManager so SigNoz can attribute the 10s+ tail
on /service/start observed in Freshdesk #173696.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix server auto issue

* More fix to server selection.

* server selection changes for IOS/Macos

* Use select sever if vpn is active.

* bump radiance - pull in empty tag fix

* lantern-core: dispatch ConnectVPN/StartVPN to SelectServer on live tunnel (#8702)

* lantern-core: dispatch ConnectVPN to SelectServer on live tunnel

When the Flutter UI triggers an auto-select on a live tunnel — most
visibly Jigar's rewrite of onSmartLocation (server_selection.dart), which
routes "switch back to Smart" through startVPN(force: true) → Dart
lantern.startVPN() → ffi.go:startVPN → c.ConnectVPN("") — radiance's
/vpn/connect endpoint rejects the request with ErrTunnelAlreadyConnected
(radiance/vpn/vpn.go:126 in VPNClient.Connect). The error is returned to
the Dart UI as a snackbar, the tunnel stays pinned to the previously
selected manual server, and lantern.log is silent because neither
LocalBackend.ConnectVPN nor VPNClient.Connect slog the ErrTunnelAlready
Connected path.

Observed on 9.0.30 beta (internal tester, Freshdesk #173763, build from
commit 405468954 which includes Jigar's 289507280). After manually
picking Bogotá, clicking "Smart" at the top of the server-selection
screen surfaces the snackbar and the tunnel keeps routing traffic
through the Bogotá samizdat outbound.

Fix: when Status() == Connected, LanternCore.ConnectVPN dispatches the
request to /server/selected (the live-tunnel outbound swap) instead of
/vpn/connect. Empty tag normalizes to vpn.AutoSelectTag — Dart sends ""
for Smart, radiance recognizes only the literal "auto" and otherwise
falls into the manual-outbound branch of SelectServer, stranding Clash
in manual mode with an empty selector. The mapping is centralized in a
small normalizeAutoTag helper used by both ConnectVPN and SelectServer.

This puts the same dispatch logic that lives in ffi.go:connectToServer
onto every caller of LanternCore.ConnectVPN — including ffi.go:startVPN
(which Jigar's rewrite now funnels through) and any future FFI/mobile
entry point.

getlantern/engineering#3291 issue 3. Supersedes earlier work on
fisk/connect-dispatch-select-when-connected (485bf5a00), which was
scoped to this same dispatch but predated the current refactor branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* vpn_tunnel: dispatch StartVPN to SelectServer on live tunnel (mobile path)

Mobile.StartVPN (the gomobile entry point for Android MainActivity and
iOS VPNManager) routes through vpn_tunnel.StartVPN(client), which calls
client.ConnectVPN(ctx, vpn.AutoSelectTag) directly — bypassing
lanterncore.Core. Jigar's onSmartLocation rewrite dispatches "switch
back to Smart" through startVPN(force: true), which on Android/iOS
lands here. Same ErrTunnelAlreadyConnected bug as the FFI path fixed in
the previous commit.

Mirror the VPNStatus dispatch pattern garmr already added to
vpn_tunnel.ConnectToServer in 405468954: when Status() == Connected,
swap outbound via /server/selected; otherwise fall through to the
existing /vpn/connect start.

Together with the LanternCore.ConnectVPN dispatch, this closes the
Smart-from-connected bug on every platform (Windows FFI, Android/iOS
gomobile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ffi: drop now-redundant VPNStatus dispatch in connectToServer

LanternCore.ConnectVPN already routes to /server/selected when the
tunnel is live (added earlier in this PR), so ffi.go:connectToServer's
own VPNStatus check is duplicate work. Collapse to a single c.ConnectVPN
call — both the live-tunnel-swap and fresh-connect paths flow through
the dispatch one layer down.

Behavior unchanged. The "start service failed" error wrapper is kept
for Dart-side snackbar stability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* lantern-core: collapse dispatch to a single implementation in vpn_tunnel

Three functions had independent VPNStatus → SelectServer-vs-ConnectVPN
dispatches after the earlier commits: LanternCore.ConnectVPN,
vpn_tunnel.StartVPN (both added in this PR), and vpn_tunnel.ConnectToServer
(pre-existing from 405468954). Consolidate so vpn_tunnel.ConnectToServer
is the authoritative dispatch and the other two delegate.

- LanternCore.ConnectVPN → vpn_tunnel.ConnectToServer(lc.client, tag)
- vpn_tunnel.StartVPN → ConnectToServer(client, vpn.AutoSelectTag)

LanternCore.SelectServer keeps its own empty-tag normalization since its
scope is the one-shot SelectServer IPC, not the dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* lantern-core: drop client-side empty-tag normalization (radiance fac9089) (#8703)

Patrick's radiance fac9089 ("fix(vpn): treat the empty string as
AutoSelect in SelectServer") is now pinned on this branch via
72a6c6282. Radiance normalizes tag == "" → AutoSelectTag on both
ConnectVPN and SelectServer, so the client-side normalizations we
added earlier (normalizeAutoTag helper in core.go, `if tag == ""` in
vpn_tunnel.ConnectToServer) are redundant — radiance handles the Dart
"" convention uniformly.

Remove:
- LanternCore.normalizeAutoTag helper + its use in SelectServer
- `if tag == "" { tag = vpn.AutoSelectTag }` branch in
  vpn_tunnel.ConnectToServer
- lantern-core/core_test.go (only tested the removed helper)

Behavior unchanged end-to-end: empty tag still means auto-select on
every path (FFI, gomobile, connectToServer, startVPN).

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bump radiance to refactor tip (d5a1872) — pull in LocalBackend.SelectServer empty-tag fix (#8705)

radiance@d5a1872 completes fac9089's empty-string → AutoSelectTag
normalization by extending it to LocalBackend.SelectServer, which
previously only matched the literal "auto" and fell through to the
srvManager lookup for tag == "" — producing "no server found with tag"
(HTTP 500, snackbar) on Smart-from-connected flows after the client-
side normalization was removed in this branch's 6de3c9aa9.

Reported on Lantern 9.0.30 beta via Freshdesk #173773.

go.mod + go.sum bump only; no lantern code changes. Pinned commit:
getlantern/radiance@d5a18726afbc (#444).

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Garmr/refactor mobile logstream (#8701)

* feat(logs): stream diagnostic logs via ipc TailLogs on mobile

Adds a mobile gomobile binding for ipc.Client.TailLogs (TailLogs +
LogSubscription) and switches Android and iOS to consume it, replacing
the per-platform log-file tailers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(logs): stream diagnostic logs via ipc TailLogs on macos

Switches the macOS log stream to MobileTailLogs, matching iOS. Removes
the file-watching LogTailer (no remaining callers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(logs): harden TailLogs against nil, panics, and listener leaks

- Reject nil listener in mobile.TailLogs; recover from panics crossing
  the gomobile bridge so the stream survives unexpected bridge errors.
- Retain the Kotlin LogListener in a field so the Go side's reference
  stays strongly rooted on the JVM.
- On iOS/macOS, cancel any pre-existing subscription before starting a
  new one and clear the stored listener when MobileTailLogs errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(logs): share TailLogs plumbing across mobile and ffi

Adds lantern-core/logs.Subscribe wrapping ipc.Client.TailLogs so the
mobile and desktop integrations go through one helper. Drops the iOS
LogTailer dead code and the unused lantern-core/logging package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update log formatting

* Fix issue with ios

* Fix macos logs issue

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jigar-f <jigar@getlantern.org>

* ffi + lantern-core: drop non-Linux preflight; bound IPC calls with per-operation timeouts (#8707)

* ffi: skip the daemon-reachability preflight on Windows / macOS / mobile

The 300 ms preflight in lantern-core/core.go's CheckDaemonReachable
was originally tuned for the Linux flow (PR #8494 by atavism, commit
bf054f4ea), where the failure path falls back to `systemctl is-active
lanternd.service` for a rich diagnostic error. The 300 ms cap made
sense as "fast probe → systemd-rich-error", with the systemd query
adding the actual user-facing context.

Subsequent refactors (commit bd89bea7e Apr 7, then PR #8578 commit
4d4e06d9d Apr 16) generalized that preflight to all platforms but
the systemd fallback only survived in ffi_linux.go. On Windows /
macOS / mobile, ffi_nonlinux.go ended up running the same 300 ms
probe with no fallback — just an artificial guillotine in front of
ConnectVPN, which has its own "lanternd not reachable" error path
with equivalent precision.

Cold-start IPC on Windows regularly exceeds 300 ms (named-pipe dial
+ winio impersonation token dance + H2c connection preface +
goroutine scheduling on a 96-second-idle daemon), so the first VPN
toggle after launch reliably trips the timeout and shows the user a
"lanternd not reachable" error. Clicking again 10 seconds later
silently succeeds. Reproduced on the same Windows machine across
9.0.29 (Freshdesk #173696) and 9.0.30 (#173932).

Make the preflight a no-op on non-Linux. Linux keeps the original
fast-probe-then-systemdDiag flow unchanged. If we add Windows
(`sc query LanternSvc`) or macOS (`launchctl list`) diagnostics
later, restore the preflight and call them from here.

See getlantern/engineering#3382 for the full archaeology + design
discussion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ffi + lantern-core: bound IPC calls with per-operation timeouts

Companion to dropping the non-Linux daemon-reachability preflight in this
same PR. The preflight (ffi_nonlinux.go's `checkDaemonReachable`) was
introduced in commit bd89bea7e along with the *removal* of per-call
timeouts that used to live on the FFI layer:

    -    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    -    if err := c.Client().DisconnectVPN(ctx); err != nil { ... }
    +    if err := c.DisconnectVPN(); err != nil { ... }

After that change, the only IPC call with any deadline at all was the
300 ms preflight. Every other operation flowed lc.ctx (
context.WithCancel(context.Background())) straight through, meaning a
hung lanternd would freeze the UI indefinitely. Dropping the preflight
without restoring per-call timeouts removes the only line of defense.

Restore them at the LanternCore layer where they belong, with values
sized for the inherent work each operation does (state changes can run
into multi-second territory; status queries should be near-instant):

    ipcConnectTimeout     = 60 * time.Second   // ConnectVPN
    ipcStateChangeTimeout = 30 * time.Second   // SelectServer, DisconnectVPN
    ipcStatusTimeout      = 10 * time.Second   // VPNStatus, IsVPNRunning

These bound the worst case (hung daemon → user sees a clear error within
a minute, no indefinite spinner) without firing during normal slow paths.
The dialer's 10 s connect timeout (radiance/ipc/conn_windows.go) already
covers the lanternd-crashed case; these guard the lanternd-hung case.

vpn_tunnel.{StartVPN, StopVPN, ConnectToServer} take the ctx through
their signatures instead of building their own context.Background()
internally, so callers stay in charge of their own deadlines. mobile/
mobile.go updated to set 60 s / 30 s / 60 s contexts on its three
gomobile entry points.

CheckDaemonReachable's 300 ms timeout is kept untouched — Linux still
calls it from ffi_linux.go for the systemctl is-active fallback that's
the whole point of the fast probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bump radiance

* apps_windows: additional Windows app-discovery sources + heuristic noise filter

Split out from #8706 — held back from the base-bug fix branch
(fisk/fix-empty-apps-base-bug) because this is the heuristic part of
that PR, not the actual cause of Freshdesk #173774 / #173778 / #173826.
The base bug was GetEnabledApps returning nil-as-"null" instead of
empty-as-"[]" (fixed in the other branch); this PR is the broader
investigation that grew alongside the diagnosis but stands on its own
merits.

What's in here:

- **HKLM\Software\Microsoft\Windows\CurrentVersion\App Paths scanner.**
  Apps that register here so they're runnable via Win+R / shellexecute.
  Catches browsers, IDEs, Office, and most third-party apps that don't
  go through Squirrel / Start Menu.

- **HKLM and HKCU \Software\Microsoft\Windows\CurrentVersion\Run.**
  Squirrel-managed apps (Slack, Discord, VS Code Insiders) register for
  auto-start with command lines pointing at Update.exe --processStart.
  Same parser as Start Menu .lnk targets, including --processStart.

- **%LOCALAPPDATA%\<App>\Update.exe pattern.** Belt-and-suspenders for
  Squirrel installs that don't show in the registry yet but exist on
  disk under the well-known layout.

- **isAppPathsNoise filter.** App Paths is heavily polluted by
  Microsoft-bundled tooling (IE relics, Office helpers, vestigial Mail
  + tablet apps) and UWP package plumbing (winget,
  WindowsPackageManagerServer). Drops entries under known system
  paths, .NET helper assemblies, anything under \Microsoft Office\
  except primary product exes, helper-named basenames (substring
  match: "browsersupport", "lastpassexporter", "updater", "helper",
  "diagnostic", "diagcmd"), and basenames suffixed with the generic
  helper words (update, service, agent, sync, broker).

- **ParentKeyName filter removal in isNonUserFacingUninstallEntry.**
  ParentKeyName != "" was introduced in #8641 and was over-aggressive
  — plenty of legitimate end-user apps set it (Squirrel installs,
  winget packages, MSI bundle children). SystemComponent=1 and
  NoDisplay=1 are the documented signals; those stay. Drops the
  now-unused parentKeyName field on uninstallEntryMetadata too.

- **COM-failure logs in Start Menu scanner promoted Debug → Warn**
  (CoInitializeEx, WScript.Shell CreateObject, QueryInterface). These
  paths previously failed silently; now an empty result tells us
  which call broke.

- **Per-filter scan-summary tallies** at the end of each scanner with
  scanned/kept/per-drop-reason counts and a sample of kept apps for
  triage. Sample paths are redacted to filepath.Base to avoid
  including user PII (full Windows paths typically embed the
  username) in scan logs that get bundled into Report Issue tickets.

- **Hot-path constant data hoisted to package scope.**
  isAppPathsNoise's systemPaths / primaryOfficeExes / helperHints /
  suffix lists used to be reallocated on every call (runs hundreds-
  to-thousands of times per scan). Now package-level vars.

- **+152 lines of test coverage** for the new heuristics in
  apps_windows_test.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* lantern-core: fix empty Windows split-tunnel apps list + UI-process logging (#8709)

Two narrow fixes that together resolve Freshdesk #173774 / #173778 /
#173826 (Derek's "Failed to fetch installed apps" empty list on Windows
split tunneling). Split out from #8706 so they can land independently
of the broader app-discovery rework that PR also contained.

1. **GetEnabledApps returns []string{} instead of nil.**
   When no apps are split-tunneled, the previous code returned nil,
   which json.Marshal serialized as "null". Dart's jsonDecode("null")
   returns null; the receiving code does `as List`, which throws and
   the UI shows "Failed to fetch installed apps". Initializing as an
   empty slice serializes to "[]" — Dart parses that as an empty list,
   no exception, no error UI. THIS is the actual root cause of the
   empty-list reports we've been chasing; the apps-discovery scanner
   work was investigating a different (also-real but secondary) issue.

2. **UI-process slog wired up via common.Init.**
   On the refactor branch, the UI process never called common.Init.
   slog wrote to stderr (= nowhere on a GUI host), settings were
   uninitialized, no lantern.log was produced outside the daemon.
   Patrick caught this — it was a one-line miss in the refactor.

   Platform-aware so we don't double-init on platforms where the
   backend embeds in-process:
     - windows/linux: full common.Init (separate UI + daemon procs)
     - darwin/ios:    setupAppLogging into a distinct lantern-app.log
                      so the main-app slog doesn't race the tunnel
                      extension's lantern.log on lumberjack rotation
     - android:       Mobile.SetupRadiance already ran common.Init
                      upstream — fall through

3. **Auto-attach UI-process *.log to ReportIssue (windows/linux only).**
   Without it the daemon's archive glob only sees the daemon's logDir;
   UI-side lantern.log + flutter.log never reach the issue bundle. The
   daemon runs as SYSTEM on Windows; we keep UI logDir at
   %PUBLIC%\Lantern\logs so SYSTEM can read it.

The broader Windows app-discovery work from #8706 (App Paths scan, Run
keys, Squirrel pattern, isAppPathsNoise heuristic filters) is being
held in a separate PR for independent review.

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* android: init Go logging early so lantern-core/radiance debug logs aren't lost (#8711)

On Android the entire app runs in a single process, so once common.Init
runs slog.SetDefault covers everything. But common.Init only runs deep
inside SetupRadiance / StartIPCServer, which LanternVpnService launches
asynchronously from an intent fired by MainActivity.startLanternService.
Any slog call emitted in the gap — including any of the wide MethodHandler
surface that Flutter can reach before the VPN service is up — falls
through to the stdlib default (text → stderr → logcat at INFO), so DEBUG
logs vanish and the format diverges from what we use everywhere else.

Add Mobile.InitLogging as a thin gomobile-exposed wrapper around
common.Init, and call it from MainActivity.configureFlutterEngine before
startLanternService. common.Init is guarded by an atomic.Bool, so the
later call from backend.NewLocalBackend is a no-op.

Mirrors PR #8709 (Windows). Reported on Slack by Jigar.

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* FetchUserData at startup.

* Map the correct type to avoid a crash.

---------

Co-authored-by: garmr <pdixon117@gmail.com>
Co-authored-by: Jigar-f <jigar@getlantern.org>
Co-authored-by: jigar-f <132374182+jigar-f@users.noreply.github.com>
Co-authored-by: atavism <atavism@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ilya Yakelzon <reflog@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>
Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Jay <110402935+jay-418@users.noreply.github.com>
Co-authored-by: atavism <paul@getlantern.org>
Co-authored-by: garmr-ulfr <104022054+garmr-ulfr@users.noreply.github.com>
atavism added a commit that referenced this pull request May 4, 2026
)

* migrate to new ipc.Client api, first-pass

* pullin a couple fixes, update linux vpn status poller

* start ipc server ios/macos

* update radiance, fix linux daemon build

* start ipc server windows service

* fix datacap stream

* decode user response data to json

* gofmt

* update ipc request path check for linux smoke test

* Fixed issue with user apis

* redo linux packaging changes undone by merge

* move RunOffCgoStack from radiance to here, small cleanup

* fetch radiance-owned settings on demand instead of caching locally

* add missing smart-routing, ad-block, oauth calls

* clean up

* fix ref async issue for IPC calls

* gofmt

* fix test, linux package verification

* update radiance, remove server groups

* fix: return added server tags from AddServersByURL

Server tags are determined by URL content, not caller-supplied names.
addServerBasedOnURLs now returns the tags of added servers so callers
can connect using the actual tag. Also sends VPN status updates from
connectToServer on Linux so the UI reflects connection state changes.

* wrap ffi calls in runOnGoStack, update win service

* add explicit not linux build tag

* update radiance

* use RADIANCE_REPO in lanternd src

* flatten server model to match radiance, fix tests

* use loopback ipc client for mobile

* update radiance, log service install error in smoke test

* retrieve selected server from radiance instead of cacheing

* stop lantern before unintall, revert accidental service name change

* remove allow override

* fix name reference and misplaced stop call

* fix several issues

* code review

* fix toggles not registering and fetching plans

* always refetch server list when view opens

* fix crash in server select screen

* fix split tunnel website view not loading websites

* sync vpn status from system on launch

* fix stale onboarding marker persisting reinstall

* Revert "fix stale onboarding marker persisting reinstall"

This reverts commit a21a218eac7df90d678ce5d35d27892bbe893da2.

* fix vpn prompt displaying when quiting

* Macos system extension updates #2 (#8637)

* if system extension is in uninstall state do not block new installtion.

* update macos system extension test

* do not cache dart_tool

* Set the default status as unknown.

* code review updates

* Filter system apps from Windows split tunneling (#8641)

* Add split tunneling e2e test

* Fix split tunneling website smoke assertion

* Fix split tunneling smoke navigation

* code review updates

* code review updates

* code review updates

* Filter Windows system apps in split tunneling list

* code review updates

* code review updates

* Update system apps filter

* code review updates

* fix: upload and notify for nightlies even when some platforms fail (#8649)

The upload-s3 and upload-release-artifacts jobs required ALL platform
builds to succeed or be skipped. When a matrix entry failed (e.g.,
Linux arm64), the entire build-linux job reported as 'failure', which
caused both upload jobs to skip entirely — even though macOS, Android,
iOS, and Linux amd64 all succeeded.

Simplify the condition: run uploads if at least one platform build
succeeded. The upload steps already handle missing artifacts gracefully
(upload_if_exists checks for file existence).

This ensures the Slack notification goes out with download links for
whatever platforms did build successfully.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add arch to releases (#8652)

* feat: add arch to releases

* Update linux/packaging/usr/lib/systemd/system/lanternd.service

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: remove committed lanternd.service file

Agent-Logs-Url: https://github.com/getlantern/lantern/sessions/15085485-3c6a-4e1e-93ea-6e9bf0623d09

Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

* fix issues from 3173

* Refactor and fixed multiple bugs

* cache selected server location locally to avoid UI flash

* Fix tunnel issue in android

* App event issue and auto server location fixes

* added logs

* mobile: return string instead of []byte + update Swift callers (#8663)

* mobile: return string instead of []byte from gomobile-exported funcs

The gomobile wrapper copies Go pointer-containing return values to the C
thread stack using runtime.wbMove. When a GC cycle runs during the copy,
bulkBarrierPreWrite panics because the destination isn't GC-tracked.
Returning string avoids this — gomobile marshals strings via C heap
allocation rather than leaving them as Go slice headers.

See getlantern/engineering#3175 for the full crash analysis (from
Freshdesk #172640 — Derek reporting "Lantern Crash" on macOS 26.3.1).

Go changes:
  AvailableFeatures, UserData, FetchUserData, GetAvailableServers,
  GetSelectedServerJSON, OAuthLoginCallback, AcknowledgeGooglePurchase,
  AcknowledgeApplePurchase, Login, Logout, DeleteAccount

Swift changes (macos + ios): preserve Flutter contract by converting
the string back to Data for methods whose Dart side reads `bytes` via
utf8.decode (getUserData, fetchUserData, oauthLoginCallback, login,
logout, deleteAccount, acknowledgeInAppPurchase). For methods whose Dart
side expects String (featureFlags, getLanternAvailableServers,
getSelectedServerJSON), just pass the gomobile string directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* android: update MethodHandler for string-returning gomobile bindings

The gomobile-exported funcs in lantern-core/mobile/mobile.go now return
string instead of []byte. The generated Android binding will therefore
return String where it used to return ByteArray.

For each affected method, match what the iOS handler does so the Flutter
platform-channel contract stays stable:

  * Methods whose Dart callers expect bytes (Uint8List) — login,
    logout, deleteAccount, userData, fetchUserData, oauthLoginCallback,
    acknowledgeGooglePurchase — convert the String result via
    `.toByteArray(Charsets.UTF_8)` before calling success() (mirrors
    Swift's `.data(using: .utf8)`).

  * Methods whose Dart callers expect a String — availableFeatures,
    getAvailableServers, getSelectedServerJSON — drop the
    `String(byteArray)` constructor and use the return value directly,
    with the same "{}" / "[]" empty-default that iOS uses.

Addresses Copilot review on PR #8663.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* encapsulate ipc.Client behind LanternCore interface

Route all IPC operations through LanternCore methods instead of
exposing Client() to callers. Add GetSelectedServerTag,
GetAutoLocationJSON, CheckDaemonReachable, PatchSettings, and
VPNStatusEvents to the Core interface. Update FFI and mobile layers
to use them, and remove now-unused vpn_tunnel helper functions.

Also includes Flutter-side fixes: device-removal sign-in race
condition, plans fetch retry logic, and private server setup
improvements.

* ios/macos: drop invalid optional-chaining on non-optional String (#8671)

The gomobile-exported functions in lantern-core/mobile/mobile.go were
migrated from ([]byte, error) to (string, error). gomobile renders the
new signatures with a non-optional Swift String return (Data was
optional; String is not), so `json?.data(using: .utf8)` and
`payload?.data(using: .utf8)` now fail to compile:

    error: cannot use optional chaining on non-optional value of type
    'String'

Drop the `?` on all 14 call sites (7 each in ios/ and macos/). The
resulting `json.data(using: .utf8)` returns Data? anyway — an empty
Go string still produces a non-nil empty Data, which preserves the
Flutter contract the comment on these lines describes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* add android-test + android-reproduce for emulator testing and ticket reproduction (#8672)

* add android-test script for quick emulator testing with env overrides

Usage:
  scripts/android/android-test <apk> [ENV_KEY=VALUE ...]

Example:
  scripts/android/android-test lantern.apk RADIANCE_COUNTRY=BG RADIANCE_FEATURE_OVERRIDES=dns_ruleset_host_bypass

Starts an emulator, installs the APK, pushes a .env file with overrides
to the app's data dir (via adb root on Google APIs images, run-as on
debug APKs, or su on rooted devices), restarts the app, and streams
filtered logcat.

Prefers the "lantern-test" AVD if it exists (create with Google APIs
image for root access):
  sdkmanager "system-images;android-35;google_apis;arm64-v8a"
  avdmanager create avd -n lantern-test -k "system-images;android-35;google_apis;arm64-v8a"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* address review: serial targeting, su quoting, trap cleanup, fix comment

- Use -s <serial> throughout so multiple devices don't break adb
- Fix su -c quoting so $(stat ...) expands on-device
- Add trap to clean up temp .env on EXIT/INT/TERM
- Fix header comment (no /sdcard/ fallback)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-test: push .env to .lantern data dir (not app root)

The Go env package reads .env from the data directory (via
env.LoadFromDir called from common.Init), not from the app's root
data dir. Push to /data/data/$PKG/.lantern/.env so radiance finds it.

Companion: getlantern/radiance#421 (env.LoadFromDir)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-test: auto-install system image and create AVD if none exists

If no AVDs are found, the script now automatically:
1. Detects host arch (arm64 vs x86_64)
2. Installs the Google APIs system image via sdkmanager
3. Creates a "lantern-test" AVD via avdmanager

This means running android-test on a fresh machine with just the
Android SDK installed works out of the box — no manual AVD setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* address review: array for ADB_CMD, timeouts, remove unused PID

- Use bash array for ADB_CMD so paths with spaces work correctly
- Add configurable timeouts for emulator appear (120s) and boot (300s)
- Remove unused EMULATOR_PID — emulator intentionally left running
  between invocations so subsequent runs don't pay boot cost

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* add android-reproduce: reproduce Freshdesk tickets on emulator

Usage:
  android-reproduce /tmp/ticket-172722              # auto-downloads APK
  android-reproduce /tmp/ticket-172722 lantern.apk  # uses provided APK

After running /analyze-ticket, this script:
1. Extracts country + version from the ticket's config/logs
2. Downloads the matching APK from GitHub releases (gh CLI)
3. Pushes the user's exact config.json, servers.json, split-tunnel.json
   to the emulator so it gets the same proxies, DNS rules, rule sets
4. Sets RADIANCE_COUNTRY to match the user's region
5. Installs, restarts, and streams filtered logcat

This gives near-exact reproduction of Android-specific issues by
replicating the user's proxy assignments, country routing, and
sing-box config on a local emulator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-reproduce: match user's Android API level from ticket logs

Extracts sdkInt, osVersion, and model from flutter.log's "Device info"
line. Creates an AVD with the matching API level (e.g. "lantern-api36"
for a user on Android 16/SDK 36). Falls back to API 35 if the target
image isn't available.

Example for ticket #172722 (Android 16, SM-A556B):
  Creates lantern-api35 (API 36 clamped to 35), installs matching APK,
  pushes user's exact config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* android-reproduce: dynamically find closest available API image

Instead of hardcoding a fallback to API 35, step down from the user's
sdkInt until we find an installable Google APIs image. Each API level
gets its own AVD (lantern-api29, lantern-api34, etc.) that persists
across runs, building up a catalog over time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* address review: install-before-push, fix eval injection, f-string, file search

- Install APK + launch once before pushing configs (so data dir exists)
- Replace eval with mapfile for device info extraction (no shell injection)
- Fix f-string syntax error in locations display
- Search both ticket-dir and config-dir for servers.json/split-tunnel.json
- Remove unused SCRIPT_DIR
- Update android-test header to document auto-AVD-creation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix private server navigation issue

* deps: bump sing-box-minimal to v1.12.21-lantern on refactor (#8679)

Companion to #8678. The refactor branch still pins v1.12.19-lantern,
which is missing the non-fatal-rule-set-fetch fix (sing-box-minimal
9c79c311, shipped in v1.12.21-lantern). Without it, Android builds
from this branch hit the same bootstrap deadlock.

* Add IPC starter in android

1

* macos, ios and android cleanup

* lantern-core: wire config events through IPC (#8673)

* lantern-core: subscribe to config events over IPC (/config/events)

The refactor branch removed listenConfigEvents when it was discovered
that the in-process events.SubscribeContext no longer worked — the
extension's radiance process is where config.NewConfigEvent is emitted,
and the host's subscription never fires across processes.

Now that the companion radiance PR adds a /config/events SSE endpoint,
restore the listener using lc.client.ConfigEvents with the same
reconnect-with-backoff pattern listenAutoSelectedEvents uses. Each
frame fires notifyFlutter(EventTypeConfig, "") so Flutter's
app_event_notifier "config" case resumes driving
availableServersProvider.forceFetchAvailableServers() and
homeProvider.fetchUserDataIfNeeded() on every config change.

Also bumps the radiance pin to the commit that adds the endpoint.

Addresses the config-events half of getlantern/engineering#3182.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* lantern-core: update StartBackgroundListeners comment to include config

Reflects that listenConfigEvents also starts automatically from
initialize, addressing Copilot review on PR #8673.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* add android-reproduce: reproduce Freshdesk tickets on emulator

Usage:
  android-reproduce /tmp/ticket-172722              # auto-downloads APK
  android-reproduce /tmp/ticket-172722 lantern.apk  # uses provided APK

After running /analyze-ticket, this script:
1. Extracts country + version from the ticket's config/logs
2. Downloads the matching APK from GitHub releases (gh CLI)
3. Pushes the user's exact config.json, servers.json, split-tunnel.json
   to the emulator so it gets the same proxies, DNS rules, rule sets
4. Sets RADIANCE_COUNTRY to match the user's region
5. Installs, restarts, and streams filtered logcat

This gives near-exact reproduction of Android-specific issues by
replicating the user's proxy assignments, country routing, and
sing-box config on a local emulator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Adam Fisk <afisk@mini.local>

* windows ffi cleanup

* Update bindings

* point to radiance refactor branch

* feat(dev-mode): hidden 5-tap unlock on support view + expanded dev screen

Show Build number alongside Lantern version on the support view. Tapping
the Build row 5× within 3s toggles developer mode (gated to nightly/debug
builds for enabling; disabling works anywhere). The developer entry in
settings now hides unless dev mode is enabled.

Developer screen adds radiance env-var overrides (country, version,
feature overrides), a log-level dropdown, a config-fetch toggle, and
buttons to send a config request, run URL tests, show live settings/env,
and disable dev mode. Pins qpack to v0.5.1 via replace directive to match
radiance's own pin so sing-box-minimal's quic-go HTTP/3 continues to
build.

Wires radiance ipc.Client.PatchSettings / PatchEnvVars / RunOfflineURLTests
/ UpdateConfig through lantern-core and exports them via FFI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bump radiance - limit config fetch to 1 at a time

* feat(dev-mode): show spinner on in-flight action tiles

Tapping Send config request / Run URL tests / Show settings & env vars
now disables the tile and shows a spinner until the IPC call returns, so
users don't assume the button is broken during the latency before the
snackbar appears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Developer mode refactor

* Sync garmr/radiance-daemon-refactor with origin/main (#8684)

* deps: update radiance to fix outbound removal breaking config refresh (#8639)

Picks up radiance PR #405 which fixes removeOutbounds failing when
extra outbounds (non-smart Pro locations) aren't in the URL test group.
This was causing every config refresh IPC to return 500, preventing
SetURLOverrides and CheckOutbounds from running — resulting in ~50%
of bandit probe callbacks never firing.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Smart location country fix (#8638)

* Do not reset a smart location.

* code review updates

* Fix website split-tunneling reliability and CI validation (#8640)

* Add split tunneling e2e test

* Fix split tunneling website smoke assertion

* Fix split tunneling smoke navigation

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* Macos system extension updates #2 (#8637)

* if system extension is in uninstall state do not block new installtion.

* update macos system extension test

* do not cache dart_tool

* Set the default status as unknown.

* code review updates

* Filter system apps from Windows split tunneling (#8641)

* Add split tunneling e2e test

* Fix split tunneling website smoke assertion

* Fix split tunneling smoke navigation

* code review updates

* code review updates

* code review updates

* Filter Windows system apps in split tunneling list

* code review updates

* code review updates

* Update system apps filter

* code review updates

* deps: update radiance + lantern-box to fix ~20% callback failure (#8642)

Picks up:
- radiance PR #406 → lantern-box PR #231: clear URL test history
  when SetURLOverrides is called so outbounds are re-tested with
  new callback URLs
- radiance PR #405: best-effort URL test group removal (already in
  previous update, carried forward)
- lantern-box v0.0.61: includes CA cert install + history fix

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update radiance + lantern-box for callback-all-outbounds (#8644)

- radiance: removes URL test filtering, all outbounds tested (PR #407)
- lantern-box v0.0.62: 6-worker URL test pool + client delay reporting (PR #232)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Hide system apps without dropping user apps on Windows (#8643)

* code review updates

* code review updates

* code review updates

* chore: update radiance for async IPC outbound handlers (#8645)

Picks up getlantern/radiance#410: IPC outbound update/add/remove
handlers return 202 immediately and process asynchronously, fixing
the EOF errors on every config refresh.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update radiance for split tunnel persistence fix (#8646)

Picks up getlantern/radiance#411: fixes split tunnel filters silently
not persisting due to dangling slice pointers in initRuleMap.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: upload and notify for nightlies even when some platforms fail (#8649)

The upload-s3 and upload-release-artifacts jobs required ALL platform
builds to succeed or be skipped. When a matrix entry failed (e.g.,
Linux arm64), the entire build-linux job reported as 'failure', which
caused both upload jobs to skip entirely — even though macOS, Android,
iOS, and Linux amd64 all succeeded.

Simplify the condition: run uploads if at least one platform build
succeeded. The upload steps already handle missing artifacts gracefully
(upload_if_exists checks for file existence).

This ensures the Slack notification goes out with download links for
whatever platforms did build successfully.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Stabilize nightly smoke checks and platform release publishing (#8651)

* Stabilize nightly smoke checks and platform release publishing

* code review updates

* code review updates

* chore: bump radiance to latest main (lantern-box v0.0.65) (#8654)

Picks up:
- Reflex active-probe resistance: silence-timeout + masquerade
  fallback (getlantern/lantern-box#237 via radiance#413)
- TLS 1.3 minimum enforcement for Reflex
  (getlantern/lantern-box#236)
- radiance split-tunnel filter persistence fix (#411)

No Flutter / client-side behavior changes required — the Reflex
hardening is server-side.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add arch to releases (#8652)

* feat: add arch to releases

* Update linux/packaging/usr/lib/systemd/system/lanternd.service

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: remove committed lanternd.service file

Agent-Logs-Url: https://github.com/getlantern/lantern/sessions/15085485-3c6a-4e1e-93ea-6e9bf0623d09

Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>

* ran go mod tidy

* Improve Windows app discovery for shortcut wrappers (#8653)

* code review updates

* Improve Windows app discovery for shortcut wrappers

* code review updates

* code review updates

* code review updates

* The radiance-to-device limit is flow fix. (#8659)

* only use permalinks (#8658)

Co-authored-by: atavism <paul@getlantern.org>

* Add auth E2E tests and wire Linux/Windows CI (#8607)

* auth flow test updates

* auth flow test updates

* auth flow test updates

* code review updates

* code review updates

* code review updates

* code review updates

* deps: update sing-box-minimal to v1.12.21-lantern (#8660)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Show vpn conflict dialog on smart location (#8661)

* Show vpn conflict dialog on smart location

* code review updates

* chore: bump radiance and lantern-box to latest (#8664)

- radiance: f1c425231e41 → 4241e6c5a9c6 (main HEAD)
- lantern-box: v0.0.65 → v0.0.67

Ran go mod tidy.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Windows installer cleanup, improve app discovery and icon loading (#8666)

* code review updates

* Add comment

* code review updates

* remove sentry (#8665)

* Save last server location (#8655)

* save server location

* update radiance.

* Forbid AutoConnect if connect fails.

* update radiance

* code review updates

* update radiance

* code review updates (#8675)

* deps: restore sing-box-minimal v1.12.21-lantern (#8678)

PR #8655 ("Save last server location") accidentally downgraded
sing-box-minimal from v1.12.21-lantern back to v1.12.19-lantern in
go.mod during review churn. v1.12.21-lantern contains commit 9c79c311
("fix: make initial remote rule-set fetch non-fatal"), which turns the
Android bootstrap deadlock ("no available network interface" during
initial rule-set fetch) from a fatal libbox startup error into a
WARN + retry-after-start. Without it, nightly builds from main fail
to connect on any smart-routing country (Macao, Bulgaria, etc.).

Confirmed by comparing Freshdesk #172722 (broken, rule_set_remote.go:235,
v1.12.19-lantern) with #172795 (working, rule_set_remote.go:113,
v1.12.21-lantern). Same user, same device, same 9.0.25 version, same
smart-routing-bg-common-direct fetch failure — only the sing-box-minimal
version differs. The v9.0.25-beta-android tag was cut before #8655
merged, which is why Alexander's beta works while the nightly doesn't.

`go mod tidy` also dropped stale go.sum entries for superseded radiance
and lantern-box pseudo-versions and removed the unused getsentry/sentry-go
indirect (left behind after #8665).

* Makefile: fix empty common.Version on Windows CI (missing app version 400) (#8677)

* Makefile: use env-provided APP_VERSION so Windows CI populates version ldflag

common.Version in radiance was being linked as an empty string on Windows
CI builds. The `-X .../common.Version=$(APP_VERSION_PUBSPEC)` ldflag
depended on `$(shell grep ... | sed ...)` or a PowerShell fallback, and
the Windows path was producing an empty value. With common.Version empty,
backend.NewRequestWithHeaders sets X-Lantern-App-Version to "", and
lantern-cloud's /v1/config-new handler rejects the request with
400 "missing app version" — no config is returned, so the client falls
back to the embedded server list with no bandit tracks. Observed on
Freshdesk #172794 (Windows 9.0.26 nightly, radiance 400s on every retry).

Use the APP_VERSION already exported to GITHUB_ENV by build-windows.yml's
"Read app version from pubspec.yaml" step, and compute APP_VERSION_PUBSPEC
with Make built-ins ($(firstword $(subst +, ,...))) so no shell tools are
required. Drops the Windows_NT branch; local dev on Mac/Linux still uses
the grep/sed fallback (APP_VERSION ?=).

* Makefile: restore Windows local-dev fallback for APP_VERSION

The previous commit removed the Windows_NT branch under the assumption
that APP_VERSION would always come from the environment. That's true on
CI (build-windows.yml exports it to GITHUB_ENV), but local Windows
developers running `make windows-release` directly don't set the env
var, and the grep/sed fallback runs under cmd.exe where Unix-style
quoting fails silently.

Add back the Windows PowerShell branch, but only as the fallback when
APP_VERSION isn't in the environment (`?=` on both branches). CI keeps
working via the env override; local Mac/Linux uses grep/sed; local
Windows uses PowerShell Select-String. The `+`-splitting stays in
Make built-ins so it works no matter which branch produced APP_VERSION.

* Makefile: fail the build when APP_VERSION_PUBSPEC ends up empty

Adds a parse-time guard so an unresolvable version fails loudly rather
than producing a binary with empty common.Version — which is what caused
this whole bug in the first place. Addresses Copilot review feedback on
PR #8677.

$ APP_VERSION="" make
Makefile:36: *** APP_VERSION_PUBSPEC is empty; export APP_VERSION ...

* Roll in #8676: PowerShell quoting + Windows service startup log

Incorporates the non-overlapping pieces of @atavism's PR #8676 so we
can close it in favor of this PR:

- Swap the Windows APP_VERSION fallback's PowerShell invocation to
  outer-single / inner-double quoting. The previous outer-double /
  inner-single form gets mangled when Make expands $$ and cmd.exe
  passes the resulting string to powershell, even in the local-dev
  fallback path.
- Same fix for GO_VERSION's PowerShell shell-out further down in the
  Makefile (separate variable, same root cause).
- Log the Windows service startup (name, version, mode) so it's
  visible when triaging issues. Matches the log line from #8676.

* Fix data cap issue (#8668)

* Report an Issue screen fixes (#8670)

* updates to report issue screen

* updates to report issue screen

* rename report issue

* rename report issue

* code review updates

* ffi: add missing base64 import for app icon encoding

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

* code review updates

---------

Co-authored-by: Myles Horton <afisk@getlantern.org>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: jigar-f <132374182+jigar-f@users.noreply.github.com>
Co-authored-by: Ilya Yakelzon <reflog@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>
Co-authored-by: Jay <110402935+jay-418@users.noreply.github.com>

* code review updates

* bump radiance

* bump go to v1.26.2

Go v1.26.2 includes a patch to CGo that addresses some of
the bulkBarrierPreWrite panics.

* bump radiance - fix event streams on mobile

* fix sign up issue to point new radiance.

* split tunneling: treat FFI "ok" response as success, not error (#8691)

* split tunneling: treat FFI "ok" response as success, not error

_runSplitTunnelCall was checking `result != nullptr` and treating any
non-null return as an error message. But the Go FFI
(lantern-core/ffi/ffi.go) returns C.CString("ok") on success for both
addSplitTunnelItem and removeSplitTunnelItem — a non-null C string.

As a result, every successful add/remove was being reported to the UI as
a failure with message "ok". Symptoms:

- Adding a website in split tunneling showed an unstyled default
  snackbar reading "OK" (the default Material SnackBar rendering
  failure.localizedErrorMessage).
- The website appeared to not be saved — but it actually was; the
  provider's `reloaded` flag was never set, so the on-screen list never
  re-fetched from the backend.
- Re-clicking "Add" with the same domain created a duplicate entry on
  disk (visible as repeated items in split-tunnel.json) because the
  provider's local "already-added" check worked against a stale copy
  that had never been refreshed.

Fix: mirror the checkAPIError convention — treat literal "ok" as
success, parse JSON {"error": "..."} bodies for the error message, and
fall back to the raw string otherwise.

Reported in getlantern/engineering#3291 against Windows 9.0.29 build 481
(Freshdesk #173656).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* split tunneling: reuse _ffiOkResults for success-string check

Rather than hardcoding 'ok', use the existing _ffiOkResults set
({'ok', 'true'}) defined at the top of this file so the split-tunnel
path stays in sync with the other FFI success checks (e.g.
_setupRadiance at line 201).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* split tunneling: use design-system error snackbar on add (#8692)

The local showSnackbar helper in website_domain_input was using
Material's default ScaffoldMessenger.showSnackBar(SnackBar(content:
Text(message))) — producing an unstyled grey/dark snackbar that the rest
of the app doesn't use. Every call site in this file is an error path
(empty input, invalid domain, already-added, backend failure), so route
them through context.showSnackBarError which applies the app's rounded,
floating, red-background error style.

Follow-up to #8691. Addresses the "unstyled snackbar" symptom in
getlantern/engineering#3291 issue 3 for any remaining error surface
after the FFI "ok" fix.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(vpn): use SelectServer when switching servers on a live tunnel

 connectToServer previously always called ConnectVPN, which radiance
 rejects with ErrTunnelAlreadyConnected when the tunnel is up. Check
 VPNStatus first and route to SelectServer when Connected, falling
 back to ConnectVPN otherwise.

* android: detach connect() scope so withTimeout actually unblocks the UI (#8689)

* review: detach connect() scope so timeout actually unblocks the UI

Copilot flagged on #8689 that the existing coroutineScope { ... } still
hangs in exactly the scenario this change is meant to protect against.
Structured coroutineScope cancels its children on exception but then
waits for them to complete — so when withTimeout fires, we cancel the
deferred (which the JNI call ignores, since it has no suspension
points) and then block on it finishing anyway. Net effect: the UI is
still frozen, which is the symptom we're trying to prevent.

Switch to a DETACHED CoroutineScope(SupervisorJob() + Dispatchers.IO).
Its Job is not a child of the enclosing coroutine, so cancelling it
doesn't join — the orphan coroutine keeps running the JNI call in the
background until Go returns or the process exits, but the caller is
unblocked and the runCatching.onFailure path fires the timeout error
state for the UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: add single-flight gate to prevent orphan accumulation

Copilot correctly pointed out on #8689 that the detached-scope approach
can accumulate orphan coroutines if the user retries while a previous
connect() is still stuck in JNI. Each orphan pins a Dispatchers.IO
thread; enough retries against a truly deadlocked Go side could
pressure the IO pool.

Their suggested fix (Dispatchers.IO.limitedParallelism(1)) would
serialize retries behind the orphan, turning the 2nd retry into
another 60s hang. A simple single-flight AtomicBoolean gate with fast
rejection is the cleaner mitigation:

- compareAndSet rejects concurrent attempts with IllegalStateException
  (surfaces via the existing runCatching.onFailure → error state).
- The flag clears in a try/finally inside the async block, which runs
  when the JNI call eventually returns — cancellation alone can't
  break it out, but once Go completes the finally runs and a future
  retry is admitted.
- Process death (reboot, force-stop) resets the flag naturally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Show the fastest location on smart location.

* android: make restartService block until restart completes (#8697)

* android: make restartService block until restart completes

Two bugs in the platformIfce restart path that together let the tunnel
wedge in Restarting forever on Android, triggering the "Error in VPN
operation" on every subsequent Connect attempt
(getlantern/engineering#3297, Freshdesk #173681).

1. restartService() used serviceScope.launch { ... } and returned
   immediately. Radiance's Restart() treats the sync return as "restart
   succeeded" and leaves the tunnel at status=Restarting, expecting the
   platform coroutine to drive it through stopVPN → startVPN and
   transition status via Mobile.* side-effects. If the service is torn
   down before the coroutine completes (onDestroy, process pressure),
   nothing ever transitions the tunnel out of Restarting.

   Switch to runBlocking(Dispatchers.IO) so the return actually
   reflects completion. c.mu is released on the Go side before
   RestartService is invoked, so synchronous Mobile.* callbacks on
   this thread don't deadlock.

2. stopVPNTunnel() skipped Mobile.stopVPN() when Mobile.isVPNConnected()
   returned false. isVPNConnected is status == Connected — but at the
   point stopVPNTunnel is called from restartService, radiance has
   already set status=Restarting, so the guard always skips and the
   tunnel is never actually closed.

   Swap the guard for Mobile.isRadianceConnected() — i.e. only skip
   when the IPC server itself isn't up. Mobile.stopVPN() is a no-op
   when c.tunnel is nil on the Go side, so the original guard was
   redundant even for the Connected == true case.

Evidence from Freshdesk #173681 logs for the broken path:
- 15:17:34.826 Restart → 15:17:34.828 "Tunnel restarted successfully"
  (2ms total — consistent with fire-and-forget, not real teardown)
- No subsequent tunnel.init / Tunnel connection established
- 15:19:10 onDestroy logs "Skipping stopVPN — VPN tunnel was never
  started" (same isVPNConnected() check)
- 15:21:48 next Connect fails within 2ms of the IPC request with
  "tunnel is currently Restarting"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* android: drop isVPNConnected guard in onDestroy too

Same shape as the restart-path fix: if c.tunnel is non-nil on the Go
side but the tunnel status is anything other than Connected (Restarting
after a failed restart, Connecting mid-startup, Error from a prior
failure), isVPNConnected() returns false and the old guard skipped
Mobile.stopVPN(). That left the radiance tunnel state dangling across
service destroy.

Observed in Freshdesk #173681: "onDestroy — radianceConnected=true
vpnConnected=false, Skipping stopVPN — VPN tunnel was never started"
while the tunnel was actually alive at status=Restarting.

Swap the second guard for an unconditional call. Mobile.stopVPN() is a
no-op when c.tunnel is nil, so the guard was always redundant — it just
happened to also hide the non-Connected-but-non-nil case that's
load-bearing during restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* android: verify restart postcondition before returning to Go

launchVPN wraps its body in runCatching { ... }.onFailure { ... } and
returns normally regardless of whether Mobile.startVPN() threw — so a
nil return from startVPN() does not mean the restart succeeded. Without
a postcondition check, restartService would log "completed" and return
to radiance as if everything worked, even though the tunnel is still
stuck in Restarting, which defeats the whole point of making this
function block.

Check Mobile.isVPNConnected() at the end of the runBlocking block and
throw IllegalStateException if false. The exception propagates through
runBlocking → restartService → radiance's platformIfce.RestartService()
as a non-nil error, so Restart() hits the ErrorStatus branch and the
caller sees the failure.

Addresses Copilot review feedback on PR #8697.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Adam Fisk <afisk@mini.local>

* fix(vpn): don't cancel tunnel when restart's start phase fails

The PacketTunnelExtension hosts the IPC server, so cancelTunnelWithError
tears down the daemon along with the tunnel. Inline MobileStartVPN in
restartService so a failed restart leaves the extension (and IPC socket)
alive; radiance's status events surface the failure for retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix copilot issue (#8696) (#8698)

Co-authored-by: atavism <atavism@users.noreply.github.com>

* main: don't block first paint on Updater.init() (#8699)

* main: don't block first paint on Updater.init()

Moving Updater.init() off the critical path to runApp. Investigating a
one-shot black-screen-on-startup report on a local macOS dev build
(9.0.29 build 487): flutter.log stopped at the last pre-runApp log line
with no Dart exception and no crash, while the Go side kept running
normally. The only awaited call between that last log and runApp is
Updater.init().

Inside init(), the actual update check is already deferred 45 s via
Future.delayed + unawaited. But setFeedURL and setScheduledCheckInterval
are awaited — both bridge into Sparkle via the auto_updater Flutter
plugin, and both can stall on first launch: feed URL resolution,
keychain access, or a previous launch's background worker still holding
a lock. Any of those becomes a main-isolate hang that prevents runApp,
which exactly matches the observed symptom.

Fix: drop the await so Updater.init() runs concurrently with the rest
of startup. All errors are already handled inside init() itself, so
unawaited is safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review: guard sl<Updater>() lookup against failed service injection

Copilot flagged that if injectServices() throws above (caught at
main.dart:45), Updater is never registered (it's registered at
injection_container.dart:40, after storage init), and sl<Updater>()
throws synchronously. unawaited() doesn't help — the throw happens
before the Future is constructed, so it propagates out of main and
prevents runApp.

Wrap the call in try/catch + sl.isRegistered<Updater>() so any failure
to look up or start Updater.init logs and continues to runApp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(logs): stream diagnostic logs via ipc TailLogs on desktop

Wires the FFI path to radiance's ipc.Client.TailLogs and merges in-app
flutter.log records so the diagnostic logs view shows both sources.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* deps: bump radiance to refactor tip (9703bcf) (#8700)

Picks up:
- refactor(vpn): own VPN status on the client so restarts span tunnels
- vpn: instrument tunnel.start phases + VPNClient.Restart (#443)

The VPN-status-ownership refactor moves setStatus calls out of
tunnel and onto VPNClient so a restart transitions Restarting →
Disconnecting → Disconnected → Connecting → Connected cleanly.

The instrumentation PR adds child spans around libbox.Setup,
libbox.NewServiceWithContext, libbox.BoxService.Start, and
newMutableGroupManager so SigNoz can attribute the 10s+ tail
on /service/start observed in Freshdesk #173696.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix server auto issue

* More fix to server selection.

* server selection changes for IOS/Macos

* Use select sever if vpn is active.

* bump radiance - pull in empty tag fix

* lantern-core: dispatch ConnectVPN/StartVPN to SelectServer on live tunnel (#8702)

* lantern-core: dispatch ConnectVPN to SelectServer on live tunnel

When the Flutter UI triggers an auto-select on a live tunnel — most
visibly Jigar's rewrite of onSmartLocation (server_selection.dart), which
routes "switch back to Smart" through startVPN(force: true) → Dart
lantern.startVPN() → ffi.go:startVPN → c.ConnectVPN("") — radiance's
/vpn/connect endpoint rejects the request with ErrTunnelAlreadyConnected
(radiance/vpn/vpn.go:126 in VPNClient.Connect). The error is returned to
the Dart UI as a snackbar, the tunnel stays pinned to the previously
selected manual server, and lantern.log is silent because neither
LocalBackend.ConnectVPN nor VPNClient.Connect slog the ErrTunnelAlready
Connected path.

Observed on 9.0.30 beta (internal tester, Freshdesk #173763, build from
commit 405468954 which includes Jigar's 289507280). After manually
picking Bogotá, clicking "Smart" at the top of the server-selection
screen surfaces the snackbar and the tunnel keeps routing traffic
through the Bogotá samizdat outbound.

Fix: when Status() == Connected, LanternCore.ConnectVPN dispatches the
request to /server/selected (the live-tunnel outbound swap) instead of
/vpn/connect. Empty tag normalizes to vpn.AutoSelectTag — Dart sends ""
for Smart, radiance recognizes only the literal "auto" and otherwise
falls into the manual-outbound branch of SelectServer, stranding Clash
in manual mode with an empty selector. The mapping is centralized in a
small normalizeAutoTag helper used by both ConnectVPN and SelectServer.

This puts the same dispatch logic that lives in ffi.go:connectToServer
onto every caller of LanternCore.ConnectVPN — including ffi.go:startVPN
(which Jigar's rewrite now funnels through) and any future FFI/mobile
entry point.

getlantern/engineering#3291 issue 3. Supersedes earlier work on
fisk/connect-dispatch-select-when-connected (485bf5a00), which was
scoped to this same dispatch but predated the current refactor branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* vpn_tunnel: dispatch StartVPN to SelectServer on live tunnel (mobile path)

Mobile.StartVPN (the gomobile entry point for Android MainActivity and
iOS VPNManager) routes through vpn_tunnel.StartVPN(client), which calls
client.ConnectVPN(ctx, vpn.AutoSelectTag) directly — bypassing
lanterncore.Core. Jigar's onSmartLocation rewrite dispatches "switch
back to Smart" through startVPN(force: true), which on Android/iOS
lands here. Same ErrTunnelAlreadyConnected bug as the FFI path fixed in
the previous commit.

Mirror the VPNStatus dispatch pattern garmr already added to
vpn_tunnel.ConnectToServer in 405468954: when Status() == Connected,
swap outbound via /server/selected; otherwise fall through to the
existing /vpn/connect start.

Together with the LanternCore.ConnectVPN dispatch, this closes the
Smart-from-connected bug on every platform (Windows FFI, Android/iOS
gomobile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ffi: drop now-redundant VPNStatus dispatch in connectToServer

LanternCore.ConnectVPN already routes to /server/selected when the
tunnel is live (added earlier in this PR), so ffi.go:connectToServer's
own VPNStatus check is duplicate work. Collapse to a single c.ConnectVPN
call — both the live-tunnel-swap and fresh-connect paths flow through
the dispatch one layer down.

Behavior unchanged. The "start service failed" error wrapper is kept
for Dart-side snackbar stability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* lantern-core: collapse dispatch to a single implementation in vpn_tunnel

Three functions had independent VPNStatus → SelectServer-vs-ConnectVPN
dispatches after the earlier commits: LanternCore.ConnectVPN,
vpn_tunnel.StartVPN (both added in this PR), and vpn_tunnel.ConnectToServer
(pre-existing from 405468954). Consolidate so vpn_tunnel.ConnectToServer
is the authoritative dispatch and the other two delegate.

- LanternCore.ConnectVPN → vpn_tunnel.ConnectToServer(lc.client, tag)
- vpn_tunnel.StartVPN → ConnectToServer(client, vpn.AutoSelectTag)

LanternCore.SelectServer keeps its own empty-tag normalization since its
scope is the one-shot SelectServer IPC, not the dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* lantern-core: drop client-side empty-tag normalization (radiance fac9089) (#8703)

Patrick's radiance fac9089 ("fix(vpn): treat the empty string as
AutoSelect in SelectServer") is now pinned on this branch via
72a6c6282. Radiance normalizes tag == "" → AutoSelectTag on both
ConnectVPN and SelectServer, so the client-side normalizations we
added earlier (normalizeAutoTag helper in core.go, `if tag == ""` in
vpn_tunnel.ConnectToServer) are redundant — radiance handles the Dart
"" convention uniformly.

Remove:
- LanternCore.normalizeAutoTag helper + its use in SelectServer
- `if tag == "" { tag = vpn.AutoSelectTag }` branch in
  vpn_tunnel.ConnectToServer
- lantern-core/core_test.go (only tested the removed helper)

Behavior unchanged end-to-end: empty tag still means auto-select on
every path (FFI, gomobile, connectToServer, startVPN).

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bump radiance to refactor tip (d5a1872) — pull in LocalBackend.SelectServer empty-tag fix (#8705)

radiance@d5a1872 completes fac9089's empty-string → AutoSelectTag
normalization by extending it to LocalBackend.SelectServer, which
previously only matched the literal "auto" and fell through to the
srvManager lookup for tag == "" — producing "no server found with tag"
(HTTP 500, snackbar) on Smart-from-connected flows after the client-
side normalization was removed in this branch's 6de3c9aa9.

Reported on Lantern 9.0.30 beta via Freshdesk #173773.

go.mod + go.sum bump only; no lantern code changes. Pinned commit:
getlantern/radiance@d5a18726afbc (#444).

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Garmr/refactor mobile logstream (#8701)

* feat(logs): stream diagnostic logs via ipc TailLogs on mobile

Adds a mobile gomobile binding for ipc.Client.TailLogs (TailLogs +
LogSubscription) and switches Android and iOS to consume it, replacing
the per-platform log-file tailers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(logs): stream diagnostic logs via ipc TailLogs on macos

Switches the macOS log stream to MobileTailLogs, matching iOS. Removes
the file-watching LogTailer (no remaining callers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(logs): harden TailLogs against nil, panics, and listener leaks

- Reject nil listener in mobile.TailLogs; recover from panics crossing
  the gomobile bridge so the stream survives unexpected bridge errors.
- Retain the Kotlin LogListener in a field so the Go side's reference
  stays strongly rooted on the JVM.
- On iOS/macOS, cancel any pre-existing subscription before starting a
  new one and clear the stored listener when MobileTailLogs errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(logs): share TailLogs plumbing across mobile and ffi

Adds lantern-core/logs.Subscribe wrapping ipc.Client.TailLogs so the
mobile and desktop integrations go through one helper. Drops the iOS
LogTailer dead code and the unused lantern-core/logging package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update log formatting

* Fix issue with ios

* Fix macos logs issue

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jigar-f <jigar@getlantern.org>

* ffi + lantern-core: drop non-Linux preflight; bound IPC calls with per-operation timeouts (#8707)

* ffi: skip the daemon-reachability preflight on Windows / macOS / mobile

The 300 ms preflight in lantern-core/core.go's CheckDaemonReachable
was originally tuned for the Linux flow (PR #8494 by atavism, commit
bf054f4ea), where the failure path falls back to `systemctl is-active
lanternd.service` for a rich diagnostic error. The 300 ms cap made
sense as "fast probe → systemd-rich-error", with the systemd query
adding the actual user-facing context.

Subsequent refactors (commit bd89bea7e Apr 7, then PR #8578 commit
4d4e06d9d Apr 16) generalized that preflight to all platforms but
the systemd fallback only survived in ffi_linux.go. On Windows /
macOS / mobile, ffi_nonlinux.go ended up running the same 300 ms
probe with no fallback — just an artificial guillotine in front of
ConnectVPN, which has its own "lanternd not reachable" error path
with equivalent precision.

Cold-start IPC on Windows regularly exceeds 300 ms (named-pipe dial
+ winio impersonation token dance + H2c connection preface +
goroutine scheduling on a 96-second-idle daemon), so the first VPN
toggle after launch reliably trips the timeout and shows the user a
"lanternd not reachable" error. Clicking again 10 seconds later
silently succeeds. Reproduced on the same Windows machine across
9.0.29 (Freshdesk #173696) and 9.0.30 (#173932).

Make the preflight a no-op on non-Linux. Linux keeps the original
fast-probe-then-systemdDiag flow unchanged. If we add Windows
(`sc query LanternSvc`) or macOS (`launchctl list`) diagnostics
later, restore the preflight and call them from here.

See getlantern/engineering#3382 for the full archaeology + design
discussion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ffi + lantern-core: bound IPC calls with per-operation timeouts

Companion to dropping the non-Linux daemon-reachability preflight in this
same PR. The preflight (ffi_nonlinux.go's `checkDaemonReachable`) was
introduced in commit bd89bea7e along with the *removal* of per-call
timeouts that used to live on the FFI layer:

    -    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    -    if err := c.Client().DisconnectVPN(ctx); err != nil { ... }
    +    if err := c.DisconnectVPN(); err != nil { ... }

After that change, the only IPC call with any deadline at all was the
300 ms preflight. Every other operation flowed lc.ctx (
context.WithCancel(context.Background())) straight through, meaning a
hung lanternd would freeze the UI indefinitely. Dropping the preflight
without restoring per-call timeouts removes the only line of defense.

Restore them at the LanternCore layer where they belong, with values
sized for the inherent work each operation does (state changes can run
into multi-second territory; status queries should be near-instant):

    ipcConnectTimeout     = 60 * time.Second   // ConnectVPN
    ipcStateChangeTimeout = 30 * time.Second   // SelectServer, DisconnectVPN
    ipcStatusTimeout      = 10 * time.Second   // VPNStatus, IsVPNRunning

These bound the worst case (hung daemon → user sees a clear error within
a minute, no indefinite spinner) without firing during normal slow paths.
The dialer's 10 s connect timeout (radiance/ipc/conn_windows.go) already
covers the lanternd-crashed case; these guard the lanternd-hung case.

vpn_tunnel.{StartVPN, StopVPN, ConnectToServer} take the ctx through
their signatures instead of building their own context.Background()
internally, so callers stay in charge of their own deadlines. mobile/
mobile.go updated to set 60 s / 30 s / 60 s contexts on its three
gomobile entry points.

CheckDaemonReachable's 300 ms timeout is kept untouched — Linux still
calls it from ffi_linux.go for the systemctl is-active fallback that's
the whole point of the fast probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bump radiance

* lantern-core: fix empty Windows split-tunnel apps list + UI-process logging (#8709)

Two narrow fixes that together resolve Freshdesk #173774 / #173778 /
#173826 (Derek's "Failed to fetch installed apps" empty list on Windows
split tunneling). Split out from #8706 so they can land independently
of the broader app-discovery rework that PR also contained.

1. **GetEnabledApps returns []string{} instead of nil.**
   When no apps are split-tunneled, the previous code returned nil,
   which json.Marshal serialized as "null". Dart's jsonDecode("null")
   returns null; the receiving code does `as List`, which throws and
   the UI shows "Failed to fetch installed apps". Initializing as an
   empty slice serializes to "[]" — Dart parses that as an empty list,
   no exception, no error UI. THIS is the actual root cause of the
   empty-list reports we've been chasing; the apps-discovery scanner
   work was investigating a different (also-real but secondary) issue.

2. **UI-process slog wired up via common.Init.**
   On the refactor branch, the UI process never called common.Init.
   slog wrote to stderr (= nowhere on a GUI host), settings were
   uninitialized, no lantern.log was produced outside the daemon.
   Patrick caught this — it was a one-line miss in the refactor.

   Platform-aware so we don't double-init on platforms where the
   backend embeds in-process:
     - windows/linux: full common.Init (separate UI + daemon procs)
     - darwin/ios:    setupAppLogging into a distinct lantern-app.log
                      so the main-app slog doesn't race the tunnel
                      extension's lantern.log on lumberjack rotation
     - android:       Mobile.SetupRadiance already ran common.Init
                      upstream — fall through

3. **Auto-attach UI-process *.log to ReportIssue (windows/linux only).**
   Without it the daemon's archive glob only sees the daemon's logDir;
   UI-side lantern.log + flutter.log never reach the issue bundle. The
   daemon runs as SYSTEM on Windows; we keep UI logDir at
   %PUBLIC%\Lantern\logs so SYSTEM can read it.

The broader Windows app-discovery work from #8706 (App Paths scan, Run
keys, Squirrel pattern, isAppPathsNoise heuristic filters) is being
held in a separate PR for independent review.

Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* qa: plumb RADIANCE_OUTBOUND_SOCKS_ADDRESS into the Android client

Adds a debug-only path that lets a developer route every outbound
network call radiance makes from the Android client through an
upstream SOCKS5 (typically the local pinger bridge), so the bandit
treats the client as a real Russia-residential user end-to-end.

Pairs with:
  * radiance:    https://github.com/getlantern/radiance/pull/445
  * lantern-cloud: https://github.com/getlantern/lantern-cloud/pull/2649

  * `lantern-core/mobile`: new gomobile-exported `SetQAEnvOverrides(socks, tz)`
    that does `os.Setenv` for `RADIANCE_OUTBOUND_SOCKS_ADDRESS` and `TZ`.
    Must be called before `SetupRadiance`/`StartIPCServer` to take effect.
  * `android/.../LanternApp.kt`: override `onCreate` and call the new setter
    with values from Android system properties:
      `debug.lantern.outbound_socks` -> `RADIANCE_OUTBOUND_SOCKS_ADDRESS`
      `debug.lantern.tz`             -> `TZ`
    Set with `adb shell setprop debug.lantern.outbound_socks 10.0.2.2:1080`.
    No-op when the props are unset, so production builds aren't affected
    unless someone deliberately sets them on the device.
  * `go.mod`: bump radiance to the qa/outbound-socks-egress branch tip
    (will swap back to a pinned tag once that PR lands).

Verified end-to-end in an `lantern_test` AVD with packetstream + Russia
upstream:
  - LanternApp logs `QA env overrides applied: outbound_socks=10.0.2.2:1080`
  - Radiance's `/v1/config-new` response: `country=RU ip=85.172.81.50`
  - Bandit serves Russia-tier outbounds (samizdat / reflex in DE/SE/SG/etc.)
  - All sing-box outbound dials wrapped in `_dev_outbound_socks` detour
  - Browsing in the emulator's Chrome egresses from a Lantern entry server
    (e.g. Stockholm/Singapore — bandit-assigned, not the Mac's home IP)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* code review updates

---------

Co-authored-by: garmr <pdixon117@gmail.com>
Co-authored-by: Jigar-f <jigar@getlantern.org>
Co-authored-by: jigar-f <132374182+jigar-f@users.noreply.github.com>
Co-authored-by: atavism <atavism@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ilya Yakelzon <reflog@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: reflog <109876+reflog@users.noreply.github.com>
Co-authored-by: Adam Fisk <afisk@mini.local>
Co-authored-by: Jay <110402935+jay-418@users.noreply.github.com>
Co-authored-by: atavism <paul@getlantern.org>
Co-authored-by: garmr-ulfr <104022054+garmr-ulfr@users.noreply.github.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.

4 participants