@marcel91200 / caddy Mirror · read-only / commits / 156ce99d
Fork 0

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 +}