Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions guides/dns_on_ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions guides/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down
63 changes: 63 additions & 0 deletions lib/mob/app.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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: [...]`
Expand Down Expand Up @@ -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 """
Expand Down
19 changes: 19 additions & 0 deletions test/mob/app_test.exs
Original file line number Diff line number Diff line change
@@ -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