Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dae20faa24 |
+5
-1
@@ -274,8 +274,12 @@ EXPOSE 8080
|
|||||||
# port.
|
# port.
|
||||||
ENV BIND_ADDRESS=0.0.0.0
|
ENV BIND_ADDRESS=0.0.0.0
|
||||||
|
|
||||||
|
# The probe requests the container's own address, not loopback, so a
|
||||||
|
# listener that answers only on loopback (an empty or loopback
|
||||||
|
# BIND_ADDRESS) fails it. hostname -i prints one IPv4 address per
|
||||||
|
# network the container is on; the first is enough.
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/.well-known/healthcheck || exit 1
|
CMD wget --no-verbose --tries=1 --spider "http://$(hostname -i | cut -d ' ' -f 1):8080/.well-known/healthcheck" || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
CMD ["/app/webhooker"]
|
CMD ["/app/webhooker"]
|
||||||
|
|||||||
@@ -316,10 +316,9 @@ The value must be an IP address literal:
|
|||||||
|
|
||||||
- `127.0.0.1` — loopback only (the binary's default). Use this with a reverse
|
- `127.0.0.1` — loopback only (the binary's default). Use this with a reverse
|
||||||
proxy on the same host.
|
proxy on the same host.
|
||||||
- `0.0.0.0` — every IPv4 address. The image's default; on a bare host, only
|
- `0.0.0.0` — every IPv4 address and no IPv6 address. The image's default; on a
|
||||||
behind a firewall on the port.
|
bare host, only behind a firewall on the port.
|
||||||
- `::` — every address, IPv6 and (on Linux, with the default
|
- `::` — every IPv6 address and every IPv4 address.
|
||||||
`net.ipv6.bindv6only=0`) IPv4 as well.
|
|
||||||
- A specific address such as `10.0.0.5` — that interface only.
|
- A specific address such as `10.0.0.5` — that interface only.
|
||||||
|
|
||||||
An **empty** value is treated as unset, as everywhere else here, and takes the
|
An **empty** value is treated as unset, as everywhere else here, and takes the
|
||||||
@@ -340,7 +339,8 @@ machine that has no such interface — is a valid literal, so it reaches the
|
|||||||
listener and fails there. The process logs the bind error and exits non-zero
|
listener and fails there. The process logs the bind error and exits non-zero
|
||||||
rather than staying up with nothing listening. The effective value is in the
|
rather than staying up with nothing listening. The effective value is in the
|
||||||
`bindAddress` field of the startup log line, which is the way to check what a
|
`bindAddress` field of the startup log line, which is the way to check what a
|
||||||
running deployment actually bound.
|
running deployment actually bound. On a bare host, `ss -ltn` lists the socket
|
||||||
|
itself as `0.0.0.0:8080` for a `0.0.0.0` bind and as `*:8080` for `::`.
|
||||||
|
|
||||||
#### Metrics credentials
|
#### Metrics credentials
|
||||||
|
|
||||||
@@ -668,8 +668,8 @@ Either set a literal or leave the variable out entirely.
|
|||||||
Overriding `BIND_ADDRESS` to a loopback address in a container — by that route
|
Overriding `BIND_ADDRESS` to a loopback address in a container — by that route
|
||||||
or deliberately — makes the container unreachable from outside its namespace
|
or deliberately — makes the container unreachable from outside its namespace
|
||||||
even with `-p`. The published port answers nothing, and the health check fails
|
even with `-p`. The published port answers nothing, and the health check fails
|
||||||
too: it requests `http://localhost:8080`, `localhost` resolves to `::1` first,
|
too: it requests the container's own address (the first one `hostname -i`
|
||||||
and a `127.0.0.1` bind is not listening there. The container then goes
|
prints), where a loopback bind is not listening. The container then goes
|
||||||
`unhealthy` about **65 seconds** after start — from
|
`unhealthy` about **65 seconds** after start — from
|
||||||
`HEALTHCHECK --start-period=5s --interval=30s --retries=3`, so failing probes at
|
`HEALTHCHECK --start-period=5s --interval=30s --retries=3`, so failing probes at
|
||||||
5s, 35s and 65s, and `unhealthy` on the third. (Docker's probe cadence during
|
5s, 35s and 65s, and `unhealthy` on the third. (Docker's probe cadence during
|
||||||
@@ -726,8 +726,8 @@ the credentials.
|
|||||||
address (directly, or through anything that can rewrite source addresses,
|
address (directly, or through anything that can rewrite source addresses,
|
||||||
such as NAT or a published container port). The `remoteIP` field of the
|
such as NAT or a published container port). The `remoteIP` field of the
|
||||||
`http request` log line for a request that came through the proxy shows
|
`http request` log line for a request that came through the proxy shows
|
||||||
that address; the health check's own lines show `::1`. See
|
that address; the health check's own lines show the container's own
|
||||||
[Trusted proxies](#trusted-proxies).
|
address. See [Trusted proxies](#trusted-proxies).
|
||||||
- Leave `BIND_ADDRESS` and `DATA_DIR` unset: the image sets `BIND_ADDRESS`
|
- Leave `BIND_ADDRESS` and `DATA_DIR` unset: the image sets `BIND_ADDRESS`
|
||||||
to `0.0.0.0`, and `DATA_DIR` defaults to `/var/lib/webhooker`.
|
to `0.0.0.0`, and `DATA_DIR` defaults to `/var/lib/webhooker`.
|
||||||
- Everything else is optional; see [Configuration](#configuration).
|
- Everything else is optional; see [Configuration](#configuration).
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -25,6 +26,11 @@ const (
|
|||||||
// its network namespace even with a published port.
|
// its network namespace even with a published port.
|
||||||
wildcardV4 = "0.0.0.0"
|
wildcardV4 = "0.0.0.0"
|
||||||
|
|
||||||
|
// loopbackV6 and wildcardV6 are the IPv6 counterparts of the two
|
||||||
|
// above.
|
||||||
|
loopbackV6 = "::1"
|
||||||
|
wildcardV6 = "::"
|
||||||
|
|
||||||
// unavailableAddr is a TEST-NET-1 address (RFC 5737). It is a
|
// unavailableAddr is a TEST-NET-1 address (RFC 5737). It is a
|
||||||
// well-formed literal that no host is assigned, so binding it
|
// well-formed literal that no host is assigned, so binding it
|
||||||
// fails with EADDRNOTAVAIL rather than succeeding somewhere
|
// fails with EADDRNOTAVAIL rather than succeeding somewhere
|
||||||
@@ -116,6 +122,24 @@ func otherLocalAddr(t *testing.T) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skipWithoutIPv6 skips the test when this host cannot bind
|
||||||
|
// loopbackV6. Without IPv6 there is no second family for a bind to
|
||||||
|
// claim or leave alone.
|
||||||
|
func skipWithoutIPv6(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var listenCfg net.ListenConfig
|
||||||
|
|
||||||
|
l, err := listenCfg.Listen(
|
||||||
|
t.Context(), "tcp6", net.JoinHostPort(loopbackV6, "0"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("host has no IPv6 loopback to bind")
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, l.Close())
|
||||||
|
}
|
||||||
|
|
||||||
// startBoundServer starts the wired app with the given bind address
|
// startBoundServer starts the wired app with the given bind address
|
||||||
// on a free port and returns that port. The app is stopped on
|
// on a free port and returns that port. The app is stopped on
|
||||||
// cleanup.
|
// cleanup.
|
||||||
@@ -292,6 +316,50 @@ func TestBindAddress_WildcardReachesOtherAddresses(t *testing.T) {
|
|||||||
requireDialable(t, net.JoinHostPort(other, strconv.Itoa(port)))
|
requireDialable(t, net.JoinHostPort(other, strconv.Itoa(port)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBindAddress_WildcardV4IsIPv4Only pins that 0.0.0.0 answers on
|
||||||
|
// IPv4 alone, as the README says. Go listens on 0.0.0.0 with a socket
|
||||||
|
// that answers IPv6 too unless told otherwise, which put the admin UI
|
||||||
|
// and the receiver on every IPv6 address of a host whose operator had
|
||||||
|
// chosen an IPv4 bind.
|
||||||
|
func TestBindAddress_WildcardV4IsIPv4Only(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
skipWithoutIPv6(t)
|
||||||
|
|
||||||
|
port := startBoundServer(t, wildcardV4)
|
||||||
|
|
||||||
|
// Positive control: the service really is up, on IPv4.
|
||||||
|
requireDialable(t, net.JoinHostPort(loopbackV4, strconv.Itoa(port)))
|
||||||
|
|
||||||
|
dialer := net.Dialer{Timeout: dialTimeout}
|
||||||
|
|
||||||
|
conn, err := dialer.DialContext(
|
||||||
|
t.Context(), "tcp",
|
||||||
|
net.JoinHostPort(loopbackV6, strconv.Itoa(port)),
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
require.ErrorIs(
|
||||||
|
t, err, syscall.ECONNREFUSED,
|
||||||
|
"a %s bind answered on %s", wildcardV4, loopbackV6,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBindAddress_WildcardV6ReachesBothFamilies is the counterpart:
|
||||||
|
// :: is the value that binds IPv6, and it answers IPv4 as well.
|
||||||
|
func TestBindAddress_WildcardV6ReachesBothFamilies(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
skipWithoutIPv6(t)
|
||||||
|
|
||||||
|
port := startBoundServer(t, wildcardV6)
|
||||||
|
|
||||||
|
requireDialable(t, net.JoinHostPort(loopbackV6, strconv.Itoa(port)))
|
||||||
|
requireDialable(t, net.JoinHostPort(loopbackV4, strconv.Itoa(port)))
|
||||||
|
}
|
||||||
|
|
||||||
// TestBindAddress_ServesRequestsOnConfiguredAddress proves the bound
|
// TestBindAddress_ServesRequestsOnConfiguredAddress proves the bound
|
||||||
// listener serves the application rather than merely accepting TCP,
|
// listener serves the application rather than merely accepting TCP,
|
||||||
// so a bind address that is honoured cannot be mistaken for one that
|
// so a bind address that is honoured cannot be mistaken for one that
|
||||||
|
|||||||
+28
-1
@@ -1,9 +1,11 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -39,6 +41,21 @@ func (s *Server) listenAddr() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listenNetwork returns the network the HTTP listener binds on, "tcp4"
|
||||||
|
// for an IPv4 BindAddress and "tcp" for any other.
|
||||||
|
//
|
||||||
|
// On "tcp", Go binds 0.0.0.0 with a socket that answers IPv6 as well,
|
||||||
|
// exactly as it binds ::. "tcp4" keeps 0.0.0.0 to IPv4, which is what
|
||||||
|
// the README says it means. :: stays on "tcp" and answers both.
|
||||||
|
func (s *Server) listenNetwork() string {
|
||||||
|
addr, err := netip.ParseAddr(s.params.Config.BindAddress)
|
||||||
|
if err == nil && addr.Is4() {
|
||||||
|
return "tcp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "tcp"
|
||||||
|
}
|
||||||
|
|
||||||
// newHTTPServer builds the HTTP server for this Server's
|
// newHTTPServer builds the HTTP server for this Server's
|
||||||
// configuration.
|
// configuration.
|
||||||
//
|
//
|
||||||
@@ -67,7 +84,17 @@ func (s *Server) serveUntilShutdown() {
|
|||||||
|
|
||||||
s.log.Info("http begin listen", "listenaddr", s.httpServer.Addr)
|
s.log.Info("http begin listen", "listenaddr", s.httpServer.Addr)
|
||||||
|
|
||||||
err := s.httpServer.ListenAndServe()
|
// ListenAndServe always listens on "tcp", which would undo
|
||||||
|
// listenNetwork.
|
||||||
|
var listenCfg net.ListenConfig
|
||||||
|
|
||||||
|
listener, err := listenCfg.Listen(
|
||||||
|
context.Background(), s.listenNetwork(), s.httpServer.Addr,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
err = s.httpServer.Serve(listener)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
s.log.Error("listen error", "error", err)
|
s.log.Error("listen error", "error", err)
|
||||||
s.shutdownWithFailure()
|
s.shutdownWithFailure()
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
|
|||||||
s.httpServer = s.newHTTPServer()
|
s.httpServer = s.newHTTPServer()
|
||||||
|
|
||||||
lc.Append(fx.Hook{
|
lc.Append(fx.Hook{
|
||||||
|
//nolint:contextcheck // The listener outlives this hook, whose
|
||||||
|
// context fx cancels once the app has started.
|
||||||
OnStart: func(_ context.Context) error {
|
OnStart: func(_ context.Context) error {
|
||||||
s.startupTime = time.Now()
|
s.startupTime = time.Now()
|
||||||
go s.Run()
|
go s.Run()
|
||||||
|
|||||||
Reference in New Issue
Block a user