Add support for indexed named sockets to handle duplicate names

This commit is contained in:
Pavel Siomachkin 2025-09-06 22:32:26 +02:00
parent 35658131f9
commit a819b7309b
2 changed files with 143 additions and 13 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().
@ -308,8 +312,12 @@ func IsFdNetwork(netw string) bool {
// 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.
func getFdByName(name string) (int, error) {
if name == "" {
//
// 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")
}
@ -318,18 +326,45 @@ func getFdByName(name string) (int, error) {
return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set")
}
// Parse the socket names
names := strings.Split(fdNamesStr, ":")
// 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)
}
// Find the index of the requested name
for i, fdName := range names {
if fdName == name {
// File descriptors start at 3 (after stdin=0, stdout=1, stderr=2)
return 3 + i, nil
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)
}
}
return 0, fmt.Errorf("socket name '%s' not found in LISTEN_FDNAMES", name)
// 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

View file

@ -682,23 +682,65 @@ func TestGetFdByName(t *testing.T) {
expectedFd: 3,
},
{
name: "multiple sockets - first",
name: "multiple different sockets - first",
fdNames: "http:https:dns",
socketName: "http",
expectedFd: 3,
},
{
name: "multiple sockets - second",
name: "multiple different sockets - second",
fdNames: "http:https:dns",
socketName: "https",
expectedFd: 4,
},
{
name: "multiple sockets - third",
name: "multiple different sockets - third",
fdNames: "http:https:dns",
socketName: "dns",
expectedFd: 5,
},
{
name: "duplicate names - first occurrence (no index)",
fdNames: "web:web:api",
socketName: "web",
expectedFd: 3,
},
{
name: "duplicate names - first occurrence (explicit index 0)",
fdNames: "web:web:api",
socketName: "web:0",
expectedFd: 3,
},
{
name: "duplicate names - second occurrence (index 1)",
fdNames: "web:web:api",
socketName: "web:1",
expectedFd: 4,
},
{
name: "complex duplicates - first api",
fdNames: "web:api:web:api:dns",
socketName: "api:0",
expectedFd: 4,
},
{
name: "complex duplicates - second api",
fdNames: "web:api:web:api:dns",
socketName: "api:1",
expectedFd: 6,
},
{
name: "complex duplicates - first web",
fdNames: "web:api:web:api:dns",
socketName: "web:0",
expectedFd: 3,
},
{
name: "complex duplicates - second web",
fdNames: "web:api:web:api:dns",
socketName: "web:1",
expectedFd: 5,
},
{
name: "socket not found",
fdNames: "http:https",
@ -717,6 +759,30 @@ func TestGetFdByName(t *testing.T) {
socketName: "http",
expectError: true,
},
{
name: "index out of range",
fdNames: "web:web",
socketName: "web:2",
expectError: true,
},
{
name: "negative index",
fdNames: "web",
socketName: "web:-1",
expectError: true,
},
{
name: "invalid index format",
fdNames: "web",
socketName: "web:abc",
expectError: true,
},
{
name: "too many colons",
fdNames: "web",
socketName: "web:0:extra",
expectError: true,
},
}
for _, tc := range tests {
@ -788,6 +854,20 @@ func TestParseNetworkAddressFdName(t *testing.T) {
Host: "5",
},
},
{
input: "fdname/http:0",
expectAddr: NetworkAddress{
Network: "fd",
Host: "3",
},
},
{
input: "fdname/https:0",
expectAddr: NetworkAddress{
Network: "fd",
Host: "4",
},
},
{
input: "fdgramname/http",
expectAddr: NetworkAddress{
@ -802,6 +882,13 @@ func TestParseNetworkAddressFdName(t *testing.T) {
Host: "4",
},
},
{
input: "fdgramname/http:0",
expectAddr: NetworkAddress{
Network: "fdgram",
Host: "3",
},
},
{
input: "fdname/nonexistent",
expectErr: true,
@ -810,6 +897,14 @@ func TestParseNetworkAddressFdName(t *testing.T) {
input: "fdgramname/nonexistent",
expectErr: true,
},
{
input: "fdname/http:99",
expectErr: true,
},
{
input: "fdname/invalid:abc",
expectErr: true,
},
// Test that old fd/N syntax still works
{
input: "fd/7",