Files
caddy/internal/bindcap_linux.go
Francis LavoieandClaude Opus 5.5 0e392c89ba 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>
2026-10-19 19:19:28 +10:00

130 lines
4.1 KiB
Go

// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
package internal
import (
"errors"
"fmt"
"net"
"runtime"
"strconv"
"strings"
"golang.org/x/sys/unix"
)
// WithBindCapability calls listen, which should bind to address on
// 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
// bit set, the kernel refuses to execute the binary at all when the
// capability can't be granted (for example, in containers run with
// `--cap-drop ALL`, or systemd units with an empty CapabilityBoundingSet),
// 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) {
ln, err := listen()
if !errors.Is(err, unix.EACCES) || !isPortBind(network, address) {
return ln, err
}
if raise, capErr := shouldRaiseBindCapability(); capErr != nil || !raise {
return ln, err
}
// capabilities are per-thread, so raise it on a locked OS thread
// and bind from there; the goroutine exits without unlocking, so
// the runtime terminates the thread instead of reusing it with
// the raised capability (or wedges it, if it is the main thread,
// which is why we also lower it again after binding)
type result struct {
ln any
err error
}
ch := make(chan result, 1)
go func() {
runtime.LockOSThread()
if capErr := setBindCapability(true); capErr != nil {
ch <- result{err: fmt.Errorf("%w (raising CAP_NET_BIND_SERVICE: %v)", err, capErr)}
return
}
ln, err := listen()
_ = setBindCapability(false)
ch <- result{ln, err}
}()
r := <-ch
return r.ln, r.err
}
// 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
}
_, portStr, err := net.SplitHostPort(address)
if err != nil {
return false
}
port, err := strconv.ParseUint(portStr, 10, 16)
return err == nil && port > 0
}
const bindCapabilityMask = 1 << unix.CAP_NET_BIND_SERVICE
// shouldRaiseBindCapability reports whether CAP_NET_BIND_SERVICE is
// permitted but not effective for the calling thread.
func shouldRaiseBindCapability() (bool, error) {
_, data, err := getCapabilities()
if err != nil {
return false, err
}
return data[0].Permitted&bindCapabilityMask != 0 &&
data[0].Effective&bindCapabilityMask == 0, nil
}
// setBindCapability adds or removes CAP_NET_BIND_SERVICE in the
// effective set of the calling thread. To add it, it must already
// be permitted.
func setBindCapability(effective bool) error {
hdr, data, err := getCapabilities()
if err != nil {
return err
}
if effective {
data[0].Effective |= bindCapabilityMask
} else {
data[0].Effective &^= bindCapabilityMask
}
return unix.Capset(&hdr, &data[0])
}
func getCapabilities() (unix.CapUserHeader, [2]unix.CapUserData, error) {
hdr := unix.CapUserHeader{Version: unix.LINUX_CAPABILITY_VERSION_3}
var data [2]unix.CapUserData
err := unix.Capget(&hdr, &data[0])
return hdr, data, err
}