Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 19:45 UTC
listeners: Add support for named socket activation (#7243)
Pavel committed
Oct 21, 2025 at 06:55 UTC
156ce99d3a46be8cefe8502b2c30b757e4deb79f
2 files changed
+365
-1
listeners.go
+81
-1
@@ -38,6 +38,10 @@ import (
38
"github.com/caddyserver/caddy/v2/internal"
39
)
40
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
+
45
// NetworkAddress represents one or more network addresses.
46
// It contains the individual components for a parsed network
47
// address of the form accepted by ParseNetworkAddress().
@@ -305,6 +309,64 @@ func IsFdNetwork(netw string) bool {
309
return strings.HasPrefix(netw, "fd")
310
}
311
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
+
370
// ParseNetworkAddress parses addr into its individual
371
// components. The input string is expected to be of
372
// the form "network/host:port-range" where any part is
@@ -336,9 +398,27 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
398
}, err
399
}
400
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
+
419
return NetworkAddress{
420
Network: network,
341
- Host: host,
421
+ Host: fdAddr,
422
}, nil
423
}
424
var start, end uint64
listeners_test.go
+284
@@ -15,6 +15,7 @@
15
package caddy
16
17
import (
18
+ "os"
19
"reflect"
20
"testing"
21
@@ -652,3 +653,286 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) {
653
}
654
}
655
}
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
+}