# `ExSandbox.Egress.Relay`
[🔗](https://github.com/FoundryStack/ex_sandbox/blob/v1.2.0/lib/ex_sandbox/egress/relay.ex#L1)

Forwards a **permitted** connection to its destination (005 T060a9,
`contracts/egress.md`).

## Why this is a module and not three lines in the accept loop

Egress.Pool's `relay/2` — since removed, along with the rest of that module's
socket handling and the name itself — 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 acceptor dies at `OriginalDst.read/1`, so a test
driving the listener never reaches this code at all — the same vacuity
`acceptor_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 acceptor failing to accept
long after and nowhere near the cause.

# `destination`

```elixir
@type destination() :: {:inet.ip4_address(), :inet.port_number()}
```

Where a permitted connection is headed.

# `splice`

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

# `upstream_mark`

```elixir
@spec upstream_mark() :: non_neg_integer()
```

The `SO_MARK` value `splice/3` asks `NetnsSocket.socket/2` to set.

Public **only** so `acceptor_mark_wiring_test.exs` can assert that the mark
used 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.

⚠️ `upstream_connect/4` below calls **this function**, not `acceptor_mark/0`
directly, and the indirection is the whole point rather than a style. It was
briefly inlined, and the test kept passing while proving nothing: it compared
`Netns.acceptor_mark()` to `Netns.acceptor_mark()` and would have stayed green
with the relay setting any value at all, or none. A seam nothing reads is a
seam that has stopped seaming.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
