mirror of
https://github.com/caddyserver/caddy.git
synced 2026-11-11 19:37:55 +00:00
core: Retry privileged binds on EACCES instead of checking port < 1024 (#8253)
WithBindCapability assumed the kernel's privileged port range ends at 1024, but it is set by net.ipv4.ip_unprivileged_port_start, which can be anywhere from 0 (Docker's default) to 65535. With a value above 1024, a non-root bind to e.g. :1500 failed even though the capability was permitted. Now listen is called normally first, and only if it fails with EACCES on a TCP/UDP port, and CAP_NET_BIND_SERVICE is permitted but not effective, the capability is raised and the bind retried. This also skips raising it when the range starts lower than the port. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e91867d35c
commit
0e392c89ba
2 files changed
+59
-25
No files matched your search
+21
-13
@@ -17,6 +17,7 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
@@ -27,9 +28,15 @@ import (
|
||||
)
|
||||
|
||||
// WithBindCapability calls listen, which should bind to address on
|
||||
// network, raising CAP_NET_BIND_SERVICE for the duration of the call
|
||||
// if address is a privileged TCP or UDP port and the capability is in
|
||||
// the permitted set but not the effective set.
|
||||
// network. If that fails with EACCES for a TCP or UDP port, and
|
||||
// CAP_NET_BIND_SERVICE is in the permitted set but not the effective
|
||||
// set, it raises the capability and calls listen again, lowering the
|
||||
// capability afterwards.
|
||||
//
|
||||
// Retrying on EACCES, instead of checking the port up front, means the
|
||||
// capability is only raised when the kernel actually requires it, which
|
||||
// depends on net.ipv4.ip_unprivileged_port_start (often 0 in containers,
|
||||
// and possibly above 1024).
|
||||
//
|
||||
// This allows the caddy binary to be granted the capability with
|
||||
// `setcap cap_net_bind_service=+p` instead of `=+ep`. With the effective
|
||||
@@ -39,11 +46,12 @@ import (
|
||||
// even if Caddy never binds a privileged port. See "Safety checking for
|
||||
// capability-dumb binaries" in capabilities(7).
|
||||
func WithBindCapability(network, address string, listen func() (any, error)) (any, error) {
|
||||
if !isPrivilegedPort(network, address) {
|
||||
return listen()
|
||||
ln, err := listen()
|
||||
if !errors.Is(err, unix.EACCES) || !isPortBind(network, address) {
|
||||
return ln, err
|
||||
}
|
||||
if raise, err := shouldRaiseBindCapability(); err != nil || !raise {
|
||||
return listen()
|
||||
if raise, capErr := shouldRaiseBindCapability(); capErr != nil || !raise {
|
||||
return ln, err
|
||||
}
|
||||
|
||||
// capabilities are per-thread, so raise it on a locked OS thread
|
||||
@@ -58,8 +66,8 @@ func WithBindCapability(network, address string, listen func() (any, error)) (an
|
||||
ch := make(chan result, 1)
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
if err := setBindCapability(true); err != nil {
|
||||
ch <- result{err: fmt.Errorf("raising CAP_NET_BIND_SERVICE: %v", err)}
|
||||
if capErr := setBindCapability(true); capErr != nil {
|
||||
ch <- result{err: fmt.Errorf("%w (raising CAP_NET_BIND_SERVICE: %v)", err, capErr)}
|
||||
return
|
||||
}
|
||||
ln, err := listen()
|
||||
@@ -70,9 +78,9 @@ func WithBindCapability(network, address string, listen func() (any, error)) (an
|
||||
return r.ln, r.err
|
||||
}
|
||||
|
||||
// isPrivilegedPort reports whether address is a TCP or UDP
|
||||
// address with a port below 1024 (other than 0).
|
||||
func isPrivilegedPort(network, address string) bool {
|
||||
// isPortBind reports whether address is a TCP or UDP
|
||||
// address with a nonzero port.
|
||||
func isPortBind(network, address string) bool {
|
||||
if !strings.HasPrefix(network, "tcp") && !strings.HasPrefix(network, "udp") {
|
||||
return false
|
||||
}
|
||||
@@ -81,7 +89,7 @@ func isPrivilegedPort(network, address string) bool {
|
||||
return false
|
||||
}
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
return err == nil && port > 0 && port < 1024
|
||||
return err == nil && port > 0
|
||||
}
|
||||
|
||||
const bindCapabilityMask = 1 << unix.CAP_NET_BIND_SERVICE
|
||||
|
||||
@@ -16,10 +16,14 @@ package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestIsPrivilegedPort(t *testing.T) {
|
||||
func TestIsPortBind(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
network, address string
|
||||
want bool
|
||||
@@ -28,15 +32,17 @@ func TestIsPrivilegedPort(t *testing.T) {
|
||||
{"tcp4", "127.0.0.1:443", true},
|
||||
{"udp6", "[::1]:443", true},
|
||||
{"tcp", ":1023", true},
|
||||
{"tcp", ":1024", false},
|
||||
{"tcp", ":8080", false},
|
||||
{"tcp", ":1024", true},
|
||||
{"tcp", ":8080", true},
|
||||
{"tcp", ":65535", true},
|
||||
{"tcp", ":65536", false},
|
||||
{"tcp", ":0", false},
|
||||
{"unix", "/run/caddy.sock", false},
|
||||
{"fd", "3", false},
|
||||
{"ip4:icmp", "127.0.0.1", false},
|
||||
} {
|
||||
if got := isPrivilegedPort(tc.network, tc.address); got != tc.want {
|
||||
t.Errorf("isPrivilegedPort(%q, %q) = %v, want %v", tc.network, tc.address, got, tc.want)
|
||||
if got := isPortBind(tc.network, tc.address); got != tc.want {
|
||||
t.Errorf("isPortBind(%q, %q) = %v, want %v", tc.network, tc.address, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,17 +53,37 @@ func TestWithBindCapabilityPassthrough(t *testing.T) {
|
||||
t.Fatalf("reading capabilities: %v", err)
|
||||
}
|
||||
|
||||
// a privileged-port EACCES is retried only if the capability can be raised
|
||||
retryCalls := 1
|
||||
if before {
|
||||
retryCalls = 2
|
||||
}
|
||||
|
||||
// shaped like the error net.ListenConfig.Listen returns
|
||||
eacces := &net.OpError{Op: "listen", Net: "tcp", Err: os.NewSyscallError("bind", unix.EACCES)}
|
||||
sentinel := errors.New("sentinel")
|
||||
for _, tc := range []struct{ network, address string }{
|
||||
{"tcp", "localhost:80"},
|
||||
{"udp", ":8080"},
|
||||
{"unix", "/tmp/caddy.sock"},
|
||||
for _, tc := range []struct {
|
||||
network, address string
|
||||
err error
|
||||
wantCalls int
|
||||
}{
|
||||
{"tcp", "localhost:80", nil, 1},
|
||||
{"tcp", "localhost:80", sentinel, 1},
|
||||
{"udp", ":8080", sentinel, 1},
|
||||
{"unix", "/tmp/caddy.sock", eacces, 1},
|
||||
{"tcp", ":0", eacces, 1},
|
||||
{"tcp", "localhost:80", eacces, retryCalls},
|
||||
} {
|
||||
calls := 0
|
||||
ln, err := WithBindCapability(tc.network, tc.address, func() (any, error) {
|
||||
return "listener", sentinel
|
||||
calls++
|
||||
return "listener", tc.err
|
||||
})
|
||||
if ln != "listener" || !errors.Is(err, sentinel) {
|
||||
t.Errorf("%s/%s: got (%v, %v), want listen's return values", tc.network, tc.address, ln, err)
|
||||
if calls != tc.wantCalls {
|
||||
t.Errorf("%s/%s/%v: listen called %d times, want %d", tc.network, tc.address, tc.err, calls, tc.wantCalls)
|
||||
}
|
||||
if ln != "listener" || !errors.Is(err, tc.err) {
|
||||
t.Errorf("%s/%s/%v: got (%v, %v), want listen's return values", tc.network, tc.address, tc.err, ln, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user