The 029-FR-016 instrument: did any bytes cross?
Every probe in 029 Phase 1 onward is built on this. It exists because the
obvious instrument — "did :gen_tcp.connect/4 return :ok?" — reports a
boundary that is not there.
Why "did connect succeed?" is the wrong question
⚠️ ExSandbox.Egress.Acceptor is a transparent proxy. The redirect sends
every outbound connection to it, so the TCP handshake completes — against the
acceptor — for a permitted and a refused destination alike. On a refusal the
acceptor closes without answering, which is what FR-011a requires: a
denied destination must be indistinguishable from an unreachable one.
MEASURED (egress-research.md:365-368):
# a listener that accepts and immediately closes -- the refusal path
:gen_tcp.connect(~c"127.0.0.1", port, [], 2000) #=> {:ok, #Port<0.4>}A connect-shaped probe gets :ok both times and reports egress policed when
nothing is policed. This is the third weak-negative instrument this corpus has
been bitten by.
Why the probe speaks first
⚠️ Most services — 1.1.1.1:443 among them — wait for the client before
saying anything. A read-only probe cannot tell a reached-but-quiet destination
from a refused one inside its timeout, so leg/4 always sends before it
reads.
⚠️ Silence is not a refusal — the reason this module is not one function
A destination that drops packets and a destination with nobody listening are
indistinguishable. Both are silence. So :silent on its own is not
evidence of anything, and a probe that reports it as a refusal is a probe that
passes when the network is unplugged, when the timeout is too short, when the
responder never started, and when the runner has no route at all.
The previous phase hit this exactly: a UDP check passed, and the pass was vacuous, because both legs it probed were under no obligation to answer.
So silence is only readable as a refusal once something else has
established, independently, that bytes could have crossed. That is the
reference leg, and attempt/2 will not return a refusal without one:
reference leg → subject leg → reference leg⚠️ The reference leg runs on both sides of the subject, and both runs must cross. Running it only beforehand leaves a window: the responder dies, the route drops, the container is torn down, and the subject's silence is then scored as a held boundary. Bracketing costs two loopback round-trips and closes that window.
⚠️ The reference leg is not the subject destination. Its job is to answer "if bytes could cross right now, would this instrument see them?" — it is aimed at a responder that is obliged to answer. A reference leg pointed at the same destination the subject is testing answers a different, useless question.
⚠️ This instrument deliberately disagrees with Mechanism.Beam.probe_exprs/3
That probe scores a read timeout as reached, and its comment is right to: its question is "was this refused?", asked with no reference leg, and with no reference leg the safe reading of silence is "reached" — because scoring silence as a refusal would be the false pass, reporting a held boundary while a real breach against a quiet service goes unnoticed.
This module scores the same timeout as :silent, and then refuses to
interpret it until the reference leg has crossed twice. The two are not in
conflict; they are the same caution reached from opposite ends. probe_exprs/3
declines to read silence at all. attempt/2 earns the right to read it.
Answers and verdicts are different types, on purpose
answer/0 is what one leg observed. verdict/0 is what the set of legs
means. Collapsing them is how a single silent leg becomes a claimed boundary.
leg/4 :: ... -> answer() # an observation, uninterpreted
verdict/2 :: answer, answer -> verdict() # pure, and the only interpreter
attempt/2 :: fun, fun -> verdict() # runs the legs, gatedverdict/2 is pure and takes no sockets, so the interpretation table is
testable without a network.
Summary
Types
What a single leg observed. Uninterpreted on purpose.
A responder standing by to answer the reference leg.
The transports 029 has to cover. FR-013: an allowlist a tenant can bypass by choosing a transport is not an allowlist.
What the set of legs means.
Functions
The instrument. Runs reference → subject → reference and interprets.
Stops a responder.
Runs one leg and reports what it observed, without interpreting it.
A convenience leg aimed at a responder — the usual reference for attempt/2.
Starts a responder for the reference leg to aim at, on an ephemeral port.
Interprets a subject leg in light of a reference leg. Pure.
Types
What a single leg observed. Uninterpreted on purpose.
{:crossed, bytes}— bytes came back. The only positive evidence there is.:silent— nothing came back inside the timeout, or the peer closed without answering. ⚠️ Means nothing on its own. See the moduledoc.{:no_socket, reason}— the socket could not be opened or the connect failed outright (:econnrefused,:enetunreach, …). Also not, by itself, a refusal: an unplugged cable produces it too.
@type responder() :: %{ transport: transport(), address: :inet.ip_address(), port: :inet.port_number(), banner: binary(), pid: pid() }
A responder standing by to answer the reference leg.
@type transport() :: :tcp | :udp
The transports 029 has to cover. FR-013: an allowlist a tenant can bypass by choosing a transport is not an allowlist.
@type verdict() :: {:reached, binary()} | {:refused, answer()} | {:inconclusive, {:reference_leg, :before | :after, answer()}}
What the set of legs means.
{:reached, bytes}— bytes crossed from the subject vantage point.{:refused, answer}— the subject observed no bytes while the reference leg was crossing, carrying the subject's own answer as the evidence.{:inconclusive, reason}— the reference leg did not cross, so nothing the subject observed can be attributed. ⚠️ This is a result, not an error. A run that reports it has not failed; it has declined to lie.
Functions
The instrument. Runs reference → subject → reference and interprets.
Both functions are zero-arity and return an answer/0 — typically
leg/4 partially applied. Keeping them opaque is what lets the subject leg
run somewhere this process cannot reach directly (inside a sandbox, over
:peer.call/4, through nsenter) while the reference leg runs here.
⚠️ Both reference runs must cross. If the one after the subject does not,
the verdict is {:inconclusive, {:reference_leg, :after, answer}} even though
the first one crossed — the machinery stopped working at some point during the
subject leg and there is no way to know which side of it.
@spec close_responder(responder()) :: :ok
Stops a responder.
⚠️ Kills the pid this module started, captured at responder/2. Never a
pattern-matched sweep over processes.
@spec leg( transport(), :inet.ip_address() | charlist() | String.t(), :inet.port_number(), keyword() ) :: answer()
Runs one leg and reports what it observed, without interpreting it.
⚠️ Returns an answer/0, never a verdict. A caller reading :silent as a
refusal without a reference leg has rebuilt the bug this module exists to
prevent — use attempt/2.
Options: :payload (default "ex-sandbox-byte-probe"),
:timeout (default 3000 ms).
A convenience leg aimed at a responder — the usual reference for attempt/2.
Starts a responder for the reference leg to aim at, on an ephemeral port.
⚠️ Ephemeral (port 0) and bound to loopback. 029's tasks marked [P] must
not bind a fixed port — the contended resources in this project are the build
and port 4002, not the files.
The responder reads whatever the probe says, replies with banner, and closes.
⚠️ It reads before it replies so it behaves like a real service, and so a
probe that speaks first does not race its reply against the close.
Options: :banner (default a unique binary), :address (default
{127, 0, 0, 1}).
Interprets a subject leg in light of a reference leg. Pure.
⚠️ The {:crossed, _} reference clause comes first and there is no clause
that reads a subject answer without one. That is the gate, expressed as
pattern matching rather than as discipline: there is no code path from
:silent to {:refused, _} that does not pass a crossing reference leg.