# `ConduitMcp.Client.URLPolicy`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/client/url_policy.ex#L1)

Decides whether the client may connect to a URL, before any connection is
made. It guards against server-supplied URLs (OAuth metadata, redirects,
`resource_metadata`) pointing the client at internal networks (SSRF).

    iex> ConduitMcp.Client.URLPolicy.check("https://127.1/", :public_only)
    {:error, {:blocked_address, {127, 0, 0, 1}}}

## Policies

  * `:public_only` - `https` only; the host is resolved (A and AAAA) and the
    URL is refused if **any** address is blocked (see below), unless that
    address falls inside an `:allow_private` CIDR. An IP-literal host is
    checked directly.
  * `:any` - `http` or `https`, no address checks and no DNS lookup.

On success the result carries the resolved `addresses`, so the caller can
connect to exactly those (pinning) instead of resolving again and racing a
DNS rebind; `host` stays the hostname for TLS SNI and certificate checks.

## Host parsing

Hosts are parsed, never pattern-matched as strings. A host whose last label
is numeric is parsed with the WHATWG URL IPv4 rules, so every notation a
browser or resolver accepts is normalized first: `127.1`, `0x7f.1`,
`0177.0.0.1`, `017700000001`, `2130706433` and `127.0.0.1.` are all
`{127, 0, 0, 1}`, and an invalid numeric host (`1.2.3.4.5`, `09.1`) is
refused. Bracketed IPv6 literals are parsed strictly. A URL with user info,
a percent-encoded or non-ASCII host, an IPv6 zone id, a backslash, or
whitespace or control characters is refused outright.

## Blocked addresses

IPv4: `0.0.0.0/8`, `10.0.0.0/8`, `100.64.0.0/10`, `127.0.0.0/8`,
`169.254.0.0/16`, `172.16.0.0/12`, `192.0.0.0/24`, `192.168.0.0/16`,
`198.18.0.0/15`, `224.0.0.0/4`, `240.0.0.0/4`.

IPv6: `::/96` (which holds `::`, `::1` and the deprecated IPv4-compatible
form), `100::/64`, `64:ff9b:1::/48`, `fc00::/7`, `fe80::/10`, `fec0::/10`,
`ff00::/8`. IPv6 forms that embed an IPv4 address are unwrapped and the
embedded address is checked against the IPv4 list: IPv4-mapped
`::ffff:0:0/96`, IPv4-translated `::ffff:0:0:0/96`, NAT64 `64:ff9b::/96`
and 6to4 `2002::/16`. So `[::ffff:127.0.0.1]` and `[64:ff9b::a00:1]` are
blocked, while a NAT64 address embedding a public IPv4 address (as DNS64
networks synthesize) is not.

# `cidr`

```elixir
@type cidr() :: {:inet.ip_address(), non_neg_integer()}
```

A CIDR block: a base address and a prefix length.

# `policy`

```elixir
@type policy() :: :public_only | :any
```

# `reason`

```elixir
@type reason() ::
  :invalid_url
  | {:scheme_not_allowed, String.t() | nil}
  | :userinfo_not_allowed
  | :invalid_host
  | {:blocked_address, :inet.ip_address()}
  | {:resolve_failed, term()}
```

# `resolver`

```elixir
@type resolver() :: (String.t() -&gt; {:ok, [:inet.ip_address()]} | {:error, term()})
```

# `target`

```elixir
@type target() :: %{
  scheme: String.t(),
  host: String.t(),
  port: :inet.port_number(),
  addresses: [:inet.ip_address()]
}
```

# `blocked?`

```elixir
@spec blocked?(:inet.ip_address()) :: boolean()
```

Whether `address` is in a blocked range, after unwrapping IPv6 forms that
embed an IPv4 address (see the moduledoc).

    iex> ConduitMcp.Client.URLPolicy.blocked?({0, 0, 0, 0, 0, 0xFFFF, 0x7F00, 1})
    true
    iex> ConduitMcp.Client.URLPolicy.blocked?({93, 184, 216, 34})
    false

# `check`

```elixir
@spec check(String.t(), policy(), keyword()) :: {:ok, target()} | {:error, reason()}
```

Checks `url` against `policy`.

## Options

  * `:allow_private` - CIDRs (strings such as `"10.1.0.0/16"`, or
    `t:cidr/0` tuples) whose addresses `:public_only` admits even though
    they are blocked. Raises `ArgumentError` on an invalid entry.
  * `:resolver` - `t:resolver/0` used for hostnames (default: the system
    resolver, A and AAAA).
  * `:resolve_timeout` - milliseconds per lookup family for the default
    resolver (default 5000).

# `parse_cidr`

```elixir
@spec parse_cidr(String.t() | cidr()) :: {:ok, cidr()} | :error
```

Parses a CIDR string (`"10.0.0.0/8"`, `"fd00::/8"`; a bare address is a
single host) or validates a `t:cidr/0` tuple.

    iex> ConduitMcp.Client.URLPolicy.parse_cidr("10.0.0.0/8")
    {:ok, {{10, 0, 0, 0}, 8}}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
