diff --git a/CHANGELOG.md b/CHANGELOG.md index ca845233..c89d3105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Full module documentation: [hexdocs.pm/mob](https://hexdocs.pm/mob). --- +## [Unreleased] + +### Fixed +- iOS: `Mob.App.start/0` now switches `:inet_db` to file-only lookup and seeds `localhost` before any user code runs — BEAM's default `:native` lookup tries to `execve` the `inet_gethost` port program, which the iOS sandbox refuses, crashing the first `Node.connect` / `:erpc.call` / `gen_tcp.connect/3` with `:badarg`. Apps no longer need to set the lookup chain themselves; `Mob.DNS.configure_pure_beam/1` still composes on top for outbound DNS. See `guides/dns_on_ios.md`. + ## [0.6.7] ### Added diff --git a/guides/dns_on_ios.md b/guides/dns_on_ios.md index f5cd58af..7a189e02 100644 --- a/guides/dns_on_ios.md +++ b/guides/dns_on_ios.md @@ -34,6 +34,25 @@ first in the chain. --- +## What mob already does for you + +Before any of this matters, `Mob.App.start/0` runs +`Mob.App.configure_ios_inet_db/0` on iOS (both simulator and device), +which switches the lookup chain to `[:file]` and seeds `localhost`. +That's the minimum needed so `Node.connect`, `:erpc.call`, and +`gen_tcp.connect/3` with a binary host don't crash the calling +process on the first lookup. Apps don't have to do anything for +distribution and local-loopback TCP to work. + +`Mob.DNS.configure_pure_beam/1` is the next step *on top of that* — +it upgrades the chain to `[:file, :dns]` and seeds fallback +nameservers so outbound HTTP to public hosts works. The framework +default doesn't enable `:dns` because apps that don't talk to the +public internet shouldn't pay for fallback-nameserver state they +won't use. + +--- + ## Why this exists BEAM resolves hostnames the same way it always has: it spawns an diff --git a/guides/troubleshooting.md b/guides/troubleshooting.md index ba5c6551..c5740942 100644 --- a/guides/troubleshooting.md +++ b/guides/troubleshooting.md @@ -342,6 +342,11 @@ app sandbox forbids. Every hostname lookup through `:inet` fails immediately. Android works because its OTP helpers ship as `lib*.so` in `jniLibs/`, which SELinux allows to exec; iOS has no equivalent escape hatch. +`Mob.App.start/0` already switches the lookup chain to `[:file]` on iOS so +distribution and local-loopback TCP work without setup. That doesn't help +public-internet hostnames though — you still need to opt into one of the +DNS strategies below to talk to Req / Finch / Mint endpoints. + **Fix:** Call `Mob.DNS.resolve/1` once per backend before your first request, typically in your app's `on_start/0`: diff --git a/lib/mob/app.ex b/lib/mob/app.ex index f3ec68ab..c5bf51ab 100644 --- a/lib/mob/app.ex +++ b/lib/mob/app.ex @@ -66,6 +66,17 @@ defmodule Mob.App do Do not override — implement `on_start/0` instead. """ def start do + # iOS-only: BEAM's default :native hostname lookup spawns the + # `inet_gethost` port program via execve, which the iOS app + # sandbox refuses. Any subsequent code path that resolves a + # hostname — Node.connect, :erpc.call, gen_tcp.connect with a + # binary host, Logger forwarding to a named node — crashes the + # calling process with badarg before this is fixed. Switch to + # file-only lookup and seed `localhost` so distribution and + # local TCP work out of the box. Apps that need real outbound + # DNS layer Mob.DNS.configure_pure_beam/1 on top in on_start/0. + Mob.App.configure_ios_inet_db() + Mob.NativeLogger.install() # Compile theme from options passed to `use Mob.App, theme: [...]` @@ -123,6 +134,58 @@ defmodule Mob.App do end end + @doc """ + Apply the iOS-only `:inet_db` workaround so distribution, RPC, and + TCP-by-hostname don't crash on the first lookup. + + iOS sandboxes any app that isn't Apple's own and refuses `execve` of + binaries the app didn't get a special pass for. BEAM's default + `:native` hostname-resolution path spawns the `inet_gethost` port + program — exactly the kind of `execve` iOS rejects — so the very + first `:inet.getaddr/2` call (transitively reached by `Node.connect`, + `:erpc.call`, `gen_tcp.connect/3` with a binary host, etc.) crashes + the calling process with `:badarg`. The simulator hits the same + failure for a related but distinct reason: `inet_gethost` doesn't + live at the path BEAM expects under the mob iOS sim OTP layout. + Either way, the fix is the same. + + Switching the lookup chain to `[:file]` keeps everything in BEAM's + in-process name table — no port program, no fork, no `execve`. We + also seed `localhost` so apps using `@localhost` node names (or any + `gen_tcp` call that resolves `"localhost"`) work without further + setup. + + Called automatically by the macro-generated `start/0` before + anything else, so app `on_start/0` code never has to think about it. + Apps that need outbound DNS (Req / Finch / Mint to arbitrary hosts) + can layer `Mob.DNS.configure_pure_beam/1` on top — it upgrades the + chain to `[:file, :dns]` and seeds fallback nameservers, while the + file-table entries we add here keep winning. + + Other platforms (`:android`, `:host`) are unaffected — BEAM's native + resolver works there. Safe to call on host BEAM where the NIF isn't + loaded; rescues the `UndefinedFunctionError` / `ErlangError` and + returns `:ok`. + """ + @spec configure_ios_inet_db() :: :ok + def configure_ios_inet_db do + case safe_platform() do + :ios -> + :inet_db.set_lookup([:file]) + :inet_db.add_host({127, 0, 0, 1}, [~c"localhost"]) + :ok + + _ -> + :ok + end + end + + defp safe_platform do + :mob_nif.platform() + rescue + _ in [UndefinedFunctionError, ErlangError] -> :host + end + # ── Navigation helpers ───────────────────────────────────────────────────── @doc """ diff --git a/test/mob/app_test.exs b/test/mob/app_test.exs new file mode 100644 index 00000000..3d28ac90 --- /dev/null +++ b/test/mob/app_test.exs @@ -0,0 +1,19 @@ +defmodule Mob.AppTest do + use ExUnit.Case, async: false + + describe "configure_ios_inet_db/0" do + test "is a no-op on host BEAM where the NIF isn't loaded" do + lookup_before = :inet_db.res_option(:lookup) + + assert :ok = Mob.App.configure_ios_inet_db() + + assert :inet_db.res_option(:lookup) == lookup_before + end + + test "idempotent — repeated calls don't crash or stack state" do + assert :ok = Mob.App.configure_ios_inet_db() + assert :ok = Mob.App.configure_ios_inet_db() + assert :ok = Mob.App.configure_ios_inet_db() + end + end +end