@marcel91200 / caddy Mirror · read-only / commits / 565c1c30
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 17:15 UTC

autohttps: deterministic logic and strict bind checking on Linux (#7435)

* http: fix non-deterministic auto-https and improve Linux bind matching * docs: restore historical context about Linux bind behavior

Paulo Henrique committed Jan 16, 2026 at 12:51 UTC 565c1c3054e1cd60b64275217b8e8c4da3b18856
2 files changed +38 -12
modules/caddyhttp/autohttps.go
+23 -3
@@ -90,7 +90,16 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
90 // the log configuration for an HTTPS enabled server
91 var logCfg *ServerLogConfig
92
93 - for srvName, srv := range app.Servers {
93 + // Sort server names to ensure deterministic iteration.
94 + // This prevents race conditions where the order of server processing
95 + // could affect which server gets assigned the HTTP->HTTPS redirect listener.
96 + srvNames := make([]string, 0, len(app.Servers))
97 + for name := range app.Servers {
98 + srvNames = append(srvNames, name)
99 + }
100 + slices.Sort(srvNames)
101 + for _, srvName := range srvNames {
102 + srv := app.Servers[srvName]
103 // as a prerequisite, provision route matchers; this is
104 // required for all routes on all servers, and must be
105 // done before we attempt to do phase 1 of auto HTTPS,
@@ -398,15 +407,26 @@ uniqueDomainsLoop:
407 return append(routes, app.makeRedirRoute(uint(app.httpsPort()), MatcherSet{MatchProtocol("http")}))
408 }
409
410 + // Sort redirect addresses to ensure deterministic process
411 + redirServerAddrsSorted := make([]string, 0, len(redirServers))
412 + for addr := range redirServers {
413 + redirServerAddrsSorted = append(redirServerAddrsSorted, addr)
414 + }
415 + slices.Sort(redirServerAddrsSorted)
416 +
417 redirServersLoop:
402 - for redirServerAddr, routes := range redirServers {
418 + for _, redirServerAddr := range redirServerAddrsSorted {
419 + routes := redirServers[redirServerAddr]
420 // for each redirect listener, see if there's already a
421 // server configured to listen on that exact address; if so,
422 // insert the redirect route to the end of its route list
423 // after any other routes with host matchers; otherwise,
424 // we'll create a new server for all the listener addresses
425 // that are unused and serve the remaining redirects from it
409 - for _, srv := range app.Servers {
426 +
427 + // Use the sorted srvNames to consistently find the target server
428 + for _, srvName := range srvNames {
429 + srv := app.Servers[srvName]
430 // only look at servers which listen on an address which
431 // we want to add redirects to
432 if !srv.hasListenerAddress(redirServerAddr) {
modules/caddyhttp/server.go
+15 -9
@@ -556,15 +556,21 @@ func (s *Server) hasListenerAddress(fullAddr string) bool {
556 // The second issue seems very similar to a discussion here:
557 // https://github.com/nodejs/node/issues/9390
558 //
559 - // This is very easy to reproduce by creating an HTTP server
560 - // that listens to both addresses or just one with a host
561 - // interface; or for a more confusing reproduction, try
562 - // listening on "127.0.0.1:80" and ":443" and you'll see
563 - // the error, if you take away the GOOS condition below.
564 - //
565 - // So, an address is equivalent if the port is in the port
566 - // range, and if not on Linux, the host is the same... sigh.
567 - if (runtime.GOOS == "linux" || thisAddrs.Host == laddrs.Host) &&
559 + // However, binding to *different specific* interfaces
560 + // (e.g. 127.0.0.2:80 and 127.0.0.3:80) IS allowed on Linux.
561 + // The conflict only happens when mixing specific IPs with
562 + // wildcards (0.0.0.0 or ::).
563 +
564 + // Hosts match exactly (e.g. 127.0.0.2 == 127.0.0.2) -> Conflict.
565 + hostMatch := thisAddrs.Host == laddrs.Host
566 +
567 + // On Linux, specific IP vs Wildcard fails to bind.
568 + // So if we are on Linux AND either host is empty (wildcard), we treat
569 + // it as a match (conflict). But if both are specific and different
570 + // (127.0.0.2 vs 127.0.0.3), this remains false (no conflict).
571 + linuxWildcardConflict := runtime.GOOS == "linux" && (thisAddrs.Host == "" || laddrs.Host == "")
572 +
573 + if (hostMatch || linuxWildcardConflict) &&
574 (laddrs.StartPort <= thisAddrs.EndPort) &&
575 (laddrs.StartPort >= thisAddrs.StartPort) {
576 return true