Skip to content

Commit 00e68aa

Browse files
Siomachkinfrancislavoie
authored andcommitted
Add support for indexed named sockets to handle duplicate names
1 parent 18a48df commit 00e68aa

File tree

2 files changed

+139
-9
lines changed

2 files changed

+139
-9
lines changed

listeners.go

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ import (
3838
"github.com/caddyserver/caddy/v2/internal"
3939
)
4040

41+
// listenFdsStart is the first file descriptor number for systemd socket activation.
42+
// File descriptors 0, 1, 2 are reserved for stdin, stdout, stderr.
43+
const listenFdsStart = 3
44+
4145
// NetworkAddress represents one or more network addresses.
4246
// It contains the individual components for a parsed network
4347
// address of the form accepted by ParseNetworkAddress().
@@ -308,8 +312,12 @@ func IsFdNetwork(netw string) bool {
308312
// getFdByName returns the file descriptor number for the given
309313
// socket name from systemd's LISTEN_FDNAMES environment variable.
310314
// Socket names are provided by systemd via socket activation.
311-
func getFdByName(name string) (int, error) {
312-
if name == "" {
315+
//
316+
// The name can optionally include an index to handle multiple sockets
317+
// with the same name: "web:0" for first, "web:1" for second, etc.
318+
// If no index is specified, defaults to index 0 (first occurrence).
319+
func getFdByName(nameWithIndex string) (int, error) {
320+
if nameWithIndex == "" {
313321
return 0, fmt.Errorf("socket name cannot be empty")
314322
}
315323

@@ -318,18 +326,45 @@ func getFdByName(name string) (int, error) {
318326
return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set")
319327
}
320328

329+
// Parse name and optional index
330+
parts := strings.Split(nameWithIndex, ":")
331+
if len(parts) > 2 {
332+
return 0, fmt.Errorf("invalid socket name format '%s': too many colons", nameWithIndex)
333+
}
334+
335+
name := parts[0]
336+
targetIndex := 0
337+
338+
if len(parts) > 1 {
339+
var err error
340+
targetIndex, err = strconv.Atoi(parts[1])
341+
if err != nil {
342+
return 0, fmt.Errorf("invalid socket index '%s': %v", parts[1], err)
343+
}
344+
if targetIndex < 0 {
345+
return 0, fmt.Errorf("socket index cannot be negative: %d", targetIndex)
346+
}
347+
}
348+
321349
// Parse the socket names
322350
names := strings.Split(fdNamesStr, ":")
323351

324-
// Find the index of the requested name
352+
// Find the Nth occurrence of the requested name
353+
matchCount := 0
325354
for i, fdName := range names {
326355
if fdName == name {
327-
// File descriptors start at 3 (after stdin=0, stdout=1, stderr=2)
328-
return 3 + i, nil
356+
if matchCount == targetIndex {
357+
return listenFdsStart + i, nil
358+
}
359+
matchCount++
329360
}
330361
}
331362

332-
return 0, fmt.Errorf("socket name '%s' not found in LISTEN_FDNAMES", name)
363+
if matchCount == 0 {
364+
return 0, fmt.Errorf("socket name '%s' not found in LISTEN_FDNAMES", name)
365+
}
366+
367+
return 0, fmt.Errorf("socket name '%s' found %d times, but index %d requested", name, matchCount, targetIndex)
333368
}
334369

335370
// ParseNetworkAddress parses addr into its individual

listeners_test.go

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -682,23 +682,65 @@ func TestGetFdByName(t *testing.T) {
682682
expectedFd: 3,
683683
},
684684
{
685-
name: "multiple sockets - first",
685+
name: "multiple different sockets - first",
686686
fdNames: "http:https:dns",
687687
socketName: "http",
688688
expectedFd: 3,
689689
},
690690
{
691-
name: "multiple sockets - second",
691+
name: "multiple different sockets - second",
692692
fdNames: "http:https:dns",
693693
socketName: "https",
694694
expectedFd: 4,
695695
},
696696
{
697-
name: "multiple sockets - third",
697+
name: "multiple different sockets - third",
698698
fdNames: "http:https:dns",
699699
socketName: "dns",
700700
expectedFd: 5,
701701
},
702+
{
703+
name: "duplicate names - first occurrence (no index)",
704+
fdNames: "web:web:api",
705+
socketName: "web",
706+
expectedFd: 3,
707+
},
708+
{
709+
name: "duplicate names - first occurrence (explicit index 0)",
710+
fdNames: "web:web:api",
711+
socketName: "web:0",
712+
expectedFd: 3,
713+
},
714+
{
715+
name: "duplicate names - second occurrence (index 1)",
716+
fdNames: "web:web:api",
717+
socketName: "web:1",
718+
expectedFd: 4,
719+
},
720+
{
721+
name: "complex duplicates - first api",
722+
fdNames: "web:api:web:api:dns",
723+
socketName: "api:0",
724+
expectedFd: 4,
725+
},
726+
{
727+
name: "complex duplicates - second api",
728+
fdNames: "web:api:web:api:dns",
729+
socketName: "api:1",
730+
expectedFd: 6,
731+
},
732+
{
733+
name: "complex duplicates - first web",
734+
fdNames: "web:api:web:api:dns",
735+
socketName: "web:0",
736+
expectedFd: 3,
737+
},
738+
{
739+
name: "complex duplicates - second web",
740+
fdNames: "web:api:web:api:dns",
741+
socketName: "web:1",
742+
expectedFd: 5,
743+
},
702744
{
703745
name: "socket not found",
704746
fdNames: "http:https",
@@ -717,6 +759,30 @@ func TestGetFdByName(t *testing.T) {
717759
socketName: "http",
718760
expectError: true,
719761
},
762+
{
763+
name: "index out of range",
764+
fdNames: "web:web",
765+
socketName: "web:2",
766+
expectError: true,
767+
},
768+
{
769+
name: "negative index",
770+
fdNames: "web",
771+
socketName: "web:-1",
772+
expectError: true,
773+
},
774+
{
775+
name: "invalid index format",
776+
fdNames: "web",
777+
socketName: "web:abc",
778+
expectError: true,
779+
},
780+
{
781+
name: "too many colons",
782+
fdNames: "web",
783+
socketName: "web:0:extra",
784+
expectError: true,
785+
},
720786
}
721787

722788
for _, tc := range tests {
@@ -788,6 +854,20 @@ func TestParseNetworkAddressFdName(t *testing.T) {
788854
Host: "5",
789855
},
790856
},
857+
{
858+
input: "fdname/http:0",
859+
expectAddr: NetworkAddress{
860+
Network: "fd",
861+
Host: "3",
862+
},
863+
},
864+
{
865+
input: "fdname/https:0",
866+
expectAddr: NetworkAddress{
867+
Network: "fd",
868+
Host: "4",
869+
},
870+
},
791871
{
792872
input: "fdgramname/http",
793873
expectAddr: NetworkAddress{
@@ -802,6 +882,13 @@ func TestParseNetworkAddressFdName(t *testing.T) {
802882
Host: "4",
803883
},
804884
},
885+
{
886+
input: "fdgramname/http:0",
887+
expectAddr: NetworkAddress{
888+
Network: "fdgram",
889+
Host: "3",
890+
},
891+
},
805892
{
806893
input: "fdname/nonexistent",
807894
expectErr: true,
@@ -810,6 +897,14 @@ func TestParseNetworkAddressFdName(t *testing.T) {
810897
input: "fdgramname/nonexistent",
811898
expectErr: true,
812899
},
900+
{
901+
input: "fdname/http:99",
902+
expectErr: true,
903+
},
904+
{
905+
input: "fdname/invalid:abc",
906+
expectErr: true,
907+
},
813908
// Test that old fd/N syntax still works
814909
{
815910
input: "fd/7",

0 commit comments

Comments
 (0)