From 851494ea8e41f8c71e0760c0e197d8c8d2feedca Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 31 Oct 2025 08:21:56 -0600 Subject: [PATCH 01/10] networks.go with sd and sdgram --- listeners.go | 165 ++++-------------------------------- modules/caddyhttp/server.go | 31 ------- networks.go | 119 ++++++++++++++++++++++++++ networks_nosystemd.go | 29 +++++++ networks_systemd.go | 158 ++++++++++++++++++++++++++++++++++ 5 files changed, 324 insertions(+), 178 deletions(-) create mode 100644 networks.go create mode 100644 networks_nosystemd.go create mode 100644 networks_systemd.go diff --git a/listeners.go b/listeners.go index b673c86e109..de0c9402baf 100644 --- a/listeners.go +++ b/listeners.go @@ -38,10 +38,6 @@ 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(). @@ -137,31 +133,29 @@ func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) // Listen synchronizes binds to unix domain sockets to avoid race conditions // while an existing socket is unlinked. func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) { - if na.IsUnixNetwork() { - unixSocketsMu.Lock() - defer unixSocketsMu.Unlock() - } + var ( + ln any + err error + ) - // check to see if plugin provides listener - if ln, err := getListenerFromPlugin(ctx, na.Network, na.Host, na.port(), portOffset, config); ln != nil || err != nil { + // check to see if network provides a listener + if ln, err = getListenerFromNetwork(ctx, na.Network, na.Host, na.port(), portOffset, config); ln != nil || err != nil { return ln, err } // create (or reuse) the listener ourselves - return na.listen(ctx, portOffset, config) -} - -func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) { var ( - ln any - err error address string unixFileMode fs.FileMode ) + // lock other unix sockets from being bound and // split unix socket addr early so lnKey // is independent of permissions bits if na.IsUnixNetwork() { + unixSocketsMu.Lock() + defer unixSocketsMu.Unlock() + address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host) if err != nil { return nil, err @@ -172,7 +166,7 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net address = na.JoinHostPort(portOffset) } - if strings.HasPrefix(na.Network, "ip") { + if na.IsIpNetwork() { ln, err = config.ListenPacket(ctx, na.Network, address) } else { if na.IsUnixNetwork() { @@ -220,6 +214,12 @@ func (na NetworkAddress) IsFdNetwork() bool { return IsFdNetwork(na.Network) } +// IsIpNetwork returns true if na.Network starts with +// ip: ip4: or ip6: +func (na NetworkAddress) IsIpNetwork() bool { + return IsIpNetwork(na.Network) +} + // JoinHostPort is like net.JoinHostPort, but where the port // is StartPort + offset. func (na NetworkAddress) JoinHostPort(offset uint) string { @@ -299,74 +299,6 @@ func (na NetworkAddress) String() string { return JoinNetworkAddress(na.Network, na.Host, na.port()) } -// IsUnixNetwork returns true if the netw is a unix network. -func IsUnixNetwork(netw string) bool { - return strings.HasPrefix(netw, "unix") -} - -// IsFdNetwork returns true if the netw is a fd network. -func IsFdNetwork(netw string) bool { - return strings.HasPrefix(netw, "fd") -} - -// 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. -// -// 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") - } - - fdNamesStr := os.Getenv("LISTEN_FDNAMES") - if fdNamesStr == "" { - return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set") - } - - // 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) - } - - 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) - } - } - - // 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 // components. The input string is expected to be of // the form "network/host:port-range" where any part is @@ -398,27 +330,9 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui }, err } if IsFdNetwork(network) { - fdAddr := host - - // Handle named socket activation (fdname/name, fdgramname/name) - if strings.HasPrefix(network, "fdname") || strings.HasPrefix(network, "fdgramname") { - fdNum, err := getFdByName(host) - if err != nil { - return NetworkAddress{}, fmt.Errorf("named socket activation: %v", err) - } - fdAddr = strconv.Itoa(fdNum) - - // Normalize network to standard fd/fdgram - if strings.HasPrefix(network, "fdname") { - network = "fd" - } else { - network = "fdgram" - } - } - return NetworkAddress{ Network: network, - Host: fdAddr, + Host: host, }, nil } var start, end uint64 @@ -713,55 +627,12 @@ func (fcql *fakeCloseQuicListener) Close() error { return nil } -// RegisterNetwork registers a network type with Caddy so that if a listener is -// created for that network type, getListener will be invoked to get the listener. -// This should be called during init() and will panic if the network type is standard -// or reserved, or if it is already registered. EXPERIMENTAL and subject to change. -func RegisterNetwork(network string, getListener ListenerFunc) { - network = strings.TrimSpace(strings.ToLower(network)) - - if network == "tcp" || network == "tcp4" || network == "tcp6" || - network == "udp" || network == "udp4" || network == "udp6" || - network == "unix" || network == "unixpacket" || network == "unixgram" || - strings.HasPrefix(network, "ip:") || strings.HasPrefix(network, "ip4:") || strings.HasPrefix(network, "ip6:") || - network == "fd" || network == "fdgram" { - panic("network type " + network + " is reserved") - } - - if _, ok := networkTypes[strings.ToLower(network)]; ok { - panic("network type " + network + " is already registered") - } - - networkTypes[network] = getListener -} - var unixSocketsMu sync.Mutex -// getListenerFromPlugin returns a listener on the given network and address -// if a plugin has registered the network name. It may return (nil, nil) if -// no plugin can provide a listener. -func getListenerFromPlugin(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { - // get listener from plugin if network type is registered - if getListener, ok := networkTypes[network]; ok { - Log().Debug("getting listener from plugin", zap.String("network", network)) - return getListener(ctx, network, host, port, portOffset, config) - } - - return nil, nil -} - func listenerKey(network, addr string) string { return network + "/" + addr } -// ListenerFunc is a function that can return a listener given a network and address. -// The listeners must be capable of overlapping: with Caddy, new configs are loaded -// before old ones are unloaded, so listeners may overlap briefly if the configs -// both need the same listener. EXPERIMENTAL and subject to change. -type ListenerFunc func(ctx context.Context, network, host, portRange string, portOffset uint, cfg net.ListenConfig) (any, error) - -var networkTypes = map[string]ListenerFunc{} - // ListenerWrapper is a type that wraps a listener // so it can modify the input listener's methods. // Modules that implement this interface are found diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index ac30f40286e..d3e654533e1 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -1124,34 +1124,3 @@ const ( // For tracking the real client IP (affected by trusted_proxy) ClientIPVarKey string = "client_ip" ) - -var networkTypesHTTP3 = map[string]string{ - "unixgram": "unixgram", - "udp": "udp", - "udp4": "udp4", - "udp6": "udp6", - "tcp": "udp", - "tcp4": "udp4", - "tcp6": "udp6", - "fdgram": "fdgram", -} - -// RegisterNetworkHTTP3 registers a mapping from non-HTTP/3 network to HTTP/3 -// network. This should be called during init() and will panic if the network -// type is standard, reserved, or already registered. -// -// EXPERIMENTAL: Subject to change. -func RegisterNetworkHTTP3(originalNetwork, h3Network string) { - if _, ok := networkTypesHTTP3[strings.ToLower(originalNetwork)]; ok { - panic("network type " + originalNetwork + " is already registered") - } - networkTypesHTTP3[originalNetwork] = h3Network -} - -func getHTTP3Network(originalNetwork string) (string, error) { - h3Network, ok := networkTypesHTTP3[strings.ToLower(originalNetwork)] - if !ok { - return "", fmt.Errorf("network '%s' cannot handle HTTP/3 connections", originalNetwork) - } - return h3Network, nil -} diff --git a/networks.go b/networks.go new file mode 100644 index 00000000000..6241fbe4055 --- /dev/null +++ b/networks.go @@ -0,0 +1,119 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddy + +import ( + "strings" + "context" + "net" + "sync" + "go.uber.org/zap" + "fmt" +) + +// IsUnixNetwork returns true if the netw is a unix network. +func IsUnixNetwork(netw string) bool { + return netw == "unix" || netw == "unixgram" || netw == "unixpacket" +} + +// IsIpNetwork returns true if the netw is an ip network. +func IsIpNetwork(netw string) bool { + return strings.HasPrefix(netw, "ip:") || strings.HasPrefix(netw, "ip4:") || strings.HasPrefix(netw, "ip6:") +} + +// IsFdNetwork returns true if the netw is a fd network. +func IsFdNetwork(netw string) bool { + return netw == "fd" || netw == "fdgram" +} + +// ListenerFunc is a function that can return a listener given a network and address. +// The listeners must be capable of overlapping: with Caddy, new configs are loaded +// before old ones are unloaded, so listeners may overlap briefly if the configs +// both need the same listener. EXPERIMENTAL and subject to change. +type ListenerFunc func(ctx context.Context, network, host, portRange string, portOffset uint, cfg net.ListenConfig) (any, error) + +var networkPlugins = map[string]ListenerFunc{} +var networkPluginsMu sync.RWMutex + +// RegisterNetwork registers a network plugin with Caddy so that if a listener is +// created for that network plugin, getListener will be invoked to get the listener. +// This should be called during init() and will panic if the network type is standard +// or reserved, or if it is already registered. EXPERIMENTAL and subject to change. +func RegisterNetwork(network string, getListener ListenerFunc) { + network = strings.TrimSpace(strings.ToLower(network)) + + if IsReservedNetwork(network) { + panic("network type " + network + " is reserved") + } + + if _, ok := networkPlugins[strings.ToLower(network)]; ok { + panic("network type " + network + " is already registered") + } + + networkPluginsMu.Lock() + defer networkPluginsMu.Unlock() + + networkPlugins[network] = getListener +} + +// getListenerFromPlugin returns a listener on the given network and address +// if a plugin has registered the network name. It may return (nil, nil) if +// no plugin can provide a listener. +func getListenerFromPlugin(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { + networkPluginsMu.RLock() + defer networkPluginsMu.RUnlock() + + // get listener from plugin if network is registered + if getListener, ok := networkPlugins[network]; ok { + Log().Debug("getting listener from plugin", zap.String("network", network)) + return getListener(ctx, network, host, port, portOffset, config) + } + + return nil, nil +} + +var networkHTTP3Plugins = map[string]string{} +var networkHTTP3PluginsMu sync.RWMutex + +// RegisterNetworkHTTP3 registers a mapping from non-HTTP/3 network to HTTP/3 +// network. This should be called during init() and will panic if the network +// type is standard, reserved, or already registered. +// +// EXPERIMENTAL: Subject to change. +func RegisterNetworkHTTP3(originalNetwork, h3Network string) { + if IsReservedNetwork(originalNetwork) { + panic("network type " + originalNetwork + " is reserved") + } + if _, ok := networkHTTP3Plugins[strings.ToLower(originalNetwork)]; ok { + panic("network type " + originalNetwork + " is already registered") + } + + networkHTTP3PluginsMu.Lock() + defer networkHTTP3PluginsMu.Unlock() + + networkHTTP3Plugins[originalNetwork] = h3Network +} + +func getHTTP3Plugin(originalNetwork string) (string, error) { + networkHTTP3PluginsMu.RLock() + defer networkHTTP3PluginsMu.RUnlock() + + h3Network, ok := networkHTTP3Plugins[strings.ToLower(originalNetwork)] + if !ok { + return "", fmt.Errorf("network '%s' cannot handle HTTP/3 connections", originalNetwork) + } + + return h3Network, nil +} diff --git a/networks_nosystemd.go b/networks_nosystemd.go new file mode 100644 index 00000000000..63424800f3d --- /dev/null +++ b/networks_nosystemd.go @@ -0,0 +1,29 @@ +//go:build !linux || nosystemd + +package caddy + +func IsReservedNetwork(network string) bool { + return network == "tcp" || network == "tcp4" || network == "tcp6" || + network == "udp" || network == "udp4" || network == "udp6" || + IsUnixNetwork(network) || + IsIpNetwork(network) || + IsFdNetwork(network) +} + +func getListenerFromNetwork(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { + return getListenerFromPlugin(ctx, network, host, port, portOffset, config) +} + +func getHTTP3Network(originalNetwork string) (string, error) { + switch originalNetwork { + case "unixgram": return "unixgram", nil + case "udp": return "udp", nil + case "udp4": return "udp4", nil + case "udp6": return "udp6", nil + case "tcp": return "udp", nil + case "tcp4": return "udp4", nil + case "tcp6": return "udp6", nil + case "fdgram": return "fdgram", nil + } + return getHTTP3Plugin(originalNetwork) +} diff --git a/networks_systemd.go b/networks_systemd.go new file mode 100644 index 00000000000..4a1ddb2def4 --- /dev/null +++ b/networks_systemd.go @@ -0,0 +1,158 @@ +//go:build linux && !nosystemd + +package caddy + +import ( + "context" + "net" + "sync" + "os" + "errors" + "strconv" + "fmt" + "strings" +) + +func IsSdNetwork(network string) bool { + return network == "sd" || network == "sdgram" +} + +func IsReservedNetwork(network string) bool { + return network == "tcp" || network == "tcp4" || network == "tcp6" || + network == "udp" || network == "udp4" || network == "udp6" || + IsUnixNetwork(network) || + IsIpNetwork(network) || + IsFdNetwork(network) || + IsSdNetwork(network) +} + +var ( + nameToFiles map[string][]int + nameToFilesErr error + nameToFilesMu sync.Mutex +) + +func sdListenFds() (map[string][]int, error) { + nameToFilesMu.Lock() + defer nameToFilesMu.Unlock() + + if nameToFilesErr != nil { + return nil, nameToFilesErr + } + + if nameToFiles != nil { + return nameToFiles, nil + } + + const lnFdsStart = 3 + + lnPid, ok := os.LookupEnv("LISTEN_PID") + if !ok { + nameToFilesErr = errors.New("LISTEN_PID is unset.") + return nil, nameToFilesErr + } + + pid, err := strconv.ParseUint(lnPid, 0, strconv.IntSize) + if err != nil { + nameToFilesErr = err + return nil, nameToFilesErr + } + + if pid != uint64(os.Getpid()) { + nameToFilesErr = fmt.Errorf("LISTEN_PID does not match pid: %d != %d", pid, os.Getpid()) + return nil, nameToFilesErr + } + + lnFds, ok := os.LookupEnv("LISTEN_FDS") + if !ok { + nameToFilesErr = errors.New("LISTEN_FDS is unset.") + return nil, nameToFilesErr + } + + fds, err := strconv.ParseUint(lnFds, 0, strconv.IntSize) + if err != nil { + nameToFilesErr = err + return nil, nameToFilesErr + } + + lnFdnames, ok := os.LookupEnv("LISTEN_FDNAMES") + if !ok { + nameToFilesErr = errors.New("LISTEN_FDNAMES is unset.") + return nil, nameToFilesErr + } + + fdNames := strings.Split(lnFdnames, ":") + if fds != uint64(len(fdNames)) { + nameToFilesErr = fmt.Errorf("LISTEN_FDS does not match LISTEN_FDNAMES length: %d != %d", fds, len(fdNames)) + return nil, nameToFilesErr + } + + nameToFiles = make(map[string][]int, len(fdNames)) + for index, name := range fdNames { + nameToFiles[name] = append(nameToFiles[name], lnFdsStart+index) + } + + return nameToFiles, nil +} + +func getListenerFromNetwork(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { + if IsSdNetwork(network) { + sdLnFds, err := sdListenFds() + if err != nil { + return nil, err + } + + name, index, li := host, portOffset, strings.LastIndex(host, "/") + if li >= 0 { + name = host[:li] + i, err := strconv.ParseUint(host[li+1:], 0, strconv.IntSize) + if err != nil { + return nil, err + } + index += uint(i) + } + + files, ok := sdLnFds[name] + if !ok { + return nil, fmt.Errorf("invalid listen fd name: %s", name) + } + + if uint(len(files)) <= index { + return nil, fmt.Errorf("invalid listen fd index: %d", index) + } + file := files[index] + + var fdNetwork string + switch network { + case "sd": + fdNetwork = "fd" + case "sdgram": + fdNetwork = "fdgram" + default: + return nil, fmt.Errorf("invalid network: %s", network) + } + + na, err := ParseNetworkAddress(JoinNetworkAddress(fdNetwork, strconv.Itoa(file), port)) + if err != nil { + return nil, err + } + + return na.Listen(ctx, portOffset, config) + } + return getListenerFromPlugin(ctx, network, host, port, portOffset, config) +} + +func getHTTP3Network(originalNetwork string) (string, error) { + switch originalNetwork { + case "unixgram": return "unixgram", nil + case "udp": return "udp", nil + case "udp4": return "udp4", nil + case "udp6": return "udp6", nil + case "tcp": return "udp", nil + case "tcp4": return "udp4", nil + case "tcp6": return "udp6", nil + case "fdgram": return "fdgram", nil + case "sdgram": return "sdgram", nil + } + return getHTTP3Plugin(originalNetwork) +} From 8725ffe77021852098b42848e9286ea96f45e115 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 31 Oct 2025 18:01:58 -0600 Subject: [PATCH 02/10] update tests --- listeners_test.go | 206 ++++++++++++++++++++++++++++++++------------ networks_systemd.go | 102 +++++++++++----------- 2 files changed, 200 insertions(+), 108 deletions(-) diff --git a/listeners_test.go b/listeners_test.go index c2cc255f21f..ce3312b4610 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -18,6 +18,7 @@ import ( "os" "reflect" "testing" + "strconv" "github.com/caddyserver/caddy/v2/internal" ) @@ -658,6 +659,8 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) { func TestGetFdByName(t *testing.T) { // Save original environment originalFdNames := os.Getenv("LISTEN_FDNAMES") + originalFds := os.Getenv("LISTEN_FDS") + originalPid := os.Getenv("LISTEN_PID") // Restore environment after test defer func() { @@ -666,121 +669,150 @@ func TestGetFdByName(t *testing.T) { } else { os.Unsetenv("LISTEN_FDNAMES") } + if originalFds != "" { + os.Setenv("LISTEN_FDS", originalFds) + } else { + os.Unsetenv("LISTEN_FDS") + } + if originalPid != "" { + os.Setenv("LISTEN_PID", originalPid) + } else { + os.Unsetenv("LISTEN_PID") + } }() tests := []struct { name string fdNames string + fds string socketName string - expectedFd int + expectedFd uint expectError bool }{ { name: "simple http socket", fdNames: "http", + fds: "1", socketName: "http", expectedFd: 3, }, { name: "multiple different sockets - first", fdNames: "http:https:dns", + fds: "3", socketName: "http", expectedFd: 3, }, { name: "multiple different sockets - second", fdNames: "http:https:dns", + fds: "3", socketName: "https", expectedFd: 4, }, { name: "multiple different sockets - third", fdNames: "http:https:dns", + fds: "3", socketName: "dns", expectedFd: 5, }, { name: "duplicate names - first occurrence (no index)", fdNames: "web:web:api", + fds: "3", socketName: "web", expectedFd: 3, }, { name: "duplicate names - first occurrence (explicit index 0)", fdNames: "web:web:api", - socketName: "web:0", + fds: "3", + socketName: "web/0", expectedFd: 3, }, { name: "duplicate names - second occurrence (index 1)", fdNames: "web:web:api", - socketName: "web:1", + fds: "3", + socketName: "web/1", expectedFd: 4, }, { name: "complex duplicates - first api", fdNames: "web:api:web:api:dns", - socketName: "api:0", + fds: "5", + socketName: "api/0", expectedFd: 4, }, { name: "complex duplicates - second api", fdNames: "web:api:web:api:dns", - socketName: "api:1", + fds: "5", + socketName: "api/1", expectedFd: 6, }, { name: "complex duplicates - first web", fdNames: "web:api:web:api:dns", - socketName: "web:0", + fds: "5", + socketName: "web/0", expectedFd: 3, }, { name: "complex duplicates - second web", fdNames: "web:api:web:api:dns", - socketName: "web:1", + fds: "5", + socketName: "web/1", expectedFd: 5, }, { name: "socket not found", fdNames: "http:https", + fds: "2", socketName: "missing", expectError: true, }, { name: "empty socket name", fdNames: "http", + fds: "1", socketName: "", expectError: true, }, { name: "missing LISTEN_FDNAMES", fdNames: "", + fds: "", socketName: "http", expectError: true, }, { name: "index out of range", fdNames: "web:web", - socketName: "web:2", + fds: "2", + socketName: "web/2", expectError: true, }, { name: "negative index", fdNames: "web", - socketName: "web:-1", + fds: "1", + socketName: "web/-1", expectError: true, }, { name: "invalid index format", fdNames: "web", - socketName: "web:abc", + fds: "1", + socketName: "web/abc", expectError: true, }, { name: "too many colons", fdNames: "web", - socketName: "web:0:extra", + fds: "1", + socketName: "web/0/extra", expectError: true, }, } @@ -794,8 +826,24 @@ func TestGetFdByName(t *testing.T) { os.Unsetenv("LISTEN_FDNAMES") } + if tc.fds != "" { + os.Setenv("LISTEN_FDS", tc.fds) + } else { + os.Unsetenv("LISTEN_FDS") + } + + os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid())) + // Test the function - fd, err := getFdByName(tc.socketName) + var ( + listenFdsWithNames map[string][]uint + err error + fd uint + ) + listenFdsWithNames, err = sdListenFdsWithNames() + if err == nil { + fd, err = sdListenFd(listenFdsWithNames, tc.socketName, 0) + } if tc.expectError { if err == nil { @@ -817,113 +865,158 @@ func TestGetFdByName(t *testing.T) { func TestParseNetworkAddressFdName(t *testing.T) { // Save and restore environment originalFdNames := os.Getenv("LISTEN_FDNAMES") + originalFds := os.Getenv("LISTEN_FDS") + originalPid := os.Getenv("LISTEN_PID") + defer func() { if originalFdNames != "" { os.Setenv("LISTEN_FDNAMES", originalFdNames) } else { os.Unsetenv("LISTEN_FDNAMES") } + if originalFds != "" { + os.Setenv("LISTEN_FDS", originalFds) + } else { + os.Unsetenv("LISTEN_FDS") + } + if originalPid != "" { + os.Setenv("LISTEN_PID", originalPid) + } else { + os.Unsetenv("LISTEN_PID") + } }() // Set up test environment os.Setenv("LISTEN_FDNAMES", "http:https:dns") + os.Setenv("LISTEN_FDS", "3") + os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid())) tests := []struct { - input string - expectAddr NetworkAddress - expectErr bool + input string + expectedAddr NetworkAddress + expectedFd uint + expectErr bool }{ { - input: "fdname/http", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "3", + input: "sd/http", + expectedAddr: NetworkAddress{ + Network: "sd", + Host: "http", }, + expectedFd: 3, }, { - input: "fdname/https", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "4", + input: "sd/https", + expectedAddr: NetworkAddress{ + Network: "sd", + Host: "https", }, + expectedFd: 4, }, { - input: "fdname/dns", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "5", + input: "sd/dns", + expectedAddr: NetworkAddress{ + Network: "sd", + Host: "dns", }, + expectedFd: 5, }, { - input: "fdname/http:0", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "3", + input: "sd/http/0", + expectedAddr: NetworkAddress{ + Network: "sd", + Host: "http/0", }, + expectedFd: 3, }, { - input: "fdname/https:0", - expectAddr: NetworkAddress{ - Network: "fd", - Host: "4", + input: "sd/https/0", + expectedAddr: NetworkAddress{ + Network: "sd", + Host: "https/0", }, + expectedFd: 4, }, { - input: "fdgramname/http", - expectAddr: NetworkAddress{ - Network: "fdgram", - Host: "3", + input: "sdgram/http", + expectedAddr: NetworkAddress{ + Network: "sdgram", + Host: "http", }, + expectedFd: 3, }, { - input: "fdgramname/https", - expectAddr: NetworkAddress{ - Network: "fdgram", - Host: "4", + input: "sdgram/https", + expectedAddr: NetworkAddress{ + Network: "sdgram", + Host: "https", }, + expectedFd: 4, }, { - input: "fdgramname/http:0", - expectAddr: NetworkAddress{ - Network: "fdgram", - Host: "3", + input: "sdgram/http/0", + expectedAddr: NetworkAddress{ + Network: "sdgram", + Host: "http/0", }, + expectedFd: 3, }, { - input: "fdname/nonexistent", + input: "sd/nonexistent", expectErr: true, }, { - input: "fdgramname/nonexistent", + input: "sd/nonexistent", expectErr: true, }, { - input: "fdname/http:99", + input: "sd/http/99", expectErr: true, }, { - input: "fdname/invalid:abc", + input: "sd/invalid/abc", expectErr: true, }, // Test that old fd/N syntax still works { input: "fd/7", - expectAddr: NetworkAddress{ + expectedAddr: NetworkAddress{ Network: "fd", Host: "7", }, + expectedFd: 7, }, { input: "fdgram/8", - expectAddr: NetworkAddress{ + expectedAddr: NetworkAddress{ Network: "fdgram", Host: "8", }, + expectedFd: 8, }, } for i, tc := range tests { actualAddr, err := ParseNetworkAddress(tc.input) + var ( + listenFdsWithNames map[string][]uint + fd uint + ) + if err == nil { + switch actualAddr.Network { + case "fd": fallthrough + case "fdgram": + var fd64 uint64 + fd64, err = strconv.ParseUint(actualAddr.Host, 0, strconv.IntSize) + if err == nil { + fd = uint(fd64) + } + case "sd": fallthrough + case "sdgram": + listenFdsWithNames, err = sdListenFdsWithNames() + fd, err = sdListenFd(listenFdsWithNames, actualAddr.Host, 0) + } + } if tc.expectErr && err == nil { t.Errorf("Test %d (%s): Expected error but got none", i, tc.input) @@ -931,8 +1024,11 @@ func TestParseNetworkAddressFdName(t *testing.T) { if !tc.expectErr && err != nil { t.Errorf("Test %d (%s): Expected no error but got: %v", i, tc.input, err) } - if !tc.expectErr && !reflect.DeepEqual(tc.expectAddr, actualAddr) { - t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectAddr, actualAddr) + if !tc.expectErr && !reflect.DeepEqual(tc.expectedAddr, actualAddr) { + t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectedAddr, actualAddr) + } + if !tc.expectErr && fd != tc.expectedFd { + t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd) } } } diff --git a/networks_systemd.go b/networks_systemd.go index 4a1ddb2def4..73f3b0fb07b 100644 --- a/networks_systemd.go +++ b/networks_systemd.go @@ -26,101 +26,97 @@ func IsReservedNetwork(network string) bool { IsSdNetwork(network) } -var ( - nameToFiles map[string][]int - nameToFilesErr error - nameToFilesMu sync.Mutex -) - -func sdListenFds() (map[string][]int, error) { - nameToFilesMu.Lock() - defer nameToFilesMu.Unlock() - - if nameToFilesErr != nil { - return nil, nameToFilesErr - } - - if nameToFiles != nil { - return nameToFiles, nil - } - +func sdListenFdsWithNames() (map[string][]uint, error) { const lnFdsStart = 3 lnPid, ok := os.LookupEnv("LISTEN_PID") if !ok { - nameToFilesErr = errors.New("LISTEN_PID is unset.") - return nil, nameToFilesErr + return nil, errors.New("LISTEN_PID is unset.") } pid, err := strconv.ParseUint(lnPid, 0, strconv.IntSize) if err != nil { - nameToFilesErr = err - return nil, nameToFilesErr + return nil, err } if pid != uint64(os.Getpid()) { - nameToFilesErr = fmt.Errorf("LISTEN_PID does not match pid: %d != %d", pid, os.Getpid()) - return nil, nameToFilesErr + return nil, fmt.Errorf("LISTEN_PID does not match pid: %d != %d", pid, os.Getpid()) } lnFds, ok := os.LookupEnv("LISTEN_FDS") if !ok { - nameToFilesErr = errors.New("LISTEN_FDS is unset.") - return nil, nameToFilesErr + return nil, errors.New("LISTEN_FDS is unset.") } fds, err := strconv.ParseUint(lnFds, 0, strconv.IntSize) if err != nil { - nameToFilesErr = err - return nil, nameToFilesErr + return nil, err } lnFdnames, ok := os.LookupEnv("LISTEN_FDNAMES") if !ok { - nameToFilesErr = errors.New("LISTEN_FDNAMES is unset.") - return nil, nameToFilesErr + return nil, errors.New("LISTEN_FDNAMES is unset.") } fdNames := strings.Split(lnFdnames, ":") if fds != uint64(len(fdNames)) { - nameToFilesErr = fmt.Errorf("LISTEN_FDS does not match LISTEN_FDNAMES length: %d != %d", fds, len(fdNames)) - return nil, nameToFilesErr + return nil, fmt.Errorf("LISTEN_FDS does not match LISTEN_FDNAMES length: %d != %d", fds, len(fdNames)) } - nameToFiles = make(map[string][]int, len(fdNames)) + nameToFiles := make(map[string][]uint, len(fdNames)) for index, name := range fdNames { - nameToFiles[name] = append(nameToFiles[name], lnFdsStart+index) + nameToFiles[name] = append(nameToFiles[name], lnFdsStart+uint(index)) } return nameToFiles, nil } -func getListenerFromNetwork(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { - if IsSdNetwork(network) { - sdLnFds, err := sdListenFds() +func sdListenFd(nameToFiles map[string][]uint, host string, portOffset uint) (uint, error) { + name, index, li := host, portOffset, strings.LastIndex(host, "/") + if li >= 0 { + name = host[:li] + i, err := strconv.ParseUint(host[li+1:], 0, strconv.IntSize) if err != nil { - return nil, err + return 0, err } + index += uint(i) + } - name, index, li := host, portOffset, strings.LastIndex(host, "/") - if li >= 0 { - name = host[:li] - i, err := strconv.ParseUint(host[li+1:], 0, strconv.IntSize) - if err != nil { - return nil, err - } - index += uint(i) + files, ok := nameToFiles[name] + if !ok { + return 0, fmt.Errorf("invalid listen fd name: %s", name) + } + + if uint(len(files)) <= index { + return 0, fmt.Errorf("invalid listen fd index: %d", index) + } + + return files[index], nil +} + +var ( + initNameToFiles map[string][]uint + initNameToFilesErr error + initNameToFilesMu sync.Mutex +) + +func getListenerFromNetwork(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { + if IsSdNetwork(network) { + initNameToFilesMu.Lock() + defer initNameToFilesMu.Unlock() + + if initNameToFiles == nil && initNameToFilesErr == nil { + initNameToFiles, initNameToFilesErr = sdListenFdsWithNames() } - files, ok := sdLnFds[name] - if !ok { - return nil, fmt.Errorf("invalid listen fd name: %s", name) + if initNameToFilesErr != nil { + return nil, initNameToFilesErr } - if uint(len(files)) <= index { - return nil, fmt.Errorf("invalid listen fd index: %d", index) + file, err := sdListenFd(initNameToFiles, host, portOffset) + if err != nil { + return nil, err } - file := files[index] var fdNetwork string switch network { @@ -132,7 +128,7 @@ func getListenerFromNetwork(ctx context.Context, network, host, port string, por return nil, fmt.Errorf("invalid network: %s", network) } - na, err := ParseNetworkAddress(JoinNetworkAddress(fdNetwork, strconv.Itoa(file), port)) + na, err := ParseNetworkAddress(JoinNetworkAddress(fdNetwork, strconv.FormatUint(uint64(file),10), port)) if err != nil { return nil, err } From 659e3081b8029c1b22118f0a22abff8b13d0a220 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 31 Oct 2025 18:23:20 -0600 Subject: [PATCH 03/10] gofmt --- listeners.go | 4 ++-- listeners_test.go | 32 ++++++++++++++++-------------- modules/logging/filters_test.go | 4 ++-- networks.go | 6 +++--- networks_nosystemd.go | 24 ++++++++++++++-------- networks_systemd.go | 35 +++++++++++++++++++++------------ 6 files changed, 62 insertions(+), 43 deletions(-) diff --git a/listeners.go b/listeners.go index de0c9402baf..6623a57d1d5 100644 --- a/listeners.go +++ b/listeners.go @@ -134,8 +134,8 @@ func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) // while an existing socket is unlinked. func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) { var ( - ln any - err error + ln any + err error ) // check to see if network provides a listener diff --git a/listeners_test.go b/listeners_test.go index ce3312b4610..7fd26fdea5a 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -17,8 +17,8 @@ package caddy import ( "os" "reflect" - "testing" "strconv" + "testing" "github.com/caddyserver/caddy/v2/internal" ) @@ -837,8 +837,8 @@ func TestGetFdByName(t *testing.T) { // Test the function var ( listenFdsWithNames map[string][]uint - err error - fd uint + err error + fd uint ) listenFdsWithNames, err = sdListenFdsWithNames() if err == nil { @@ -1000,21 +1000,23 @@ func TestParseNetworkAddressFdName(t *testing.T) { actualAddr, err := ParseNetworkAddress(tc.input) var ( listenFdsWithNames map[string][]uint - fd uint + fd uint ) if err == nil { switch actualAddr.Network { - case "fd": fallthrough - case "fdgram": - var fd64 uint64 - fd64, err = strconv.ParseUint(actualAddr.Host, 0, strconv.IntSize) - if err == nil { - fd = uint(fd64) - } - case "sd": fallthrough - case "sdgram": - listenFdsWithNames, err = sdListenFdsWithNames() - fd, err = sdListenFd(listenFdsWithNames, actualAddr.Host, 0) + case "fd": + fallthrough + case "fdgram": + var fd64 uint64 + fd64, err = strconv.ParseUint(actualAddr.Host, 0, strconv.IntSize) + if err == nil { + fd = uint(fd64) + } + case "sd": + fallthrough + case "sdgram": + listenFdsWithNames, err = sdListenFdsWithNames() + fd, err = sdListenFd(listenFdsWithNames, actualAddr.Host, 0) } } diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go index 42aa297575b..cf35e717827 100644 --- a/modules/logging/filters_test.go +++ b/modules/logging/filters_test.go @@ -404,12 +404,12 @@ func TestMultiRegexpFilterInputSizeLimit(t *testing.T) { // Test with very large input (should be truncated) largeInput := strings.Repeat("test", 300000) // Creates ~1.2MB string out := f.Filter(zapcore.Field{String: largeInput}) - + // The input should be truncated to 1MB and still processed if len(out.String) > 1000000 { t.Fatalf("output string not truncated: length %d", len(out.String)) } - + // Should still contain replacements within the truncated portion if !strings.Contains(out.String, "REPLACED") { t.Fatalf("replacements not applied to truncated input") diff --git a/networks.go b/networks.go index 6241fbe4055..d527c73236b 100644 --- a/networks.go +++ b/networks.go @@ -15,12 +15,12 @@ package caddy import ( - "strings" "context" + "fmt" + "go.uber.org/zap" "net" + "strings" "sync" - "go.uber.org/zap" - "fmt" ) // IsUnixNetwork returns true if the netw is a unix network. diff --git a/networks_nosystemd.go b/networks_nosystemd.go index 63424800f3d..90cfa7f3166 100644 --- a/networks_nosystemd.go +++ b/networks_nosystemd.go @@ -16,14 +16,22 @@ func getListenerFromNetwork(ctx context.Context, network, host, port string, por func getHTTP3Network(originalNetwork string) (string, error) { switch originalNetwork { - case "unixgram": return "unixgram", nil - case "udp": return "udp", nil - case "udp4": return "udp4", nil - case "udp6": return "udp6", nil - case "tcp": return "udp", nil - case "tcp4": return "udp4", nil - case "tcp6": return "udp6", nil - case "fdgram": return "fdgram", nil + case "unixgram": + return "unixgram", nil + case "udp": + return "udp", nil + case "udp4": + return "udp4", nil + case "udp6": + return "udp6", nil + case "tcp": + return "udp", nil + case "tcp4": + return "udp4", nil + case "tcp6": + return "udp6", nil + case "fdgram": + return "fdgram", nil } return getHTTP3Plugin(originalNetwork) } diff --git a/networks_systemd.go b/networks_systemd.go index 73f3b0fb07b..e0c02a05d01 100644 --- a/networks_systemd.go +++ b/networks_systemd.go @@ -4,13 +4,13 @@ package caddy import ( "context" + "errors" + "fmt" "net" - "sync" "os" - "errors" "strconv" - "fmt" "strings" + "sync" ) func IsSdNetwork(network string) bool { @@ -128,7 +128,7 @@ func getListenerFromNetwork(ctx context.Context, network, host, port string, por return nil, fmt.Errorf("invalid network: %s", network) } - na, err := ParseNetworkAddress(JoinNetworkAddress(fdNetwork, strconv.FormatUint(uint64(file),10), port)) + na, err := ParseNetworkAddress(JoinNetworkAddress(fdNetwork, strconv.FormatUint(uint64(file), 10), port)) if err != nil { return nil, err } @@ -140,15 +140,24 @@ func getListenerFromNetwork(ctx context.Context, network, host, port string, por func getHTTP3Network(originalNetwork string) (string, error) { switch originalNetwork { - case "unixgram": return "unixgram", nil - case "udp": return "udp", nil - case "udp4": return "udp4", nil - case "udp6": return "udp6", nil - case "tcp": return "udp", nil - case "tcp4": return "udp4", nil - case "tcp6": return "udp6", nil - case "fdgram": return "fdgram", nil - case "sdgram": return "sdgram", nil + case "unixgram": + return "unixgram", nil + case "udp": + return "udp", nil + case "udp4": + return "udp4", nil + case "udp6": + return "udp6", nil + case "tcp": + return "udp", nil + case "tcp4": + return "udp4", nil + case "tcp6": + return "udp6", nil + case "fdgram": + return "fdgram", nil + case "sdgram": + return "sdgram", nil } return getHTTP3Plugin(originalNetwork) } From c87d9c38deac73d3b87817a5fb7110ff8374c9c0 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 31 Oct 2025 18:44:07 -0600 Subject: [PATCH 04/10] use caddy package --- modules/caddyhttp/server.go | 2 +- networks_nosystemd.go | 2 +- networks_systemd.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index d3e654533e1..a171bf60a30 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -620,7 +620,7 @@ func (s *Server) findLastRouteWithHostMatcher() int { // not already done, and then uses that server to serve HTTP/3 over // the listener, with Server s as the handler. func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error { - h3net, err := getHTTP3Network(addr.Network) + h3net, err := caddy.GetHTTP3Network(addr.Network) if err != nil { return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err) } diff --git a/networks_nosystemd.go b/networks_nosystemd.go index 90cfa7f3166..605bf02e511 100644 --- a/networks_nosystemd.go +++ b/networks_nosystemd.go @@ -14,7 +14,7 @@ func getListenerFromNetwork(ctx context.Context, network, host, port string, por return getListenerFromPlugin(ctx, network, host, port, portOffset, config) } -func getHTTP3Network(originalNetwork string) (string, error) { +func GetHTTP3Network(originalNetwork string) (string, error) { switch originalNetwork { case "unixgram": return "unixgram", nil diff --git a/networks_systemd.go b/networks_systemd.go index e0c02a05d01..fd03c1bd446 100644 --- a/networks_systemd.go +++ b/networks_systemd.go @@ -138,7 +138,7 @@ func getListenerFromNetwork(ctx context.Context, network, host, port string, por return getListenerFromPlugin(ctx, network, host, port, portOffset, config) } -func getHTTP3Network(originalNetwork string) (string, error) { +func GetHTTP3Network(originalNetwork string) (string, error) { switch originalNetwork { case "unixgram": return "unixgram", nil From 4c3696238a82e19260a376fd31b6df00b14807f6 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 31 Oct 2025 18:48:38 -0600 Subject: [PATCH 05/10] fix nosystemd build --- networks_nosystemd.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/networks_nosystemd.go b/networks_nosystemd.go index 605bf02e511..f56f79e0525 100644 --- a/networks_nosystemd.go +++ b/networks_nosystemd.go @@ -2,6 +2,11 @@ package caddy +import ( + "context" + "net" +) + func IsReservedNetwork(network string) bool { return network == "tcp" || network == "tcp4" || network == "tcp6" || network == "udp" || network == "udp4" || network == "udp6" || From e5c4b04873e4a577c05fb0ddc22d61a6efc1fc15 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 31 Oct 2025 18:51:06 -0600 Subject: [PATCH 06/10] gofumpt --- networks.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/networks.go b/networks.go index d527c73236b..f739bc71566 100644 --- a/networks.go +++ b/networks.go @@ -17,10 +17,11 @@ package caddy import ( "context" "fmt" - "go.uber.org/zap" "net" "strings" "sync" + + "go.uber.org/zap" ) // IsUnixNetwork returns true if the netw is a unix network. @@ -44,8 +45,10 @@ func IsFdNetwork(netw string) bool { // both need the same listener. EXPERIMENTAL and subject to change. type ListenerFunc func(ctx context.Context, network, host, portRange string, portOffset uint, cfg net.ListenConfig) (any, error) -var networkPlugins = map[string]ListenerFunc{} -var networkPluginsMu sync.RWMutex +var ( + networkPlugins = map[string]ListenerFunc{} + networkPluginsMu sync.RWMutex +) // RegisterNetwork registers a network plugin with Caddy so that if a listener is // created for that network plugin, getListener will be invoked to get the listener. @@ -84,8 +87,10 @@ func getListenerFromPlugin(ctx context.Context, network, host, port string, port return nil, nil } -var networkHTTP3Plugins = map[string]string{} -var networkHTTP3PluginsMu sync.RWMutex +var ( + networkHTTP3Plugins = map[string]string{} + networkHTTP3PluginsMu sync.RWMutex +) // RegisterNetworkHTTP3 registers a mapping from non-HTTP/3 network to HTTP/3 // network. This should be called during init() and will panic if the network From dfe2fe20367ae72b615286309302c228c8334de0 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 31 Oct 2025 19:03:52 -0600 Subject: [PATCH 07/10] fix unix+h2c --- networks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/networks.go b/networks.go index f739bc71566..52d5a4a38c4 100644 --- a/networks.go +++ b/networks.go @@ -26,7 +26,7 @@ import ( // IsUnixNetwork returns true if the netw is a unix network. func IsUnixNetwork(netw string) bool { - return netw == "unix" || netw == "unixgram" || netw == "unixpacket" + return netw == "unix" || netw == "unixgram" || netw == "unixpacket" || netw == "unix+h2c" } // IsIpNetwork returns true if the netw is an ip network. From ac84e68c5f2b0c75681c237607fa3968fb16c3b9 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 31 Oct 2025 19:12:10 -0600 Subject: [PATCH 08/10] separate listeners_test target --- listeners_test.go | 382 ------------------------------------- listeners_test_systemd.go | 390 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 390 insertions(+), 382 deletions(-) create mode 100644 listeners_test_systemd.go diff --git a/listeners_test.go b/listeners_test.go index 7fd26fdea5a..a4cadd3aab1 100644 --- a/listeners_test.go +++ b/listeners_test.go @@ -15,9 +15,7 @@ package caddy import ( - "os" "reflect" - "strconv" "testing" "github.com/caddyserver/caddy/v2/internal" @@ -654,383 +652,3 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) { } } } - -// TestGetFdByName tests the getFdByName function for systemd socket activation. -func TestGetFdByName(t *testing.T) { - // Save original environment - originalFdNames := os.Getenv("LISTEN_FDNAMES") - originalFds := os.Getenv("LISTEN_FDS") - originalPid := os.Getenv("LISTEN_PID") - - // Restore environment after test - defer func() { - if originalFdNames != "" { - os.Setenv("LISTEN_FDNAMES", originalFdNames) - } else { - os.Unsetenv("LISTEN_FDNAMES") - } - if originalFds != "" { - os.Setenv("LISTEN_FDS", originalFds) - } else { - os.Unsetenv("LISTEN_FDS") - } - if originalPid != "" { - os.Setenv("LISTEN_PID", originalPid) - } else { - os.Unsetenv("LISTEN_PID") - } - }() - - tests := []struct { - name string - fdNames string - fds string - socketName string - expectedFd uint - expectError bool - }{ - { - name: "simple http socket", - fdNames: "http", - fds: "1", - socketName: "http", - expectedFd: 3, - }, - { - name: "multiple different sockets - first", - fdNames: "http:https:dns", - fds: "3", - socketName: "http", - expectedFd: 3, - }, - { - name: "multiple different sockets - second", - fdNames: "http:https:dns", - fds: "3", - socketName: "https", - expectedFd: 4, - }, - { - name: "multiple different sockets - third", - fdNames: "http:https:dns", - fds: "3", - socketName: "dns", - expectedFd: 5, - }, - { - name: "duplicate names - first occurrence (no index)", - fdNames: "web:web:api", - fds: "3", - socketName: "web", - expectedFd: 3, - }, - { - name: "duplicate names - first occurrence (explicit index 0)", - fdNames: "web:web:api", - fds: "3", - socketName: "web/0", - expectedFd: 3, - }, - { - name: "duplicate names - second occurrence (index 1)", - fdNames: "web:web:api", - fds: "3", - socketName: "web/1", - expectedFd: 4, - }, - { - name: "complex duplicates - first api", - fdNames: "web:api:web:api:dns", - fds: "5", - socketName: "api/0", - expectedFd: 4, - }, - { - name: "complex duplicates - second api", - fdNames: "web:api:web:api:dns", - fds: "5", - socketName: "api/1", - expectedFd: 6, - }, - { - name: "complex duplicates - first web", - fdNames: "web:api:web:api:dns", - fds: "5", - socketName: "web/0", - expectedFd: 3, - }, - { - name: "complex duplicates - second web", - fdNames: "web:api:web:api:dns", - fds: "5", - socketName: "web/1", - expectedFd: 5, - }, - { - name: "socket not found", - fdNames: "http:https", - fds: "2", - socketName: "missing", - expectError: true, - }, - { - name: "empty socket name", - fdNames: "http", - fds: "1", - socketName: "", - expectError: true, - }, - { - name: "missing LISTEN_FDNAMES", - fdNames: "", - fds: "", - socketName: "http", - expectError: true, - }, - { - name: "index out of range", - fdNames: "web:web", - fds: "2", - socketName: "web/2", - expectError: true, - }, - { - name: "negative index", - fdNames: "web", - fds: "1", - socketName: "web/-1", - expectError: true, - }, - { - name: "invalid index format", - fdNames: "web", - fds: "1", - socketName: "web/abc", - expectError: true, - }, - { - name: "too many colons", - fdNames: "web", - fds: "1", - socketName: "web/0/extra", - expectError: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // Set up environment - if tc.fdNames != "" { - os.Setenv("LISTEN_FDNAMES", tc.fdNames) - } else { - os.Unsetenv("LISTEN_FDNAMES") - } - - if tc.fds != "" { - os.Setenv("LISTEN_FDS", tc.fds) - } else { - os.Unsetenv("LISTEN_FDS") - } - - os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid())) - - // Test the function - var ( - listenFdsWithNames map[string][]uint - err error - fd uint - ) - listenFdsWithNames, err = sdListenFdsWithNames() - if err == nil { - fd, err = sdListenFd(listenFdsWithNames, tc.socketName, 0) - } - - if tc.expectError { - if err == nil { - t.Errorf("Expected error but got none") - } - } else { - if err != nil { - t.Errorf("Expected no error but got: %v", err) - } - if fd != tc.expectedFd { - t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd) - } - } - }) - } -} - -// TestParseNetworkAddressFdName tests parsing of fdname and fdgramname addresses. -func TestParseNetworkAddressFdName(t *testing.T) { - // Save and restore environment - originalFdNames := os.Getenv("LISTEN_FDNAMES") - originalFds := os.Getenv("LISTEN_FDS") - originalPid := os.Getenv("LISTEN_PID") - - defer func() { - if originalFdNames != "" { - os.Setenv("LISTEN_FDNAMES", originalFdNames) - } else { - os.Unsetenv("LISTEN_FDNAMES") - } - if originalFds != "" { - os.Setenv("LISTEN_FDS", originalFds) - } else { - os.Unsetenv("LISTEN_FDS") - } - if originalPid != "" { - os.Setenv("LISTEN_PID", originalPid) - } else { - os.Unsetenv("LISTEN_PID") - } - }() - - // Set up test environment - os.Setenv("LISTEN_FDNAMES", "http:https:dns") - os.Setenv("LISTEN_FDS", "3") - os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid())) - - tests := []struct { - input string - expectedAddr NetworkAddress - expectedFd uint - expectErr bool - }{ - { - input: "sd/http", - expectedAddr: NetworkAddress{ - Network: "sd", - Host: "http", - }, - expectedFd: 3, - }, - { - input: "sd/https", - expectedAddr: NetworkAddress{ - Network: "sd", - Host: "https", - }, - expectedFd: 4, - }, - { - input: "sd/dns", - expectedAddr: NetworkAddress{ - Network: "sd", - Host: "dns", - }, - expectedFd: 5, - }, - { - input: "sd/http/0", - expectedAddr: NetworkAddress{ - Network: "sd", - Host: "http/0", - }, - expectedFd: 3, - }, - { - input: "sd/https/0", - expectedAddr: NetworkAddress{ - Network: "sd", - Host: "https/0", - }, - expectedFd: 4, - }, - { - input: "sdgram/http", - expectedAddr: NetworkAddress{ - Network: "sdgram", - Host: "http", - }, - expectedFd: 3, - }, - { - input: "sdgram/https", - expectedAddr: NetworkAddress{ - Network: "sdgram", - Host: "https", - }, - expectedFd: 4, - }, - { - input: "sdgram/http/0", - expectedAddr: NetworkAddress{ - Network: "sdgram", - Host: "http/0", - }, - expectedFd: 3, - }, - { - input: "sd/nonexistent", - expectErr: true, - }, - { - input: "sd/nonexistent", - expectErr: true, - }, - { - input: "sd/http/99", - expectErr: true, - }, - { - input: "sd/invalid/abc", - expectErr: true, - }, - // Test that old fd/N syntax still works - { - input: "fd/7", - expectedAddr: NetworkAddress{ - Network: "fd", - Host: "7", - }, - expectedFd: 7, - }, - { - input: "fdgram/8", - expectedAddr: NetworkAddress{ - Network: "fdgram", - Host: "8", - }, - expectedFd: 8, - }, - } - - for i, tc := range tests { - actualAddr, err := ParseNetworkAddress(tc.input) - var ( - listenFdsWithNames map[string][]uint - fd uint - ) - if err == nil { - switch actualAddr.Network { - case "fd": - fallthrough - case "fdgram": - var fd64 uint64 - fd64, err = strconv.ParseUint(actualAddr.Host, 0, strconv.IntSize) - if err == nil { - fd = uint(fd64) - } - case "sd": - fallthrough - case "sdgram": - listenFdsWithNames, err = sdListenFdsWithNames() - fd, err = sdListenFd(listenFdsWithNames, actualAddr.Host, 0) - } - } - - if tc.expectErr && err == nil { - t.Errorf("Test %d (%s): Expected error but got none", i, tc.input) - } - if !tc.expectErr && err != nil { - t.Errorf("Test %d (%s): Expected no error but got: %v", i, tc.input, err) - } - if !tc.expectErr && !reflect.DeepEqual(tc.expectedAddr, actualAddr) { - t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectedAddr, actualAddr) - } - if !tc.expectErr && fd != tc.expectedFd { - t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd) - } - } -} diff --git a/listeners_test_systemd.go b/listeners_test_systemd.go new file mode 100644 index 00000000000..866e922d760 --- /dev/null +++ b/listeners_test_systemd.go @@ -0,0 +1,390 @@ +//go:build linux && !nosystemd + +package caddy + +import ( + "os" + "reflect" + "strconv" + "testing" +) + +// TestSdListenFd tests the sdListenFd function for systemd socket activation. +func TestSdListenFd(t *testing.T) { + // Save original environment + originalFdNames := os.Getenv("LISTEN_FDNAMES") + originalFds := os.Getenv("LISTEN_FDS") + originalPid := os.Getenv("LISTEN_PID") + + // Restore environment after test + defer func() { + if originalFdNames != "" { + os.Setenv("LISTEN_FDNAMES", originalFdNames) + } else { + os.Unsetenv("LISTEN_FDNAMES") + } + if originalFds != "" { + os.Setenv("LISTEN_FDS", originalFds) + } else { + os.Unsetenv("LISTEN_FDS") + } + if originalPid != "" { + os.Setenv("LISTEN_PID", originalPid) + } else { + os.Unsetenv("LISTEN_PID") + } + }() + + tests := []struct { + name string + fdNames string + fds string + socketName string + expectedFd uint + expectError bool + }{ + { + name: "simple http socket", + fdNames: "http", + fds: "1", + socketName: "http", + expectedFd: 3, + }, + { + name: "multiple different sockets - first", + fdNames: "http:https:dns", + fds: "3", + socketName: "http", + expectedFd: 3, + }, + { + name: "multiple different sockets - second", + fdNames: "http:https:dns", + fds: "3", + socketName: "https", + expectedFd: 4, + }, + { + name: "multiple different sockets - third", + fdNames: "http:https:dns", + fds: "3", + socketName: "dns", + expectedFd: 5, + }, + { + name: "duplicate names - first occurrence (no index)", + fdNames: "web:web:api", + fds: "3", + socketName: "web", + expectedFd: 3, + }, + { + name: "duplicate names - first occurrence (explicit index 0)", + fdNames: "web:web:api", + fds: "3", + socketName: "web/0", + expectedFd: 3, + }, + { + name: "duplicate names - second occurrence (index 1)", + fdNames: "web:web:api", + fds: "3", + socketName: "web/1", + expectedFd: 4, + }, + { + name: "complex duplicates - first api", + fdNames: "web:api:web:api:dns", + fds: "5", + socketName: "api/0", + expectedFd: 4, + }, + { + name: "complex duplicates - second api", + fdNames: "web:api:web:api:dns", + fds: "5", + socketName: "api/1", + expectedFd: 6, + }, + { + name: "complex duplicates - first web", + fdNames: "web:api:web:api:dns", + fds: "5", + socketName: "web/0", + expectedFd: 3, + }, + { + name: "complex duplicates - second web", + fdNames: "web:api:web:api:dns", + fds: "5", + socketName: "web/1", + expectedFd: 5, + }, + { + name: "socket not found", + fdNames: "http:https", + fds: "2", + socketName: "missing", + expectError: true, + }, + { + name: "empty socket name", + fdNames: "http", + fds: "1", + socketName: "", + expectError: true, + }, + { + name: "missing LISTEN_FDNAMES", + fdNames: "", + fds: "", + socketName: "http", + expectError: true, + }, + { + name: "index out of range", + fdNames: "web:web", + fds: "2", + socketName: "web/2", + expectError: true, + }, + { + name: "negative index", + fdNames: "web", + fds: "1", + socketName: "web/-1", + expectError: true, + }, + { + name: "invalid index format", + fdNames: "web", + fds: "1", + socketName: "web/abc", + expectError: true, + }, + { + name: "too many colons", + fdNames: "web", + fds: "1", + socketName: "web/0/extra", + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Set up environment + if tc.fdNames != "" { + os.Setenv("LISTEN_FDNAMES", tc.fdNames) + } else { + os.Unsetenv("LISTEN_FDNAMES") + } + + if tc.fds != "" { + os.Setenv("LISTEN_FDS", tc.fds) + } else { + os.Unsetenv("LISTEN_FDS") + } + + os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid())) + + // Test the function + var ( + listenFdsWithNames map[string][]uint + err error + fd uint + ) + listenFdsWithNames, err = sdListenFdsWithNames() + if err == nil { + fd, err = sdListenFd(listenFdsWithNames, tc.socketName, 0) + } + + if tc.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + if fd != tc.expectedFd { + t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd) + } + } + }) + } +} + +// TestParseNetworkAddressSd tests parsing of sd and sdgram addresses. +func TestParseNetworkAddressSd(t *testing.T) { + // Save and restore environment + originalFdNames := os.Getenv("LISTEN_FDNAMES") + originalFds := os.Getenv("LISTEN_FDS") + originalPid := os.Getenv("LISTEN_PID") + + defer func() { + if originalFdNames != "" { + os.Setenv("LISTEN_FDNAMES", originalFdNames) + } else { + os.Unsetenv("LISTEN_FDNAMES") + } + if originalFds != "" { + os.Setenv("LISTEN_FDS", originalFds) + } else { + os.Unsetenv("LISTEN_FDS") + } + if originalPid != "" { + os.Setenv("LISTEN_PID", originalPid) + } else { + os.Unsetenv("LISTEN_PID") + } + }() + + // Set up test environment + os.Setenv("LISTEN_FDNAMES", "http:https:dns") + os.Setenv("LISTEN_FDS", "3") + os.Setenv("LISTEN_PID", strconv.Itoa(os.Getpid())) + + tests := []struct { + input string + expectedAddr NetworkAddress + expectedFd uint + expectErr bool + }{ + { + input: "sd/http", + expectedAddr: NetworkAddress{ + Network: "sd", + Host: "http", + }, + expectedFd: 3, + }, + { + input: "sd/https", + expectedAddr: NetworkAddress{ + Network: "sd", + Host: "https", + }, + expectedFd: 4, + }, + { + input: "sd/dns", + expectedAddr: NetworkAddress{ + Network: "sd", + Host: "dns", + }, + expectedFd: 5, + }, + { + input: "sd/http/0", + expectedAddr: NetworkAddress{ + Network: "sd", + Host: "http/0", + }, + expectedFd: 3, + }, + { + input: "sd/https/0", + expectedAddr: NetworkAddress{ + Network: "sd", + Host: "https/0", + }, + expectedFd: 4, + }, + { + input: "sdgram/http", + expectedAddr: NetworkAddress{ + Network: "sdgram", + Host: "http", + }, + expectedFd: 3, + }, + { + input: "sdgram/https", + expectedAddr: NetworkAddress{ + Network: "sdgram", + Host: "https", + }, + expectedFd: 4, + }, + { + input: "sdgram/http/0", + expectedAddr: NetworkAddress{ + Network: "sdgram", + Host: "http/0", + }, + expectedFd: 3, + }, + { + input: "sd/nonexistent", + expectErr: true, + }, + { + input: "sd/nonexistent", + expectErr: true, + }, + { + input: "sd/http/99", + expectErr: true, + }, + { + input: "sd/invalid/abc", + expectErr: true, + }, + // Test that old fd/N syntax still works + { + input: "fd/7", + expectedAddr: NetworkAddress{ + Network: "fd", + Host: "7", + }, + expectedFd: 7, + }, + { + input: "fdgram/8", + expectedAddr: NetworkAddress{ + Network: "fdgram", + Host: "8", + }, + expectedFd: 8, + }, + } + + for i, tc := range tests { + actualAddr, err := ParseNetworkAddress(tc.input) + var ( + listenFdsWithNames map[string][]uint + fd uint + ) + if err == nil { + switch actualAddr.Network { + case "fd": + fallthrough + case "fdgram": + var fd64 uint64 + fd64, err = strconv.ParseUint(actualAddr.Host, 0, strconv.IntSize) + if err == nil { + fd = uint(fd64) + } + case "sd": + fallthrough + case "sdgram": + listenFdsWithNames, err = sdListenFdsWithNames() + fd, err = sdListenFd(listenFdsWithNames, actualAddr.Host, 0) + } + } + + if tc.expectErr && err == nil { + t.Errorf("Test %d (%s): Expected error but got none", i, tc.input) + } + if !tc.expectErr && err != nil { + t.Errorf("Test %d (%s): Expected no error but got: %v", i, tc.input, err) + } + if !tc.expectErr && !reflect.DeepEqual(tc.expectedAddr, actualAddr) { + t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectedAddr, actualAddr) + } + if !tc.expectErr && fd != tc.expectedFd { + t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd) + } + } +} From be042868d9b1a903c89954a1ceda0acb95262f49 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Fri, 31 Oct 2025 20:23:26 -0600 Subject: [PATCH 09/10] deprecate caddyhttp.RegisterNetworkHTTP3 --- modules/caddyhttp/server.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index a171bf60a30..7c095fa97a4 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -1124,3 +1124,8 @@ const ( // For tracking the real client IP (affected by trusted_proxy) ClientIPVarKey string = "client_ip" ) + +// DEPRECATED: moved to caddy.RegisterNetworkHTTP3 +func RegisterNetworkHTTP3(originalNetwork, h3Network string) { + caddy.RegisterNetworkHTTP3(originalNetwork, h3Network) +} From 5ba3608e8902ca78081116f69982e21d9beb343c Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Sat, 1 Nov 2025 17:37:06 -0600 Subject: [PATCH 10/10] getSdFd --- listeners_test_systemd.go | 8 ++-- networks_systemd.go | 85 +++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 39 deletions(-) diff --git a/listeners_test_systemd.go b/listeners_test_systemd.go index 866e922d760..b393a653f73 100644 --- a/listeners_test_systemd.go +++ b/listeners_test_systemd.go @@ -9,8 +9,8 @@ import ( "testing" ) -// TestSdListenFd tests the sdListenFd function for systemd socket activation. -func TestSdListenFd(t *testing.T) { +// TestGetSdFd tests the getSdFd function for systemd socket activation. +func TestGetSdFd(t *testing.T) { // Save original environment originalFdNames := os.Getenv("LISTEN_FDNAMES") originalFds := os.Getenv("LISTEN_FDS") @@ -196,7 +196,7 @@ func TestSdListenFd(t *testing.T) { ) listenFdsWithNames, err = sdListenFdsWithNames() if err == nil { - fd, err = sdListenFd(listenFdsWithNames, tc.socketName, 0) + fd, err = getSdFd(listenFdsWithNames, tc.socketName, 0) } if tc.expectError { @@ -370,7 +370,7 @@ func TestParseNetworkAddressSd(t *testing.T) { fallthrough case "sdgram": listenFdsWithNames, err = sdListenFdsWithNames() - fd, err = sdListenFd(listenFdsWithNames, actualAddr.Host, 0) + fd, err = getSdFd(listenFdsWithNames, actualAddr.Host, 0) } } diff --git a/networks_systemd.go b/networks_systemd.go index fd03c1bd446..d99f46c87bc 100644 --- a/networks_systemd.go +++ b/networks_systemd.go @@ -26,29 +26,38 @@ func IsReservedNetwork(network string) bool { IsSdNetwork(network) } -func sdListenFdsWithNames() (map[string][]uint, error) { - const lnFdsStart = 3 - +func sdListenFds() (int, error) { lnPid, ok := os.LookupEnv("LISTEN_PID") if !ok { - return nil, errors.New("LISTEN_PID is unset.") + return 0, errors.New("LISTEN_PID is unset.") } - pid, err := strconv.ParseUint(lnPid, 0, strconv.IntSize) + pid, err := strconv.Atoi(lnPid) if err != nil { - return nil, err + return 0, err } - if pid != uint64(os.Getpid()) { - return nil, fmt.Errorf("LISTEN_PID does not match pid: %d != %d", pid, os.Getpid()) + if pid != os.Getpid() { + return 0, fmt.Errorf("LISTEN_PID does not match pid: %d != %d", pid, os.Getpid()) } lnFds, ok := os.LookupEnv("LISTEN_FDS") if !ok { - return nil, errors.New("LISTEN_FDS is unset.") + return 0, errors.New("LISTEN_FDS is unset.") } - fds, err := strconv.ParseUint(lnFds, 0, strconv.IntSize) + fds, err := strconv.Atoi(lnFds) + if err != nil { + return 0, err + } + + return fds, nil +} + +func sdListenFdsWithNames() (map[string][]uint, error) { + const lnFdsStart = 3 + + fds, err := sdListenFds() if err != nil { return nil, err } @@ -59,7 +68,7 @@ func sdListenFdsWithNames() (map[string][]uint, error) { } fdNames := strings.Split(lnFdnames, ":") - if fds != uint64(len(fdNames)) { + if fds != len(fdNames) { return nil, fmt.Errorf("LISTEN_FDS does not match LISTEN_FDNAMES length: %d != %d", fds, len(fdNames)) } @@ -71,7 +80,7 @@ func sdListenFdsWithNames() (map[string][]uint, error) { return nameToFiles, nil } -func sdListenFd(nameToFiles map[string][]uint, host string, portOffset uint) (uint, error) { +func getSdFd(nameToFiles map[string][]uint, host string, portOffset uint) (uint, error) { name, index, li := host, portOffset, strings.LastIndex(host, "/") if li >= 0 { name = host[:li] @@ -100,40 +109,46 @@ var ( initNameToFilesMu sync.Mutex ) -func getListenerFromNetwork(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { - if IsSdNetwork(network) { +func getListenerFromSd(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { + func() { initNameToFilesMu.Lock() defer initNameToFilesMu.Unlock() if initNameToFiles == nil && initNameToFilesErr == nil { initNameToFiles, initNameToFilesErr = sdListenFdsWithNames() } + }() - if initNameToFilesErr != nil { - return nil, initNameToFilesErr - } + if initNameToFilesErr != nil { + return nil, initNameToFilesErr + } - file, err := sdListenFd(initNameToFiles, host, portOffset) - if err != nil { - return nil, err - } + file, err := getSdFd(initNameToFiles, host, portOffset) + if err != nil { + return nil, err + } - var fdNetwork string - switch network { - case "sd": - fdNetwork = "fd" - case "sdgram": - fdNetwork = "fdgram" - default: - return nil, fmt.Errorf("invalid network: %s", network) - } + var fdNetwork string + switch network { + case "sd": + fdNetwork = "fd" + case "sdgram": + fdNetwork = "fdgram" + default: + return nil, fmt.Errorf("invalid network: %s", network) + } - na, err := ParseNetworkAddress(JoinNetworkAddress(fdNetwork, strconv.FormatUint(uint64(file), 10), port)) - if err != nil { - return nil, err - } + na, err := ParseNetworkAddress(JoinNetworkAddress(fdNetwork, strconv.FormatUint(uint64(file), 10), port)) + if err != nil { + return nil, err + } - return na.Listen(ctx, portOffset, config) + return na.Listen(ctx, portOffset, config) +} + +func getListenerFromNetwork(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { + if IsSdNetwork(network) { + return getListenerFromSd(ctx, network, host, port, portOffset, config) } return getListenerFromPlugin(ctx, network, host, port, portOffset, config) }