Mob.DNS (mob v0.6.0)

Copy Markdown View Source

Hostname → IP resolution that works around BEAM's broken DNS path on iOS.

Why this exists

BEAM resolves hostnames by spawning an external helper called inet_gethost (a port program). On macOS, Linux, Windows that works fine. On iOS it doesn't — the iOS app sandbox forbids execve of any binary the app didn't get a special pass for, and there's no equivalent of Android's lib*.so escape hatch. Result: :inet.getaddr/2 (and therefore Req, Finch, Mint, ReqLLM, and basically every Elixir HTTP library) fails on iOS the moment a request hits a hostname rather than a literal IP.

This module side-steps the problem by calling Darwin's getaddrinfo directly via a NIF, then seeding :inet_db with the result so subsequent BEAM-level lookups for the same host succeed from the in-process file table.

Android isn't affected — mob_beam.zig ships inet_gethost as libinet_gethost.so in jniLibs/, which the SELinux policy allows to execve. The NIF here would work on Android too but isn't wired up by default; the BEAM path is already functional there.

How to use it

Resolve each hostname your app talks to before the first Req / Finch / Mint call to that host. Once resolved, :inet_db retains the mapping for the lifetime of the BEAM, so subsequent HTTP calls go through without you doing anything else.

# At app startup, or before the first call:
{:ok, _ip} = Mob.DNS.resolve("api.example.com")

# Now this just works on iOS:
Req.get!("https://api.example.com/v1/things")

For a small fixed set of hosts, the convenience helper preresolve/1 does the whole list at once:

Mob.DNS.preresolve([
  "api.example.com",
  "auth.example.com"
])

Scope and limitations

  • IPv4 only. Most cloud endpoints serve A records; IPv6 is a follow-up if it becomes useful.
  • One IP per host. If the hostname has multiple A records, the first one is used. BEAM caches the result; failover isn't automatic. If your endpoint cycles IPs frequently you may need to re-resolve.
  • No automatic refresh. Mappings stay in :inet_db until the BEAM exits. If a backend's IP changes mid-session, the cached entry will be stale — call resolve/1 again to refresh.
  • Doesn't help raw NIF networking. If a third-party NIF calls libc getaddrinfo itself, it never goes through BEAM's DNS layer and doesn't need (or benefit from) this fix — it already works on iOS. Only :inet-mediated lookups (which covers almost all Elixir HTTP libraries) need our help.
  • iOS only effectively. On Android and host (dev, macOS, Linux) the NIF works but is unnecessary; BEAM's built-in path is fine there.

Errors

{:ok, {a, b, c, d}}              # success
{:error, :badarg}                # host arg invalid
{:error, :nxdomain}              # no such hostname
{:error, :timeout}               # resolver TRY_AGAIN
{:error, :no_address}            # resolved but no IPv4
{:error, {:gai, code}}           # raw getaddrinfo error code
{:error, :nif_not_loaded}        # called off-device (host tests)

Summary

Types

The error shapes resolve/1 can return.

Hostname to resolve. Latin-1 only — we're not in a domain that uses IDN.

Functions

Resolve a list of hostnames. Returns a map of host → result so the caller can see which ones failed.

Resolve host to an IPv4 address and seed :inet_db so subsequent :inet.getaddr/2 lookups (and thus Req / Finch / Mint) find it.

True when host is already seeded in :inet_db.

Types

error_reason()

@type error_reason() ::
  :badarg
  | :nxdomain
  | :timeout
  | :no_address
  | :nif_not_loaded
  | {:gai, integer()}

The error shapes resolve/1 can return.

host()

@type host() :: String.t() | charlist()

Hostname to resolve. Latin-1 only — we're not in a domain that uses IDN.

Functions

preresolve(hosts)

@spec preresolve([host()]) :: %{
  required(host()) => {:ok, :inet.ip4_address()} | {:error, error_reason()}
}

Resolve a list of hostnames. Returns a map of host → result so the caller can see which ones failed.

Useful at app startup for the known-fixed set of backends your app talks to.

%{
  "api.example.com" => {:ok, {93, 184, 216, 34}},
  "auth.example.com" => {:error, :nxdomain}
}

resolve(host)

@spec resolve(host()) :: {:ok, :inet.ip4_address()} | {:error, error_reason()}

Resolve host to an IPv4 address and seed :inet_db so subsequent :inet.getaddr/2 lookups (and thus Req / Finch / Mint) find it.

Idempotent — calling for the same host twice is harmless.

See module doc for usage, scope, and error shapes.

resolved?(host)

@spec resolved?(host()) :: boolean()

True when host is already seeded in :inet_db.

Useful for short-circuiting in caller code that wants to avoid an unnecessary NIF call — but resolve/1 is idempotent, so calling it again is also fine.