From 338d9ad275a63cd17f4563154a7301d67c9812fa Mon Sep 17 00:00:00 2001 From: ksankeerth Date: Thu, 10 Jul 2025 19:43:31 +0530 Subject: [PATCH] caddyhttp: prevent use of admin ports by listeners --- listeners.go | 44 ++++++++++++- listeners_test.go | 138 +++++++++++++++++++++++++++++++++++++++ modules/caddyhttp/app.go | 2 +- 3 files changed, 181 insertions(+), 3 deletions(-) diff --git a/listeners.go b/listeners.go index c0d018bb3d7..f34b2128efc 100644 --- a/listeners.go +++ b/listeners.go @@ -24,6 +24,7 @@ import ( "net" "net/netip" "os" + "slices" "strconv" "strings" "sync" @@ -204,6 +205,41 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net return ln, nil } +// CanBindAddress checks whether a new listener can safely be created on the +// given network, host, and port. This currently only applies to admin ports, +// and returns true if the address is not already in use; false otherwise +func CanBindAddress(netw, host string, port uint) bool { + portStr := strconv.FormatUint(uint64(port), 10) + + // check the ports are reserved for admin + if slices.Contains([]string{"tcp", "tcp4", "tcp6"}, netw) { + exitingAddrs := []string{} + if localAdminServer != nil { + exitingAddrs = append(exitingAddrs, localAdminServer.Addr) + } + if remoteAdminServer != nil { + exitingAddrs = append(exitingAddrs, remoteAdminServer.Addr) + } + // Check for exact match + if slices.Contains(exitingAddrs, net.JoinHostPort(host, portStr)) { + return false + } + + // Check for wildcard interface conflict on same port + for _, addr := range exitingAddrs { + addrHost, addrPort, err := net.SplitHostPort(addr) + if err != nil { + continue + } + if addrPort == portStr && (isWildcardInterface(host) || isWildcardInterface(addrHost)) { + return false + } + } + } + + return true +} + // IsUnixNetwork returns true if na.Network is // unix, unixgram, or unixpacket. func (na NetworkAddress) IsUnixNetwork() bool { @@ -269,10 +305,14 @@ func (na NetworkAddress) isLoopback() bool { } func (na NetworkAddress) isWildcardInterface() bool { - if na.Host == "" { + return isWildcardInterface(na.Host) +} + +func isWildcardInterface(host string) bool { + if host == "" { return true } - if ip, err := netip.ParseAddr(na.Host); err == nil { + if ip, err := netip.ParseAddr(host); err == nil { return ip.IsUnspecified() } return false diff --git a/listeners_test.go b/listeners_test.go index a4cadd3aab1..585980e9537 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -15,6 +15,8 @@ package caddy import ( + "net" + "net/http" "reflect" "testing" @@ -652,3 +654,139 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) { } } } + +func TestCanBindAddress(t *testing.T) { + originalLocalAdminServer := localAdminServer + originalRemoteAdminServer := remoteAdminServer + + defer func() { + localAdminServer = originalLocalAdminServer + remoteAdminServer = originalRemoteAdminServer + }() + + testCases := []struct { + name string + canBind bool + netw string + host string + port uint + lclAdminSvr *http.Server + remAdminSvr *http.Server + }{ + { + name: "binds on tcp :2019 when local admin server is not set", + canBind: true, + netw: "tcp", + host: "", + port: 2019, + }, + { + name: "binds on tcp :2021 when remote admin server is not set", + canBind: true, + netw: "tcp", + host: "", + port: 2021, + }, + { + name: "binds on tcp :2019 when only remote admin server is enabled on different port", + remAdminSvr: &http.Server{ + Addr: net.JoinHostPort("localhost", "2021"), + }, + canBind: true, + netw: "tcp", + host: "", + port: 2019, + }, + { + name: "fails to bind on tcp :2019 when local admin server is already on localhost:2019", + remAdminSvr: &http.Server{ + Addr: net.JoinHostPort("localhost", "2021"), + }, + lclAdminSvr: &http.Server{ + Addr: net.JoinHostPort("localhost", "2019"), + }, + canBind: false, + netw: "tcp", + host: "", + port: 2019, + }, + { + name: "binds on tcp :2019 when local admin is on different port", + lclAdminSvr: &http.Server{ + Addr: net.JoinHostPort("localhost", "2029"), + }, + canBind: true, + netw: "tcp", + host: "", + port: 2019, + }, + { + name: "binds on tcp localhost:2021 when remote admin is on different interface", + remAdminSvr: &http.Server{ + Addr: net.JoinHostPort("192.168.2.3", "2021"), + }, + lclAdminSvr: &http.Server{ + Addr: net.JoinHostPort("localhost", "2019"), + }, + canBind: true, + netw: "tcp", + host: "localhost", + port: 2021, + }, + { + name: "fails to bind on tcp :2021 when remote admin is bound to all interfaces", + remAdminSvr: &http.Server{ + Addr: net.JoinHostPort("", "2021"), + }, + lclAdminSvr: &http.Server{ + Addr: net.JoinHostPort("localhost", "2019"), + }, + canBind: false, + netw: "tcp", + host: "", + port: 2021, + }, + { + name: "fails to bind on tcp6 [::]:2019 when remote admin uses same address and port", + remAdminSvr: &http.Server{ + Addr: net.JoinHostPort("::", "2019"), + }, + canBind: false, + netw: "tcp6", + host: "::", + port: 2019, + }, + { + name: "binds on tcp6 [::1]:2019 when remote admin is on [::]:2021", + remAdminSvr: &http.Server{ + Addr: net.JoinHostPort("::", "2021"), + }, + canBind: true, + netw: "tcp6", + host: "::1", + port: 2019, + }, + { + name: "binds on tcp4 127.0.0.1:2021 when admin is on different port/interface", + lclAdminSvr: &http.Server{ + Addr: net.JoinHostPort("0.0.0.0", "2019"), + }, + canBind: true, + netw: "tcp4", + host: "127.0.0.1", + port: 2021, + }, + } + + for i, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + localAdminServer = tc.lclAdminSvr + remoteAdminServer = tc.remAdminSvr + + canBind := CanBindAddress(tc.netw, tc.host, tc.port) + if canBind != tc.canBind { + t.Errorf("Test %d: Expected %v but got: %v", i, tc.canBind, canBind) + } + }) + } +} diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index b550904e2ff..8e6e7c7ec6f 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -424,7 +424,7 @@ func (app *App) Validate() error { // we do not use <= here because PortRangeSize() adds 1 to EndPort for us for i := uint(0); i < listenAddr.PortRangeSize(); i++ { addr := caddy.JoinNetworkAddress(listenAddr.Network, listenAddr.Host, strconv.FormatUint(uint64(listenAddr.StartPort+i), 10)) - if sn, ok := lnAddrs[addr]; ok { + if sn, ok := lnAddrs[addr]; ok || !caddy.CanBindAddress(listenAddr.Network, listenAddr.Host, listenAddr.StartPort+i) { return fmt.Errorf("server %s: listener address repeated: %s (already claimed by server '%s')", srvName, addr, sn) } lnAddrs[addr] = srvName