Skip to content

fix(android): deliver default-interface updates synchronously#8890

Merged
garmr-ulfr merged 4 commits into
garmr/fix/android-default-interface-indexfrom
garmr/fix/android-default-interface-ordering
Jul 1, 2026
Merged

fix(android): deliver default-interface updates synchronously#8890
garmr-ulfr merged 4 commits into
garmr/fix/android-default-interface-indexfrom
garmr/fix/android-default-interface-ordering

Conversation

@garmr-ulfr

@garmr-ulfr garmr-ulfr commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Default-network changes are delivered to libbox through
DefaultNetworkMonitor. Each updateDefaultInterface call was dispatched to
GlobalScope.launch(Dispatchers.IO), so successive network changes could be
applied out of order and pin a stale interface index on the Go side
(last-writer-wins) — making outbounds bind to the wrong interface and fail.

This PR delivers interface updates serialized on a single background thread,
preserving network-change order while keeping the resolution work — which
sleeps between retries and calls into Go over JNI — off the main-looper
callback thread.

Changes

  • Serialize updateDefaultInterface onto a single-thread executor: updates
    apply in network-change order (a shared pool like Dispatchers.IO could
    reorder them) and off the UI thread (the callback is delivered on the main
    looper).
  • Remove the now-unused Bugs.fixAndroidStack helper (Bugs.kt); the
    x/mobile: bind with go1.22.5,1.22.6 leads to: fatal error: runtime: stack split at bad time golang/go#68760 stack workaround it duplicated is handled on the Go side via
    libbox SetupOptions.FixAndroidStack (enabled by radiance).
  • Extract interface resolution into resolveDefaultInterface /
    getInterfaceIndex / sleepBeforeRetry.
  • Recover the interface index via Os.if_nametoindex when
    NetworkInterface.getByName throws, not only when it returns null.
  • Snapshot the listener on the caller thread and mark it @Volatile so the
    worker thread reliably observes setListener writes.
  • Log when the default interface is resolved, cleared, or fails to resolve.

Stacking

Stacked on #8889 — base is garmr/fix/android-default-interface-index, so this
PR's diff is only the ordering changes. Retarget to main once #8889 merges.

Summary by CodeRabbit

  • Bug Fixes
    • Improved default network detection to be more reliable and consistent.
    • Added retry handling when resolving network interface details, reducing missed updates during network changes.
    • Better handles cases where no active network is available, preventing failed updates.

checkDefaultInterfaceUpdate dispatched each updateDefaultInterface call
to GlobalScope.launch(Dispatchers.IO), so successive default-network
changes could be applied out of order, pinning a stale interface index
(Go side is last-writer-wins).

The golang/go#68760 stack workaround that the launch was duplicating is
already handled on the Go side (libbox SetupOptions.FixAndroidStack,
enabled by radiance), so delivering synchronously preserves the
network-change ordering that DefaultNetworkListener already guarantees.

Remove the now-unused Bugs.fixAndroidStack helper.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bfc70cab-9f38-4002-8a7e-8ad6987b4690

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

DefaultNetworkMonitor's default-interface update logic is refactored to run on a dedicated single-thread executor instead of coroutine-based dispatch, replacing conditional Bugs.fixAndroidStack branching with sentinel constants and extracted retry-based interface resolution helpers. The now-unused Bugs utility object and its fixAndroidStack property are deleted.

Changes

Default network interface update refactor

Layer / File(s) Summary
Sentinel constants and executor-based dispatch
android/app/src/main/kotlin/org/getlantern/lantern/service/DefaultNetworkMonitor.kt
Adds NO_INTERFACE_NAME/NO_INTERFACE_INDEX sentinels and a ResolvedInterface holder, removes coroutine/Bugs imports, and dispatches update work through a single-thread updateExecutor, notifying the listener with sentinel values when the network is null.
Retry-based interface resolution helpers
android/app/src/main/kotlin/org/getlantern/lantern/service/DefaultNetworkMonitor.kt
Extracts resolveDefaultInterface, getInterfaceIndex, and sleepBeforeRetry, implementing a bounded retry loop that re-reads interface name/index with an Os.if_nametoindex fallback, returning null and logging on exhausted retries.
Removal of Bugs workaround
android/app/src/main/kotlin/org/getlantern/lantern/utils/Bugs.kt
Deletes the Bugs object and its fixAndroidStack SDK-version-based flag, which is no longer referenced.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant checkDefaultInterfaceUpdate
  participant updateExecutor
  participant resolveDefaultInterface
  participant getInterfaceIndex
  participant listener

  checkDefaultInterfaceUpdate->>updateExecutor: execute update task
  updateExecutor->>resolveDefaultInterface: resolve interface for network
  loop retry up to INTERFACE_RESOLUTION_RETRY_COUNT
    resolveDefaultInterface->>resolveDefaultInterface: read interfaceName from LinkProperties
    resolveDefaultInterface->>getInterfaceIndex: get index via NetworkInterface or Os.if_nametoindex
    getInterfaceIndex-->>resolveDefaultInterface: return index or null
    alt resolution failed
      resolveDefaultInterface->>resolveDefaultInterface: sleepBeforeRetry
    end
  end
  resolveDefaultInterface-->>updateExecutor: return ResolvedInterface or null
  updateExecutor->>listener: notifyDefaultInterface with resolved name/index
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: ordered default-interface delivery on Android.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch garmr/fix/android-default-interface-ordering

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses out-of-order delivery of Android default-interface updates to the Go/libbox side by removing the asynchronous GlobalScope.launch(Dispatchers.IO) dispatch and delivering updateDefaultInterface calls synchronously from DefaultNetworkMonitor.checkDefaultInterfaceUpdate. It also removes the now-unused Bugs.fixAndroidStack workaround helper.

Changes:

  • Deliver updateDefaultInterface synchronously to preserve the ordering guaranteed by DefaultNetworkListener.
  • Remove coroutine-based dispatch (GlobalScope.launch(Dispatchers.IO)) and the unused Bugs.fixAndroidStack flag.
  • Replace magic “no interface” literals with constants (NO_INTERFACE_NAME, NO_INTERFACE_INDEX).

Reviewed changes

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

File Description
android/app/src/main/kotlin/org/getlantern/lantern/utils/Bugs.kt Removes the unused Android stack workaround flag helper.
android/app/src/main/kotlin/org/getlantern/lantern/service/DefaultNetworkMonitor.kt Makes default-interface updates synchronous and removes coroutine dispatch to preserve update ordering.

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

Default-network callbacks are delivered on the main looper, so resolving
the interface (which sleeps between retries and calls into Go over JNI)
and delivering the update ran on the UI thread. Move that work onto a
single-thread executor: it stays off the UI thread while still applying
updates in network-change order, which a shared pool would not guarantee.

Extract the resolution into resolveDefaultInterface/getInterfaceIndex/
sleepBeforeRetry helpers, snapshot the listener on the caller thread and
mark it @volatile so the worker reliably observes setListener writes, and
recover via if_nametoindex when getByName throws (not just when it returns
null). Add logging when the interface is resolved, cleared, or fails to
resolve.
@garmr-ulfr

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@android/app/src/main/kotlin/org/getlantern/lantern/service/DefaultNetworkMonitor.kt`:
- Around line 82-103: The queued work in
DefaultNetworkMonitor.checkDefaultInterfaceUpdate can still call
updateDefaultInterface on a stale InterfaceUpdateListener after
closeDefaultInterfaceMonitor clears the field. Re-check the listener inside the
updateExecutor block before calling notifyDefaultInterface, or otherwise cancel
pending tasks, so only a live listener is notified. Use the listener variable in
checkDefaultInterfaceUpdate and the notifyDefaultInterface path to locate the
fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 72249e7f-8c57-4f49-86fe-a7d9e7b81aea

📥 Commits

Reviewing files that changed from the base of the PR and between 04b8e11 and bb57f16.

📒 Files selected for processing (1)
  • android/app/src/main/kotlin/org/getlantern/lantern/service/DefaultNetworkMonitor.kt

notifyDefaultInterface re-checks that the snapshotted listener is still
the active one before calling into libbox, so an update queued or being
resolved across a setListener teardown can no longer invoke a closed
handler.

Also tidy the resolution logging: split the getByName attempt from the
if_nametoindex fallback so the warning names the actual failing call and
the fallback runs once, and drop the fixed attempt-count from the
failure log since resolution can bail early on interruption.
@garmr-ulfr garmr-ulfr marked this pull request as ready for review July 1, 2026 20:29
@atavism

atavism commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Just one comment

Os.if_nametoindex was added in API 26, but the app supports API 24+.
Skip the name-based fallback on API 24/25 with an SDK_INT check instead
of letting the call fail, and drop the runCatching that was masking it.
@garmr-ulfr

Copy link
Copy Markdown
Contributor Author

Thanks for the review @atavism!

@garmr-ulfr garmr-ulfr merged commit 940b673 into garmr/fix/android-default-interface-index Jul 1, 2026
2 checks passed
@garmr-ulfr garmr-ulfr deleted the garmr/fix/android-default-interface-ordering branch July 1, 2026 21:56
atavism added a commit that referenced this pull request Jul 2, 2026
…8889)

* fix(android): recover default-interface index via Os.if_nametoindex

NetworkInterface.getByName relies on the interface-enumeration syscall,
which is restricted on some Android versions/ROMs (returns null or throws
there). When it failed, checkDefaultInterfaceUpdate exhausted its retries
without ever calling updateDefaultInterface, so sing-box never received a
default interface and every outbound failed with "no available network
interface".

Fall back to Os.if_nametoindex when getByName cannot resolve the index,
mirroring the already-hardened getInterfaces sibling, and retry only when
the index is still unresolvable. Also early-return on a null interface
name instead of burning the retry budget on getByName(null).

* move `Os.if_nametoindex` inside the `try` and yield 0 on throw

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

* fix(android): resolve default interface inside the retry loop

getLinkProperties (and its interfaceName) can briefly return null right
after a network change. Resolving it once before the loop meant a transient
null abandoned the update entirely, leaving the default interface unset for
that network change. Resolve inside the retry loop instead, matching
upstream, so a transient null is retried rather than dropped.

* fix(android): deliver default-interface updates synchronously (#8890)

* fix(android): deliver default-interface updates synchronously

checkDefaultInterfaceUpdate dispatched each updateDefaultInterface call
to GlobalScope.launch(Dispatchers.IO), so successive default-network
changes could be applied out of order, pinning a stale interface index
(Go side is last-writer-wins).

The golang/go#68760 stack workaround that the launch was duplicating is
already handled on the Go side (libbox SetupOptions.FixAndroidStack,
enabled by radiance), so delivering synchronously preserves the
network-change ordering that DefaultNetworkListener already guarantees.

Remove the now-unused Bugs.fixAndroidStack helper.

* refactor(android): serialize interface updates on a background thread

Default-network callbacks are delivered on the main looper, so resolving
the interface (which sleeps between retries and calls into Go over JNI)
and delivering the update ran on the UI thread. Move that work onto a
single-thread executor: it stays off the UI thread while still applying
updates in network-change order, which a shared pool would not guarantee.

Extract the resolution into resolveDefaultInterface/getInterfaceIndex/
sleepBeforeRetry helpers, snapshot the listener on the caller thread and
mark it @volatile so the worker reliably observes setListener writes, and
recover via if_nametoindex when getByName throws (not just when it returns
null). Add logging when the interface is resolved, cleared, or fails to
resolve.

* fix(android): skip interface updates for a torn-down listener

notifyDefaultInterface re-checks that the snapshotted listener is still
the active one before calling into libbox, so an update queued or being
resolved across a setListener teardown can no longer invoke a closed
handler.

Also tidy the resolution logging: split the getByName attempt from the
if_nametoindex fallback so the warning names the actual failing call and
the fallback runs once, and drop the fixed attempt-count from the
failure log since resolution can bail early on interruption.

* fix(android): guard if_nametoindex fallback behind API 26

Os.if_nametoindex was added in API 26, but the app supports API 24+.
Skip the name-based fallback on API 24/25 with an SDK_INT check instead
of letting the call fail, and drop the runCatching that was masking it.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: atavism <atavism@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.

3 participants