Android: enable LAN mDNS discovery via NsdManager (netlink RTM_GETLINK blocked on API >= 30) #1

Open
opened 2026-06-22 17:19:16 +01:00 by allison · 0 comments
Owner

Summary

LAN peer discovery via mDNS is disabled on Android. The Android-native replacement is NsdManager (Network Service Discovery), wired into iroh through a custom AddressLookup impl. Connectivity is unaffected — relays + DNS/Pkarr discovery cover peer reachability — but same-LAN Android peers cannot discover each other's direct address without a relay hop until this lands.

Root cause

iroh's mDNS backend (iroh-mdns-address-lookupswarm-discovery) enumerates network interfaces via the netlink RTM_GETLINK dump. Android denies RTM_GETLINK for any app targeting API ≥ 30 — it requires the nlmsg_readpriv SELinux capability that the untrusted_app domain does not have (Android b/155595000). Our app targets API 36, so the mDNS service fails to initialize, and because the failure propagates out of Endpoint::bind(), the core never starts and the app hangs on the loading screen.

Observed on a Pixel 9a (debug build), logcat:

ERROR iroh::socket: Endpoint dropped without calling `Endpoint::close`. Aborting ungracefully.
ERROR hopchat_lib: startup: core failed to start endpoint bind: Failed to create an address lookup service
avc: denied { nlmsg_readpriv } ... netlink_route_socket ... bug=b/155595000
avc: denied { search } for name="net" dev="sysfs" ... sysfs_net

This is not a missing manifest permission. CHANGE_WIFI_MULTICAST_STATE (which we already hold, for the multicast lock) governs receiving inbound multicast — not interface enumeration. There is no app-grantable permission for the netlink path; Google removed it deliberately in Android 11. Same wall is hit by Syncthing, libtorrent, Go's net, Qt, and IPFS.

Current workaround (shipped)

crates/hopchat-core/src/lib.rs — the MdnsAddressLookup is #[cfg(not(target_os = "android"))]-gated out of the endpoint builder. Android binds with presets::N0 (relays + DNS/Pkarr) only. Desktop/iOS behaviour is unchanged.

Proposed fix: NsdManager-backed AddressLookup

iroh takes a pluggable iroh::address_lookup::AddressLookup trait (we already pass MemoryLookup and, off-Android, MdnsAddressLookup). Implement a NsdAddressLookup for Android:

  1. Kotlin NsdManager wrapperregisterService to advertise this endpoint, discoverServices + resolveService to find peers. Encode the iroh EndpointId in the service name / TXT record (mirror swarm-discovery's encoding). Watch out for: serialized resolve pre-API 34 (queue resolves), name collisions, teardown on pause, and known NsdManager flakiness below Android 13.
  2. JNI bridge — start/stop from Rust (pattern already exists for the Wi-Fi multicast lock in network/multicast.rs); the new direction is Kotlin→Rust callbacks delivering discovered (EndpointId, SocketAddr) entries (global JNI ref or channel).
  3. AddressLookup impl — feed resolved peer addresses into iroh, keyed by EndpointId, modelled on MemoryLookup.

First thing to nail down: the exact method surface of AddressLookup (push vs pull, async shape), since it dictates how the Kotlin callbacks plug in.

Estimated complexity: medium — a few focused days. Bounded by the existing trait seam + JNI precedent; no upstream iroh solution exists (the iroh community discusses mDNS generally but not an Android/NsdManager path), so this would be net-new.

References

## Summary LAN peer discovery via mDNS is **disabled on Android**. The Android-native replacement is `NsdManager` (Network Service Discovery), wired into iroh through a custom `AddressLookup` impl. Connectivity is unaffected — relays + DNS/Pkarr discovery cover peer reachability — but same-LAN Android peers cannot discover each other's direct address without a relay hop until this lands. ## Root cause iroh's mDNS backend (`iroh-mdns-address-lookup` → `swarm-discovery`) enumerates network interfaces via the netlink `RTM_GETLINK` dump. Android **denies `RTM_GETLINK` for any app targeting API ≥ 30** — it requires the `nlmsg_readpriv` SELinux capability that the `untrusted_app` domain does not have (Android `b/155595000`). Our app targets API 36, so the mDNS service fails to initialize, and because the failure propagates out of `Endpoint::bind()`, the **core never starts and the app hangs on the loading screen**. Observed on a Pixel 9a (debug build), logcat: ``` ERROR iroh::socket: Endpoint dropped without calling `Endpoint::close`. Aborting ungracefully. ERROR hopchat_lib: startup: core failed to start endpoint bind: Failed to create an address lookup service avc: denied { nlmsg_readpriv } ... netlink_route_socket ... bug=b/155595000 avc: denied { search } for name="net" dev="sysfs" ... sysfs_net ``` This is **not** a missing manifest permission. `CHANGE_WIFI_MULTICAST_STATE` (which we already hold, for the multicast lock) governs *receiving* inbound multicast — not interface enumeration. There is no app-grantable permission for the netlink path; Google removed it deliberately in Android 11. Same wall is hit by Syncthing, libtorrent, Go's `net`, Qt, and IPFS. ## Current workaround (shipped) `crates/hopchat-core/src/lib.rs` — the `MdnsAddressLookup` is `#[cfg(not(target_os = "android"))]`-gated out of the endpoint builder. Android binds with `presets::N0` (relays + DNS/Pkarr) only. Desktop/iOS behaviour is unchanged. ## Proposed fix: NsdManager-backed `AddressLookup` iroh takes a pluggable `iroh::address_lookup::AddressLookup` trait (we already pass `MemoryLookup` and, off-Android, `MdnsAddressLookup`). Implement a `NsdAddressLookup` for Android: 1. **Kotlin `NsdManager` wrapper** — `registerService` to advertise this endpoint, `discoverServices` + `resolveService` to find peers. Encode the iroh `EndpointId` in the service name / TXT record (mirror swarm-discovery's encoding). Watch out for: serialized resolve pre-API 34 (queue resolves), name collisions, teardown on pause, and known NsdManager flakiness below Android 13. 2. **JNI bridge** — start/stop from Rust (pattern already exists for the Wi-Fi multicast lock in `network/multicast.rs`); the new direction is Kotlin→Rust callbacks delivering discovered `(EndpointId, SocketAddr)` entries (global JNI ref or channel). 3. **`AddressLookup` impl** — feed resolved peer addresses into iroh, keyed by `EndpointId`, modelled on `MemoryLookup`. First thing to nail down: the exact method surface of `AddressLookup` (push vs pull, async shape), since it dictates how the Kotlin callbacks plug in. **Estimated complexity:** medium — a few focused days. Bounded by the existing trait seam + JNI precedent; no upstream iroh solution exists (the iroh community discusses mDNS generally but not an Android/NsdManager path), so this would be net-new. ## References - iroh #990 — Implement peer discovery in LAN: https://github.com/n0-computer/iroh/issues/990 - iroh-mdns-address-lookup: https://lib.rs/crates/iroh-mdns-address-lookup - Syncthing #7763 — Android 11 netlinkrib permission denied: https://github.com/syncthing/syncthing/issues/7763 - libtorrent #6251 — NETLINK_ROUTE not available on Android 11+: https://github.com/arvidn/libtorrent/issues/6251 - NsdManager local-discovery example — dmix #862: https://github.com/abarisain/dmix/pull/862/files
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
HopNet/HopChat#1
No description provided.