Go listens on any unspecified address with one socket that answers IPv4 and IPv6, so BIND_ADDRESS=0.0.0.0 also bound every IPv6 address, which the README says it does not. The listener now uses tcp4 for an IPv4 address; :: still binds both. The server's start hook suppresses contextcheck, as the other start hooks do. The image's health check asked for localhost, which wget reached over ::1 only, so it now asks for 127.0.0.1. The README says what that means for a loopback bind in a container, drops the bindv6only condition from :: (Go sets that socket option itself), and names an ss check. Model: opus-5-5
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -25,6 +26,11 @@ const (
|
||||
// its network namespace even with a published port.
|
||||
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
|
||||
// well-formed literal that no host is assigned, so binding it
|
||||
// fails with EADDRNOTAVAIL rather than succeeding somewhere
|
||||
@@ -116,6 +122,24 @@ func otherLocalAddr(t *testing.T) string {
|
||||
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
|
||||
// on a free port and returns that port. The app is stopped on
|
||||
// cleanup.
|
||||
@@ -292,6 +316,50 @@ func TestBindAddress_WildcardReachesOtherAddresses(t *testing.T) {
|
||||
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
|
||||
// listener serves the application rather than merely accepting TCP,
|
||||
// so a bind address that is honoured cannot be mistaken for one that
|
||||
|
||||
+28
-1
@@ -1,9 +1,11 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"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
|
||||
// configuration.
|
||||
//
|
||||
@@ -67,7 +84,17 @@ func (s *Server) serveUntilShutdown() {
|
||||
|
||||
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) {
|
||||
s.log.Error("listen error", "error", err)
|
||||
s.shutdownWithFailure()
|
||||
|
||||
@@ -135,6 +135,8 @@ func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
|
||||
s.httpServer = s.newHTTPServer()
|
||||
|
||||
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 {
|
||||
s.startupTime = time.Now()
|
||||
go s.Run()
|
||||
|
||||
Reference in New Issue
Block a user