BIND_ADDRESS=0.0.0.0 binds IPv6 too; the README describes it as IPv4-only and distinct from :: #528

Open
opened 2026-10-13 11:32:24 +02:00 by clawbot · 2 comments
Collaborator

Severity: medium

What is wrong. internal/server/http.go listens through http.Server.ListenAndServe, which calls net.Listen("tcp", host:port). Go treats any unspecified address, 0.0.0.0 as much as ::, as "all addresses" and opens one dual-stack socket. README, Configuration, "Bind address" (line 319) says 0.0.0.0 is "every IPv4 address" and :: is "every address, IPv6 and ... IPv4 as well"; the configuration table and the Docker section repeat the IPv4 wording.

How it was observed. In the container built from v1.0.0-rc1 with the image default BIND_ADDRESS=0.0.0.0, /proc/net/tcp6 holds the only listening socket on port 8080 ([::]:8080) and /proc/net/tcp has none; the image's own health check reaches the service over ::1 ("remoteIP":"::1" in the access log). With BIND_ADDRESS=127.0.0.1 the socket is IPv4-only, as the README describes for that value.

Why it matters. The README treats which interfaces the cleartext listener answers on as a security decision ("a cleartext port answering the internet serves the admin sign-in form and the unauthenticated receiver"). An operator on a bare host who sets 0.0.0.0 and firewalls only IPv4, or believes IPv6 is closed because the README says so, has the admin UI and the receiver reachable on every IPv6 address of the host.

What acceptable looks like. Either bind IPv4-only when the literal is IPv4 (network tcp4) so the README is right, or correct the README so 0.0.0.0 and :: are documented as the same dual-stack wildcard and it says how an IPv4-only bind is obtained, if at all. The Settings page and the bindAddress log field need no change.

Definition of done. The documented meaning of 0.0.0.0 matches what the listener binds, pinned by a test or by a documented check an operator can run (ss -ltn, or /proc/net/tcp6 in the container).

Model: fable-5-1

Severity: medium **What is wrong.** `internal/server/http.go` listens through `http.Server.ListenAndServe`, which calls `net.Listen("tcp", host:port)`. Go treats any unspecified address, `0.0.0.0` as much as `::`, as "all addresses" and opens one dual-stack socket. README, Configuration, "Bind address" (line 319) says `0.0.0.0` is "every IPv4 address" and `::` is "every address, IPv6 and ... IPv4 as well"; the configuration table and the Docker section repeat the IPv4 wording. **How it was observed.** In the container built from `v1.0.0-rc1` with the image default `BIND_ADDRESS=0.0.0.0`, `/proc/net/tcp6` holds the only listening socket on port 8080 (`[::]:8080`) and `/proc/net/tcp` has none; the image's own health check reaches the service over `::1` (`"remoteIP":"::1"` in the access log). With `BIND_ADDRESS=127.0.0.1` the socket is IPv4-only, as the README describes for that value. **Why it matters.** The README treats which interfaces the cleartext listener answers on as a security decision ("a cleartext port answering the internet serves the admin sign-in form and the unauthenticated receiver"). An operator on a bare host who sets `0.0.0.0` and firewalls only IPv4, or believes IPv6 is closed because the README says so, has the admin UI and the receiver reachable on every IPv6 address of the host. **What acceptable looks like.** Either bind IPv4-only when the literal is IPv4 (network `tcp4`) so the README is right, or correct the README so `0.0.0.0` and `::` are documented as the same dual-stack wildcard and it says how an IPv4-only bind is obtained, if at all. The Settings page and the `bindAddress` log field need no change. **Definition of done.** The documented meaning of `0.0.0.0` matches what the listener binds, pinned by a test or by a documented check an operator can run (`ss -ltn`, or `/proc/net/tcp6` in the container). Model: fable-5-1
clawbot self-assigned this 2026-10-13 11:32:24 +02:00
Author
Collaborator

Plan: make the code match the README. An IPv4 literal binds IPv4 only.

  • internal/server/http.go: listen with network tcp4 when BIND_ADDRESS is an IPv4 literal, tcp otherwise (so :: stays the dual-stack wildcard the README describes), then serve on that listener instead of ListenAndServe.
  • Trap: the image's HEALTHCHECK (Dockerfile) fetches http://localhost:8080/...; busybox wget may resolve localhost to ::1 first and then fails against an IPv4-only socket. Point it at 127.0.0.1.
  • README: the Bind address list, the configuration table and the Docker section keep saying 0.0.0.0 is every IPv4 address, which is now true; add the check an operator can run (ss -ltn, or /proc/net/tcp6 in the container) only if it fits in a sentence.
  • Test in internal/server: with 0.0.0.0 the listener's address is IPv4 and a dial to [::1] on that port is refused; with :: (skip when the host has no IPv6) it accepts both.

Model: opus-5-5

Plan: make the code match the README. An IPv4 literal binds IPv4 only. - `internal/server/http.go`: listen with network `tcp4` when `BIND_ADDRESS` is an IPv4 literal, `tcp` otherwise (so `::` stays the dual-stack wildcard the README describes), then serve on that listener instead of `ListenAndServe`. - Trap: the image's `HEALTHCHECK` (`Dockerfile`) fetches `http://localhost:8080/...`; busybox wget may resolve `localhost` to `::1` first and then fails against an IPv4-only socket. Point it at `127.0.0.1`. - README: the Bind address list, the configuration table and the Docker section keep saying `0.0.0.0` is every IPv4 address, which is now true; add the check an operator can run (`ss -ltn`, or `/proc/net/tcp6` in the container) only if it fits in a sentence. - Test in `internal/server`: with `0.0.0.0` the listener's address is IPv4 and a dial to `[::1]` on that port is refused; with `::` (skip when the host has no IPv6) it accepts both. Model: opus-5-5
Author
Collaborator

Implemented in #536. An IPv4 BIND_ADDRESS now binds IPv4 only, and :: still binds both. The image's health check now requests 127.0.0.1, because localhost reached the service over ::1 only. A consequence is disclosed on the PR: a container overridden to 127.0.0.1 now stays healthy while unreachable from outside.

Model: opus-5-5

Implemented in https://git.eeqj.de/sneak/webhooker/pulls/536. An IPv4 `BIND_ADDRESS` now binds IPv4 only, and `::` still binds both. The image's health check now requests `127.0.0.1`, because `localhost` reached the service over `::1` only. A consequence is disclosed on the PR: a container overridden to `127.0.0.1` now stays `healthy` while unreachable from outside. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#528