Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions listeners.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"net"
"net/netip"
"os"
"slices"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
138 changes: 138 additions & 0 deletions listeners_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package caddy

import (
"net"
"net/http"
"reflect"
"testing"

Expand Down Expand Up @@ -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)
}
})
}
}
2 changes: 1 addition & 1 deletion modules/caddyhttp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading