Skip to content

Commit 156ce99

Browse files
authored
listeners: Add support for named socket activation (#7243)
1 parent a7885aa commit 156ce99

File tree

2 files changed

+365
-1
lines changed

2 files changed

+365
-1
lines changed

listeners.go

Lines changed: 81 additions & 1 deletion
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().
@@ -305,6 +309,64 @@ func IsFdNetwork(netw string) bool {
305309
return strings.HasPrefix(netw, "fd")
306310
}
307311

312+
// getFdByName returns the file descriptor number for the given
313+
// socket name from systemd's LISTEN_FDNAMES environment variable.
314+
// Socket names are provided by systemd via socket activation.
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 == "" {
321+
return 0, fmt.Errorf("socket name cannot be empty")
322+
}
323+
324+
fdNamesStr := os.Getenv("LISTEN_FDNAMES")
325+
if fdNamesStr == "" {
326+
return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set")
327+
}
328+
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+
349+
// Parse the socket names
350+
names := strings.Split(fdNamesStr, ":")
351+
352+
// Find the Nth occurrence of the requested name
353+
matchCount := 0
354+
for i, fdName := range names {
355+
if fdName == name {
356+
if matchCount == targetIndex {
357+
return listenFdsStart + i, nil
358+
}
359+
matchCount++
360+
}
361+
}
362+
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)
368+
}
369+
308370
// ParseNetworkAddress parses addr into its individual
309371
// components. The input string is expected to be of
310372
// the form "network/host:port-range" where any part is
@@ -336,9 +398,27 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
336398
}, err
337399
}
338400
if IsFdNetwork(network) {
401+
fdAddr := host
402+
403+
// Handle named socket activation (fdname/name, fdgramname/name)
404+
if strings.HasPrefix(network, "fdname") || strings.HasPrefix(network, "fdgramname") {
405+
fdNum, err := getFdByName(host)
406+
if err != nil {
407+
return NetworkAddress{}, fmt.Errorf("named socket activation: %v", err)
408+
}
409+
fdAddr = strconv.Itoa(fdNum)
410+
411+
// Normalize network to standard fd/fdgram
412+
if strings.HasPrefix(network, "fdname") {
413+
network = "fd"
414+
} else {
415+
network = "fdgram"
416+
}
417+
}
418+
339419
return NetworkAddress{
340420
Network: network,
341-
Host: host,
421+
Host: fdAddr,
342422
}, nil
343423
}
344424
var start, end uint64

listeners_test.go

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package caddy
1616

1717
import (
18+
"os"
1819
"reflect"
1920
"testing"
2021

@@ -652,3 +653,286 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) {
652653
}
653654
}
654655
}
656+
657+
// TestGetFdByName tests the getFdByName function for systemd socket activation.
658+
func TestGetFdByName(t *testing.T) {
659+
// Save original environment
660+
originalFdNames := os.Getenv("LISTEN_FDNAMES")
661+
662+
// Restore environment after test
663+
defer func() {
664+
if originalFdNames != "" {
665+
os.Setenv("LISTEN_FDNAMES", originalFdNames)
666+
} else {
667+
os.Unsetenv("LISTEN_FDNAMES")
668+
}
669+
}()
670+
671+
tests := []struct {
672+
name string
673+
fdNames string
674+
socketName string
675+
expectedFd int
676+
expectError bool
677+
}{
678+
{
679+
name: "simple http socket",
680+
fdNames: "http",
681+
socketName: "http",
682+
expectedFd: 3,
683+
},
684+
{
685+
name: "multiple different sockets - first",
686+
fdNames: "http:https:dns",
687+
socketName: "http",
688+
expectedFd: 3,
689+
},
690+
{
691+
name: "multiple different sockets - second",
692+
fdNames: "http:https:dns",
693+
socketName: "https",
694+
expectedFd: 4,
695+
},
696+
{
697+
name: "multiple different sockets - third",
698+
fdNames: "http:https:dns",
699+
socketName: "dns",
700+
expectedFd: 5,
701+
},
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+
},
744+
{
745+
name: "socket not found",
746+
fdNames: "http:https",
747+
socketName: "missing",
748+
expectError: true,
749+
},
750+
{
751+
name: "empty socket name",
752+
fdNames: "http",
753+
socketName: "",
754+
expectError: true,
755+
},
756+
{
757+
name: "missing LISTEN_FDNAMES",
758+
fdNames: "",
759+
socketName: "http",
760+
expectError: true,
761+
},
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+
},
786+
}
787+
788+
for _, tc := range tests {
789+
t.Run(tc.name, func(t *testing.T) {
790+
// Set up environment
791+
if tc.fdNames != "" {
792+
os.Setenv("LISTEN_FDNAMES", tc.fdNames)
793+
} else {
794+
os.Unsetenv("LISTEN_FDNAMES")
795+
}
796+
797+
// Test the function
798+
fd, err := getFdByName(tc.socketName)
799+
800+
if tc.expectError {
801+
if err == nil {
802+
t.Errorf("Expected error but got none")
803+
}
804+
} else {
805+
if err != nil {
806+
t.Errorf("Expected no error but got: %v", err)
807+
}
808+
if fd != tc.expectedFd {
809+
t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd)
810+
}
811+
}
812+
})
813+
}
814+
}
815+
816+
// TestParseNetworkAddressFdName tests parsing of fdname and fdgramname addresses.
817+
func TestParseNetworkAddressFdName(t *testing.T) {
818+
// Save and restore environment
819+
originalFdNames := os.Getenv("LISTEN_FDNAMES")
820+
defer func() {
821+
if originalFdNames != "" {
822+
os.Setenv("LISTEN_FDNAMES", originalFdNames)
823+
} else {
824+
os.Unsetenv("LISTEN_FDNAMES")
825+
}
826+
}()
827+
828+
// Set up test environment
829+
os.Setenv("LISTEN_FDNAMES", "http:https:dns")
830+
831+
tests := []struct {
832+
input string
833+
expectAddr NetworkAddress
834+
expectErr bool
835+
}{
836+
{
837+
input: "fdname/http",
838+
expectAddr: NetworkAddress{
839+
Network: "fd",
840+
Host: "3",
841+
},
842+
},
843+
{
844+
input: "fdname/https",
845+
expectAddr: NetworkAddress{
846+
Network: "fd",
847+
Host: "4",
848+
},
849+
},
850+
{
851+
input: "fdname/dns",
852+
expectAddr: NetworkAddress{
853+
Network: "fd",
854+
Host: "5",
855+
},
856+
},
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+
},
871+
{
872+
input: "fdgramname/http",
873+
expectAddr: NetworkAddress{
874+
Network: "fdgram",
875+
Host: "3",
876+
},
877+
},
878+
{
879+
input: "fdgramname/https",
880+
expectAddr: NetworkAddress{
881+
Network: "fdgram",
882+
Host: "4",
883+
},
884+
},
885+
{
886+
input: "fdgramname/http:0",
887+
expectAddr: NetworkAddress{
888+
Network: "fdgram",
889+
Host: "3",
890+
},
891+
},
892+
{
893+
input: "fdname/nonexistent",
894+
expectErr: true,
895+
},
896+
{
897+
input: "fdgramname/nonexistent",
898+
expectErr: true,
899+
},
900+
{
901+
input: "fdname/http:99",
902+
expectErr: true,
903+
},
904+
{
905+
input: "fdname/invalid:abc",
906+
expectErr: true,
907+
},
908+
// Test that old fd/N syntax still works
909+
{
910+
input: "fd/7",
911+
expectAddr: NetworkAddress{
912+
Network: "fd",
913+
Host: "7",
914+
},
915+
},
916+
{
917+
input: "fdgram/8",
918+
expectAddr: NetworkAddress{
919+
Network: "fdgram",
920+
Host: "8",
921+
},
922+
},
923+
}
924+
925+
for i, tc := range tests {
926+
actualAddr, err := ParseNetworkAddress(tc.input)
927+
928+
if tc.expectErr && err == nil {
929+
t.Errorf("Test %d (%s): Expected error but got none", i, tc.input)
930+
}
931+
if !tc.expectErr && err != nil {
932+
t.Errorf("Test %d (%s): Expected no error but got: %v", i, tc.input, err)
933+
}
934+
if !tc.expectErr && !reflect.DeepEqual(tc.expectAddr, actualAddr) {
935+
t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectAddr, actualAddr)
936+
}
937+
}
938+
}

0 commit comments

Comments
 (0)