Forwards a permitted connection to its destination (005 T060a9,
contracts/egress.md).
Why this is a module and not three lines in the pool
ExSandbox.Egress.Pool's relay/2 -- since removed -- was a placeholder that
logged and closed. That
was the honest shape while the netns did not exist — it denies something the
policy allows, which fails closed and is visible as the "permitted
destination is reachable" check not passing. It was never a false pass.
But it made two conformance checks unclosable, and separating the forwarding
from the socket accept loop is what makes the forwarding testable. Off-Linux
every connection to the pool dies at OriginalDst.read/1, so a test driving
the listener never reaches this code at all — the same vacuity
pool_transport_test.exs documents for the allowlist. splice/3 takes two
ordinary sockets, so its behaviour is reachable on any host.
⚠️ The bug direction that matters here
Every other component in this subsystem fails safe when it fails: a broken
decoder refuses, a missing policy denies, an unsupervised pool denies. The
relay is the one place where the natural bug goes the other way. Code that
forwards on an error path — a destination that could not be connected, a
socket in an unexpected state, a recv that returned something unhandled —
is a boundary that stops enforcing exactly when something is wrong with it.
So: this module never opens a socket it was not told to open, and never
continues past an error. decide/3 has already permitted this one
destination; the relay's only job is to carry bytes to it and to stop when
anything at all goes wrong. There is no retry, no fallback destination, and
no path where a failure results in more reachability than a success.
Both halves, and why the pair is torn down together
A half-duplex relay forwards the request and drops the response, which from inside the sandbox reads as a slow destination rather than a broken proxy — and passes every denial check. Both directions are carried, and either side closing tears down both, because a socket left open to a destination that is gone is a descriptor leak whose only symptom is the pool failing to accept long after and nowhere near the cause.
Summary
Types
Where a permitted connection is headed.
Functions
Connects to destination and carries bytes both ways until either side ends.
The options splice/3 passes to :gen_tcp.connect/4.
Types
@type destination() :: {:inet.ip4_address(), :inet.port_number()}
Where a permitted connection is headed.
Functions
@spec splice(:gen_tcp.socket(), destination(), keyword()) :: :ok | {:error, term()}
Connects to destination and carries bytes both ways until either side ends.
Returns :ok once the pair is torn down, and {:error, reason} if the
destination could not be reached — in which case sandbox_socket is closed
before returning, so the sandbox sees a refusal rather than a hang.
⚠️ sandbox_socket is closed on every path out of this function. From
inside the sandbox a closed socket is what a denied or unreachable
destination looks like (FR-011a), and leaving it open on an error path
turns a refusal into an indefinite hang that the conformance probe scores as
a timeout rather than a refusal.
@spec upstream_connect_opts() :: [:gen_tcp.connect_option()]
The options splice/3 passes to :gen_tcp.connect/4.
Public only so acceptor_mark_wiring_test.exs can assert that the
SO_MARK set here is the one the redirect exempts. Asserting on a duplicate
literal in the test would pass while the socket carried something else, which
is precisely the defect being guarded against.