@marcel91200 / caddy Mirror · read-only / commits / 0e392c89
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, 02:15 UTC

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>

Francis Lavoie committed Oct 19, 2026 at 05:19 UTC 0e392c89baa50f558bc24440482c5d57fff4499f
2 files changed +59 -25
internal/bindcap_linux.go
+21 -13
@@ -17,6 +17,7 @@
17 package internal
18
19 import (
20 + "errors"
21 "fmt"
22 "net"
23 "runtime"
@@ -27,9 +28,15 @@ import (
28 )
29
30 // WithBindCapability calls listen, which should bind to address on
30 -// network, raising CAP_NET_BIND_SERVICE for the duration of the call
31 -// if address is a privileged TCP or UDP port and the capability is in
32 -// the permitted set but not the effective set.
31 +// network. If that fails with EACCES for a TCP or UDP port, and
32 +// CAP_NET_BIND_SERVICE is in the permitted set but not the effective
33 +// set, it raises the capability and calls listen again, lowering the
34 +// capability afterwards.
35 +//
36 +// Retrying on EACCES, instead of checking the port up front, means the
37 +// capability is only raised when the kernel actually requires it, which
38 +// depends on net.ipv4.ip_unprivileged_port_start (often 0 in containers,
39 +// and possibly above 1024).
40 //
41 // This allows the caddy binary to be granted the capability with
42 // `setcap cap_net_bind_service=+p` instead of `=+ep`. With the effective
@@ -39,11 +46,12 @@ import (
46 // even if Caddy never binds a privileged port. See "Safety checking for
47 // capability-dumb binaries" in capabilities(7).
48 func WithBindCapability(network, address string, listen func() (any, error)) (any, error) {
42 - if !isPrivilegedPort(network, address) {
43 - return listen()
49 + ln, err := listen()
50 + if !errors.Is(err, unix.EACCES) || !isPortBind(network, address) {
51 + return ln, err
52 }
45 - if raise, err := shouldRaiseBindCapability(); err != nil || !raise {
46 - return listen()
53 + if raise, capErr := shouldRaiseBindCapability(); capErr != nil || !raise {
54 + return ln, err
55 }
56
57 // 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
66 ch := make(chan result, 1)
67 go func() {
68 runtime.LockOSThread()
61 - if err := setBindCapability(true); err != nil {
62 - ch <- result{err: fmt.Errorf("raising CAP_NET_BIND_SERVICE: %v", err)}
69 + if capErr := setBindCapability(true); capErr != nil {
70 + ch <- result{err: fmt.Errorf("%w (raising CAP_NET_BIND_SERVICE: %v)", err, capErr)}
71 return
72 }
73 ln, err := listen()
@@ -70,9 +78,9 @@ func WithBindCapability(network, address string, listen func() (any, error)) (an
78 return r.ln, r.err
79 }
80
73 -// isPrivilegedPort reports whether address is a TCP or UDP
74 -// address with a port below 1024 (other than 0).
75 -func isPrivilegedPort(network, address string) bool {
81 +// isPortBind reports whether address is a TCP or UDP
82 +// address with a nonzero port.
83 +func isPortBind(network, address string) bool {
84 if !strings.HasPrefix(network, "tcp") && !strings.HasPrefix(network, "udp") {
85 return false
86 }
@@ -81,7 +89,7 @@ func isPrivilegedPort(network, address string) bool {
89 return false
90 }
91 port, err := strconv.ParseUint(portStr, 10, 16)
84 - return err == nil && port > 0 && port < 1024
92 + return err == nil && port > 0
93 }
94
95 const bindCapabilityMask = 1 << unix.CAP_NET_BIND_SERVICE
internal/bindcap_linux_test.go
+38 -12
@@ -16,10 +16,14 @@ package internal
16
17 import (
18 "errors"
19 + "net"
20 + "os"
21 "testing"
22 +
23 + "golang.org/x/sys/unix"
24 )
25
22 -func TestIsPrivilegedPort(t *testing.T) {
26 +func TestIsPortBind(t *testing.T) {
27 for _, tc := range []struct {
28 network, address string
29 want bool
@@ -28,15 +32,17 @@ func TestIsPrivilegedPort(t *testing.T) {
32 {"tcp4", "127.0.0.1:443", true},
33 {"udp6", "[::1]:443", true},
34 {"tcp", ":1023", true},
31 - {"tcp", ":1024", false},
32 - {"tcp", ":8080", false},
35 + {"tcp", ":1024", true},
36 + {"tcp", ":8080", true},
37 + {"tcp", ":65535", true},
38 + {"tcp", ":65536", false},
39 {"tcp", ":0", false},
40 {"unix", "/run/caddy.sock", false},
41 {"fd", "3", false},
42 {"ip4:icmp", "127.0.0.1", false},
43 } {
38 - if got := isPrivilegedPort(tc.network, tc.address); got != tc.want {
39 - t.Errorf("isPrivilegedPort(%q, %q) = %v, want %v", tc.network, tc.address, got, tc.want)
44 + if got := isPortBind(tc.network, tc.address); got != tc.want {
45 + t.Errorf("isPortBind(%q, %q) = %v, want %v", tc.network, tc.address, got, tc.want)
46 }
47 }
48 }
@@ -47,17 +53,37 @@ func TestWithBindCapabilityPassthrough(t *testing.T) {
53 t.Fatalf("reading capabilities: %v", err)
54 }
55
56 + // a privileged-port EACCES is retried only if the capability can be raised
57 + retryCalls := 1
58 + if before {
59 + retryCalls = 2
60 + }
61 +
62 + // shaped like the error net.ListenConfig.Listen returns
63 + eacces := &net.OpError{Op: "listen", Net: "tcp", Err: os.NewSyscallError("bind", unix.EACCES)}
64 sentinel := errors.New("sentinel")
51 - for _, tc := range []struct{ network, address string }{
52 - {"tcp", "localhost:80"},
53 - {"udp", ":8080"},
54 - {"unix", "/tmp/caddy.sock"},
65 + for _, tc := range []struct {
66 + network, address string
67 + err error
68 + wantCalls int
69 + }{
70 + {"tcp", "localhost:80", nil, 1},
71 + {"tcp", "localhost:80", sentinel, 1},
72 + {"udp", ":8080", sentinel, 1},
73 + {"unix", "/tmp/caddy.sock", eacces, 1},
74 + {"tcp", ":0", eacces, 1},
75 + {"tcp", "localhost:80", eacces, retryCalls},
76 } {
77 + calls := 0
78 ln, err := WithBindCapability(tc.network, tc.address, func() (any, error) {
57 - return "listener", sentinel
79 + calls++
80 + return "listener", tc.err
81 })
59 - if ln != "listener" || !errors.Is(err, sentinel) {
60 - t.Errorf("%s/%s: got (%v, %v), want listen's return values", tc.network, tc.address, ln, err)
82 + if calls != tc.wantCalls {
83 + t.Errorf("%s/%s/%v: listen called %d times, want %d", tc.network, tc.address, tc.err, calls, tc.wantCalls)
84 + }
85 + if ln != "listener" || !errors.Is(err, tc.err) {
86 + t.Errorf("%s/%s/%v: got (%v, %v), want listen's return values", tc.network, tc.address, tc.err, ln, err)
87 }
88 }
89