ExSandbox.Egress.Binding (ExSandbox v1.0.0)

Copy Markdown View Source

Ties one sandbox's resolved allowlist to the /30 its traffic arrives from, and takes both back together (005 T060a2/T060a6, 005-FR-011aFR-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.

Summary

Types

t()

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

Functions

Takes a /30 and files allowed under it.

Gives back the policy and then the /30.

Types

t()

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

Functions

acquire(allowed, opts \\ [])

@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(binding, opts \\ [])

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