diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 05f8a7517..8bcaebe69 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -90,7 +90,16 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // the log configuration for an HTTPS enabled server var logCfg *ServerLogConfig - for srvName, srv := range app.Servers { + // Sort server names to ensure deterministic iteration. + // This prevents race conditions where the order of server processing + // could affect which server gets assigned the HTTP->HTTPS redirect listener. + srvNames := make([]string, 0, len(app.Servers)) + for name := range app.Servers { + srvNames = append(srvNames, name) + } + slices.Sort(srvNames) + for _, srvName := range srvNames { + srv := app.Servers[srvName] // as a prerequisite, provision route matchers; this is // required for all routes on all servers, and must be // done before we attempt to do phase 1 of auto HTTPS, @@ -398,15 +407,26 @@ uniqueDomainsLoop: return append(routes, app.makeRedirRoute(uint(app.httpsPort()), MatcherSet{MatchProtocol("http")})) } + // Sort redirect addresses to ensure deterministic process + redirServerAddrsSorted := make([]string, 0, len(redirServers)) + for addr := range redirServers { + redirServerAddrsSorted = append(redirServerAddrsSorted, addr) + } + slices.Sort(redirServerAddrsSorted) + redirServersLoop: - for redirServerAddr, routes := range redirServers { + for _, redirServerAddr := range redirServerAddrsSorted { + routes := redirServers[redirServerAddr] // for each redirect listener, see if there's already a // server configured to listen on that exact address; if so, // insert the redirect route to the end of its route list // after any other routes with host matchers; otherwise, // we'll create a new server for all the listener addresses // that are unused and serve the remaining redirects from it - for _, srv := range app.Servers { + + // Use the sorted srvNames to consistently find the target server + for _, srvName := range srvNames { + srv := app.Servers[srvName] // only look at servers which listen on an address which // we want to add redirects to if !srv.hasListenerAddress(redirServerAddr) { diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 1f1a14b2d..7d2f41a12 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -556,15 +556,21 @@ func (s *Server) hasListenerAddress(fullAddr string) bool { // The second issue seems very similar to a discussion here: // https://github.com/nodejs/node/issues/9390 // - // This is very easy to reproduce by creating an HTTP server - // that listens to both addresses or just one with a host - // interface; or for a more confusing reproduction, try - // listening on "127.0.0.1:80" and ":443" and you'll see - // the error, if you take away the GOOS condition below. - // - // So, an address is equivalent if the port is in the port - // range, and if not on Linux, the host is the same... sigh. - if (runtime.GOOS == "linux" || thisAddrs.Host == laddrs.Host) && + // However, binding to *different specific* interfaces + // (e.g. 127.0.0.2:80 and 127.0.0.3:80) IS allowed on Linux. + // The conflict only happens when mixing specific IPs with + // wildcards (0.0.0.0 or ::). + + // Hosts match exactly (e.g. 127.0.0.2 == 127.0.0.2) -> Conflict. + hostMatch := thisAddrs.Host == laddrs.Host + + // On Linux, specific IP vs Wildcard fails to bind. + // So if we are on Linux AND either host is empty (wildcard), we treat + // it as a match (conflict). But if both are specific and different + // (127.0.0.2 vs 127.0.0.3), this remains false (no conflict). + linuxWildcardConflict := runtime.GOOS == "linux" && (thisAddrs.Host == "" || laddrs.Host == "") + + if (hostMatch || linuxWildcardConflict) && (laddrs.StartPort <= thisAddrs.EndPort) && (laddrs.StartPort >= thisAddrs.StartPort) { return true