listeners: Add support for named socket activation (#7243)

This commit is contained in:
Pavel 2025-10-21 06:55:42 +02:00 committed by GitHub
parent a7885aabec
commit 156ce99d3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 365 additions and 1 deletions

View file

@ -38,6 +38,10 @@ import (
"github.com/caddyserver/caddy/v2/internal"
)
// listenFdsStart is the first file descriptor number for systemd socket activation.
// File descriptors 0, 1, 2 are reserved for stdin, stdout, stderr.
const listenFdsStart = 3
// NetworkAddress represents one or more network addresses.
// It contains the individual components for a parsed network
// address of the form accepted by ParseNetworkAddress().
@ -305,6 +309,64 @@ func IsFdNetwork(netw string) bool {
return strings.HasPrefix(netw, "fd")
}
// getFdByName returns the file descriptor number for the given
// socket name from systemd's LISTEN_FDNAMES environment variable.
// Socket names are provided by systemd via socket activation.
//
// The name can optionally include an index to handle multiple sockets
// with the same name: "web:0" for first, "web:1" for second, etc.
// If no index is specified, defaults to index 0 (first occurrence).
func getFdByName(nameWithIndex string) (int, error) {
if nameWithIndex == "" {
return 0, fmt.Errorf("socket name cannot be empty")
}
fdNamesStr := os.Getenv("LISTEN_FDNAMES")
if fdNamesStr == "" {
return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set")
}
// Parse name and optional index
parts := strings.Split(nameWithIndex, ":")
if len(parts) > 2 {
return 0, fmt.Errorf("invalid socket name format '%s': too many colons", nameWithIndex)
}
name := parts[0]
targetIndex := 0
if len(parts) > 1 {
var err error
targetIndex, err = strconv.Atoi(parts[1])
if err != nil {
return 0, fmt.Errorf("invalid socket index '%s': %v", parts[1], err)
}
if targetIndex < 0 {
return 0, fmt.Errorf("socket index cannot be negative: %d", targetIndex)
}
}
// Parse the socket names
names := strings.Split(fdNamesStr, ":")
// Find the Nth occurrence of the requested name
matchCount := 0
for i, fdName := range names {
if fdName == name {
if matchCount == targetIndex {
return listenFdsStart + i, nil
}
matchCount++
}
}
if matchCount == 0 {
return 0, fmt.Errorf("socket name '%s' not found in LISTEN_FDNAMES", name)
}
return 0, fmt.Errorf("socket name '%s' found %d times, but index %d requested", name, matchCount, targetIndex)
}
// ParseNetworkAddress parses addr into its individual
// components. The input string is expected to be of
// the form "network/host:port-range" where any part is
@ -336,9 +398,27 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
}, err
}
if IsFdNetwork(network) {
fdAddr := host
// Handle named socket activation (fdname/name, fdgramname/name)
if strings.HasPrefix(network, "fdname") || strings.HasPrefix(network, "fdgramname") {
fdNum, err := getFdByName(host)
if err != nil {
return NetworkAddress{}, fmt.Errorf("named socket activation: %v", err)
}
fdAddr = strconv.Itoa(fdNum)
// Normalize network to standard fd/fdgram
if strings.HasPrefix(network, "fdname") {
network = "fd"
} else {
network = "fdgram"
}
}
return NetworkAddress{
Network: network,
Host: host,
Host: fdAddr,
}, nil
}
var start, end uint64