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