ExSandbox.Egress.HostAliases (ExSandbox v1.0.0)

Copy Markdown View Source

Every address that is this host, as this host currently reports itself (029 T014's supply half, 029-FR-015, D103).

Why this exists as a separate module

ExSandbox.Egress.Allowlist.parse/2 takes the alias set as data and must keep doing so: it is the module that transfers whole to a second mechanism precisely because it names no mechanism, and a capability query from inside it would be the re-coupling that argument forbids. So somebody has to produce the list, and that somebody is here — on the platform side of the seam, where knowing about interfaces and routing tables is allowed.

⚠️ Discovered, never configured, and the difference is the whole point. A configured constant would be right in the config file and wrong in the namespace, and the drift is the defect: every parse-time test stays green while the address actually in use is one no entry is refused for. The list is read from the running kernel at the moment it is needed.

What counts as the host

sourcewhy
every address on every interface (:inet.getifaddrs/0)these are literally this machine, under every name it answers to
the default gatewaysee below

⚠️ The default gateway is included even though --no-map-gw is passed, and that is belt-and-braces rather than confusion. pasta's default is to map the namespace's gateway address onto the host, which makes it a second name for the host that is not 127.0.0.1 — the case D103 was written about. The flag turns that default off, so today the gateway is the real upstream router instead. Refusing it anyway costs an operator nothing they should have wanted (a sandbox has no business dialling the host's own next hop), and it means the exclusion does not quietly evaporate if the flag is ever dropped — which is exactly the kind of silent re-widening netns.ex documents --no-map-gw against.

⚠️ :host_alias wins over :rfc1918_private when both match, which is the ordering Allowlist.host_class/2 already fixes. Nearly every address here is also private, so without that ordering this whole list would be invisible in the error an operator reads.

What this cannot see

Read at the moment of the call, so an address added afterwards is not in a list already produced. That is a real gap and is stated rather than papered over: the alternative — re-reading per comparison — would put a syscall inside a pure policy function, which is the coupling the seam exists to prevent. Provisioning is where the list is taken, and provisioning is also where the refusal must land, so the two are the same moment.

⚠️ It also cannot see a name for the host (host.docker.internal and friends). Allowlist.parse/2 accepts names in the alias set and nothing here produces one, so an operator who writes a host name is not refused as :host_alias today. Unfixed here on purpose: guessing at names would produce a list that is wrong in a different direction, and the answer-side filter in ExSandbox.Egress.Resolver catches the name once it resolves.

Summary

Types

An address that is this host.

Functions

Every address that is this host, right now.

The gateway addresses in ip route show default output.

Types

host_alias()

@type host_alias() :: :inet.ip_address()

An address that is this host.

Functions

detect()

@spec detect() :: [host_alias()]

Every address that is this host, right now.

Returns [] on a host whose interfaces cannot be read — which is a loss of a guard, so it is logged rather than passing silently. It is not fatal: the static classes (:loopback, :rfc1918_private, …) still refuse most of what this list would have caught, so an empty answer narrows the guard rather than removing it.

parse_default_via(output)

@spec parse_default_via(String.t()) :: [host_alias()]

The gateway addresses in ip route show default output.

Public so the parsing is testable on a host that has no ip(8) and no default route — which is every machine this is written on. A parser exercised only where the command runs is one whose failure mode is a silently empty guard.