Skip to content

feat: a bind address you can set, because a container cannot use loopback - #12

Merged
lntvan166 merged 1 commit into
mainfrom
feat/configurable-bind-host
Aug 21, 2026
Merged

feat: a bind address you can set, because a container cannot use loopback#12
lntvan166 merged 1 commit into
mainfrom
feat/configurable-bind-host

Conversation

@lntvan166

Copy link
Copy Markdown
Owner

The bug this started from

docker-compose.yml shipped a container nothing could reach.

It published 127.0.0.1:8787:8787 — correctly, that prefix is what keeps the
dashboard off the LAN — but paddock bound 127.0.0.1 inside the container.
A published port is delivered to the container's own interface, so
docker-proxy dialled an address the listener refused.

Measured both ways on a spare port, so a live instance was never touched:

bind inside container published port
127.0.0.1 (before) HTTP 000 — unreachable
0.0.0.0 (after) HTTP 200, herdrConnected: true, 5 agents

Both refused connections from the host's LAN address. The publish prefix is
what actually protects the port, and the compose comment now says so — it
previously explained only half of the mechanism.

The change

PADDOCK_HOST, defaulting to 127.0.0.1. Empty or whitespace resolves to
loopback rather than a wildcard: PADDOCK_HOST: left half-written in a compose
file must not silently publish an unauthenticated dashboard.

A non-loopback bind warns and proceeds. It cannot be an error, because a
container requires one; it must not be silent, because decision 3 gives this
listener no authentication at all. Reachability is authority here — anything
that can open the port can read every agent's screen and type into it.

  paddock  `http://127.0.0.1:8799`  (all interfaces, port 8799)

paddock: bound to 0.0.0.0:8799 — not loopback
  every host that can reach this port has full control of your agents:
  it can read their screens and type into them. paddock has
  no authentication of its own (docs/decisions.md decision 3).
  In a container this is expected — keep the published port on 127.0.0.1.
  On a desk it means your network can drive your agents.

The banner no longer prints http://0.0.0.0:8787. It reads as a link and opens
nowhere.

Two deliberate choices

isLoopbackBind is not origin.ts's isLoopbackHost. That one classifies
Host headers, which name one machine and are never wildcards. 0.0.0.0 and
:: exist only as bind addresses. Sharing the predicate would put a seam in the
thing that file exists to keep seamless.

Decision 3 gets a scope note, not a silent contradiction. It asserted
paddock "binds loopback only", which is now false. The note records that this
adds no authentication and removes none, that what protects a non-loopback bind
is whatever sits in front of the port, and that it is not permission to bind
0.0.0.0 on a workstation.

tunnel/run.ts keeps its own hardcoded loopback bind — out of scope.

Verification

  • make check clean, make check-clean clean, 1033 pass / 0 fail
  • Default bind unchanged: LISTEN 127.0.0.1:8798, refused from the LAN, no warning
  • Wildcard bind: LISTEN 0.0.0.0:8799, HTTP 200 from the LAN address, warning shown
  • Real docker compose up --build, then torn down and the image removed

check-clean twice caught RFC1918 literals in the new test data and in a
comment; both were replaced with RFC 5737 documentation addresses rather than
touching the denylist.

🤖 Generated with Claude Code

…back

`docker-compose.yml` shipped a container nothing could reach. It published
`127.0.0.1:8787:8787`, but paddock bound `127.0.0.1` INSIDE the container, and
a published port is delivered to the container's own interface — so
docker-proxy dialled an address the listener refused. Measured both ways on a
spare port: loopback bind answers HTTP 000 through the publish, `0.0.0.0`
answers 200 with herdr connected.

So `PADDOCK_HOST`, defaulting to `127.0.0.1`. Empty or whitespace resolves to
loopback rather than a wildcard: `PADDOCK_HOST:` left half-written in a compose
file must not silently publish an unauthenticated dashboard.

A non-loopback bind warns and proceeds. It cannot be an error — a container
requires one — and it must not be silent, because decision 3 gives this
listener no authentication at all, so reachability IS authority: anything that
can open the port can read every agent's screen and type into it. The warning
names the address and says that plainly.

`isLoopbackBind` is deliberately NOT `origin.ts`'s `isLoopbackHost`. That one
classifies `Host` headers, which name one machine and are never wildcards;
`0.0.0.0` and `::` exist only as bind addresses. Sharing the predicate would
put a seam in the thing that file exists to keep seamless.

The banner no longer prints `http://0.0.0.0:8787`. It reads as a link and
opens nowhere; a wildcard bind gets a loopback URL plus the fact of the
wildcard.

Decision 3 asserted paddock "binds loopback only", which is now false, so it
gets a scope note rather than a silent contradiction — including that this adds
no authentication and removes none, and that what protects a non-loopback bind
is whatever sits in front of the port.

Test data uses RFC 5737 documentation addresses: `check-clean` refuses private
address literals in a public repo, and the predicate cannot tell them apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 21, 2026 09:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@lntvan166
lntvan166 merged commit 8d1f520 into main Aug 21, 2026
1 check passed
@lntvan166
lntvan166 deleted the feat/configurable-bind-host branch August 21, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants