Search documentation of mob

# Mob.DNS (mob v0.6.18)

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

### Recommended (set-and-forget): `configure_pure_beam/1`

At app startup, flip BEAM's lookup chain from the broken `:native` path to `[:file, :dns]` and seed fallback nameservers. After this, every `:inet.getaddr/2` resolves via raw DNS queries from inside BEAM (no port program, no `execve`), and the usual HTTP libraries just work:

    def on_start do
      Mob.DNS.configure_pure_beam()
      # …rest of startup…
    end

Defaults to Google + Cloudflare DNS. Override via opt if your network requires it. See `configure_pure_beam/1` for details and trade-offs vs the Apple-resolver-via-NIF path below.

### Per-host (when Apple-resolver semantics matter): `resolve/1`

For hostnames that need iOS's resolver — VPN-pushed DNS, `.local` / mDNS, search-domain expansion, captive portals — call `resolve/1` for each one. Idempotent and cheap; safe to call alongside `configure_pure_beam/0` (the `:file` lookup runs first, so manually-resolved entries win over the `:dns` fallback).

    {:ok, _ip} = Mob.DNS.resolve("internal.corp.local")

For a small fixed set, `preresolve/1` does the whole list at once:

    Mob.DNS.preresolve([
      "internal.corp.local",
      "service.local"
    ])

Both paths compose. The recommended pattern is `configure_pure_beam` at startup as the default, then `resolve/1` only for the OS-resolver specials.

## 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

error_reason()

The error shapes `resolve/1` can return.

host()

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

### Functions

configure_pure_beam(opts \\ [])

Configure BEAM's DNS path so `:inet.getaddr/2` (and Req / Finch / Mint / `gen_tcp:connect/3` with a hostname) works without per-host setup.

preresolve(hosts)

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

resolve(host)

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

resolved?(host)

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

# configure_pure_beam(opts \\ [])

    @spec configure_pure_beam([{:nameservers, [:inet.ip_address()]}]) :: :ok

Configure BEAM's DNS path so `:inet.getaddr/2` (and Req / Finch / Mint / `gen_tcp:connect/3` with a hostname) works without per-host setup.

Sets the lookup chain to `[:file, :dns]` and seeds fallback nameservers. Both ops are idempotent.

## Why

BEAM's default `:native` lookup spawns `inet_gethost`, which iOS refuses to `execve`. The `:dns` lookup, by contrast, performs raw UDP/TCP DNS queries from inside BEAM via `gen_udp` / `gen_tcp` — no port program, no fork. iOS doesn't block sockets, so the `:dns` path Just Works.

After calling this, the whole `:inet`-mediated HTTP stack stops needing a per-host `resolve/1` call. `:file` stays first in the chain so any host you do `resolve/1` manually still wins — the two paths compose.

## When NOT to default to this

Reach for per-host `resolve/1` (which uses libc `getaddrinfo` via the NIF, going through Apple's resolver) when you need any of:

- VPN-pushed DNS for internal hostnames
- `.local` / mDNS service discovery
- Search-domain expansion (single-label hostnames like `https://api/`)
- Captive-portal-aware lookup
- Encrypted-DNS-at-OS-level (DoH / DoT configured in iOS Settings)

These all require Apple's resolver, which only the NIF path consults. The pure-BEAM `:dns` path queries whatever nameservers you seed and nothing else.

## Opts

- `:nameservers` — list of nameserver IP tuples (IPv4 or IPv6). Defaults to `[{8, 8, 8, 8}, {1, 1, 1, 1}]` (Google + Cloudflare). Pass any list, including `[]` to skip seeding (e.g. if your app's `:kernel` env already configures them). Common alternatives:

  - `[{9, 9, 9, 9}]` — Quad9 (privacy-leaning, no logging)
  - `[{10, 0, 0, 1}, {10, 0, 0, 2}]` — your corporate resolvers

## Idempotent

Calling this twice is a no-op on the second call — duplicate nameservers aren't added, the lookup chain isn't reordered.

## Examples

    # Default — most apps need nothing more
    Mob.DNS.configure_pure_beam()

    # Override the fallback nameservers
    Mob.DNS.configure_pure_beam(nameservers: [{9, 9, 9, 9}])

    # Set the lookup chain but skip nameserver seeding
    Mob.DNS.configure_pure_beam(nameservers: [])

# 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.