Which destinations one sandbox may reach, and how that sandbox is identified
(005 T060a1/T060a2, contracts/egress.md, 005-FR-011a–FR-011e).
Identity is the source address, and it is unforgeable
A sandbox's policy is keyed by the source /30 its packets arrive from.
That address is the kernel's account of which network namespace a connection
originated in — :inet.peername/1 on the accepted socket, not anything the
connecting party said.
This matters more than it looks. Tenant code cannot claim another sandbox's allowlist, because it never asserts an identity at all: there is no token to steal, no header to spoof, and no name to collide with. The strongest form of an authorization check is one where the subject cannot participate in answering it.
⚠️ That property depends on no sandbox having a route to any other
(005-FR-011c, 003-FR-002). If two sandboxes shared a link, one could
originate a connection from within the other's /30 and inherit its policy.
This is why ExSandbox.SharedRouteMechanism exists as an adversary: it is
conformant in every outward-facing respect and fails only that, so a check
that trusts topology instead of attempting a crossing shows up as a false
pass.
Default deny
permits?/2 answers false for a /30 with no entry. A missing policy is not
an absent restriction — it is the most restrictive one. FR-011a requires an
allowlist over default-deny, and the ordering is the whole guarantee: a
lookup miss must never be the path by which something becomes reachable.
Summary
Types
A permitted destination. :any_port admits a host on every port; a specific
port admits only that one.
An IPv4 address as :inet reports it.
What one sandbox resolved, per name (029-FR-012).
The /30 a sandbox's connections originate from, as {a, b, c, d}.
Functions
The canonical form of a DNS name for comparison: lower-cased, with any trailing root dot removed.
True only when destination is explicitly permitted for source.
Reduces a source address to the /30 it belongs to.
Types
@type destination() :: {ip() | String.t(), :inet.port_number() | :any_port}
A permitted destination. :any_port admits a host on every port; a specific
port admits only that one.
@type ip() :: :inet.ip4_address()
An IPv4 address as :inet reports it.
@type resolutions() :: %{optional(String.t()) => MapSet.t(:inet.ip_address())}
What one sandbox resolved, per name (029-FR-012).
⚠️ Per sandbox, never global. A shared table would let one tenant's resolution of a name decide another tenant's verdict for it.
@type source_key() :: ip()
The /30 a sandbox's connections originate from, as {a, b, c, d}.
Functions
The canonical form of a DNS name for comparison: lower-cased, with any trailing root dot removed.
Public because the recording side and the matching side must agree exactly, and two copies of this would be two things that must stay equal forever. The symptom of them drifting is a permitted host that is silently refused, which reads as an unreachable network.
@spec permits?( [destination()], {ip() | String.t(), :inet.port_number()}, resolutions() ) :: boolean()
True only when destination is explicitly permitted for source.
Returns false for an unknown source — see the default-deny note above.
resolutions is what this sandbox resolved, as
ExSandbox.Egress.Registry.resolutions/2 returns it: %{name => MapSet of addresses}. It is what makes a hostname entry able to match at all
(029-FR-012); omitted, it defaults to %{} and no hostname entry matches
anything, which is the pre-029 behaviour and is default-deny.
@spec source_key(ip()) :: source_key()
Reduces a source address to the /30 it belongs to.
⚠️ Masking is what makes the key stable. A sandbox's connections come from a host address inside its /30, and that address is not necessarily the network address — so keying on the raw source would miss. Masking to the /30 answers "which sandbox is this?" rather than "which address is this?".