# `ExSandbox.Egress.Binding`
[🔗](https://github.com/FoundryStack/ex_sandbox/blob/v1.2.0/lib/ex_sandbox/egress/binding.ex#L1)

Ties one sandbox's resolved allowlist to the `/30` its traffic arrives from,
and takes both back together (005 T060a2/T060a6, `005-FR-011a`–`FR-011e`).

## Why the pair is a module rather than two calls

Acquiring an address and registering a policy are two operations that must
succeed or fail as one. Split across a caller they are two lines that read
independently and can be reordered, half-applied, or partly rolled back — and
every one of those states is *silent*:

  * address without policy: the sandbox is denied everything, which is
    indistinguishable from correct operation under checks that test denial,
  * policy without address: the entry is filed under a `/30` no sandbox will
    ever send from, so it enforces nothing and never expires,
  * released address with a live policy: the next tenant to receive that
    `/30` inherits it (`ExSandbox.Egress.Registry`'s reuse race).

`acquire/2` rolls the address back if registration fails, so the pool never
holds an entry the caller does not know about.

## Ordering on the way out

`release/2` drops the **policy first**, then the address — and passes the
registry check to `ExSandbox.Egress.Allocator.release/3` as a predicate
rather than relying on having done it. If a later refactor reorders these two
lines the allocator still refuses to recycle a `/30` whose policy stands, and
the address stays out rather than being handed to the next tenant. That is
the difference between an invariant and a comment.

# `t`

```elixir
@type t() :: %ExSandbox.Egress.Binding{
  gateway_address: String.t(),
  sandbox_address: String.t(),
  source_key: ExSandbox.Egress.Policy.source_key()
}
```

What a launched sandbox needs to build its namespace, and what a destroyed
one needs to give back.

# `acquire`

```elixir
@spec acquire(
  [ExSandbox.Egress.Policy.destination()],
  keyword()
) :: {:ok, t()} | {:error, ExSandbox.Egress.Allocator.refusal()}
```

Takes a `/30` and files `allowed` under it.

Refuses with `{:error, :pool_exhausted}` rather than issuing an unpoliced
sandbox: a tenant who cannot be given a policy must not be given a sandbox
that reaches everything instead.

# `release`

```elixir
@spec release(
  t(),
  keyword()
) :: :ok
```

Gives back the policy and then the `/30`.

Idempotent, and safe for a binding this host never issued (`003-FR-013`):
destroy runs for sandboxes that failed to provision, and runs twice for
sandboxes that did.

---

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