systemd.listen placeholder

This commit is contained in:
Aaron Paterson 2025-11-06 03:06:42 -07:00
parent 8285eba842
commit a33da3415b
6 changed files with 513 additions and 299 deletions

View file

@ -15,7 +15,6 @@
package caddy
import (
"os"
"reflect"
"testing"
@ -653,286 +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")
// Restore environment after test
defer func() {
if originalFdNames != "" {
os.Setenv("LISTEN_FDNAMES", originalFdNames)
} else {
os.Unsetenv("LISTEN_FDNAMES")
}
}()
tests := []struct {
name string
fdNames string
socketName string
expectedFd int
expectError bool
}{
{
name: "simple http socket",
fdNames: "http",
socketName: "http",
expectedFd: 3,
},
{
name: "multiple different sockets - first",
fdNames: "http:https:dns",
socketName: "http",
expectedFd: 3,
},
{
name: "multiple different sockets - second",
fdNames: "http:https:dns",
socketName: "https",
expectedFd: 4,
},
{
name: "multiple different sockets - third",
fdNames: "http:https:dns",
socketName: "dns",
expectedFd: 5,
},
{
name: "duplicate names - first occurrence (no index)",
fdNames: "web:web:api",
socketName: "web",
expectedFd: 3,
},
{
name: "duplicate names - first occurrence (explicit index 0)",
fdNames: "web:web:api",
socketName: "web:0",
expectedFd: 3,
},
{
name: "duplicate names - second occurrence (index 1)",
fdNames: "web:web:api",
socketName: "web:1",
expectedFd: 4,
},
{
name: "complex duplicates - first api",
fdNames: "web:api:web:api:dns",
socketName: "api:0",
expectedFd: 4,
},
{
name: "complex duplicates - second api",
fdNames: "web:api:web:api:dns",
socketName: "api:1",
expectedFd: 6,
},
{
name: "complex duplicates - first web",
fdNames: "web:api:web:api:dns",
socketName: "web:0",
expectedFd: 3,
},
{
name: "complex duplicates - second web",
fdNames: "web:api:web:api:dns",
socketName: "web:1",
expectedFd: 5,
},
{
name: "socket not found",
fdNames: "http:https",
socketName: "missing",
expectError: true,
},
{
name: "empty socket name",
fdNames: "http",
socketName: "",
expectError: true,
},
{
name: "missing LISTEN_FDNAMES",
fdNames: "",
socketName: "http",
expectError: true,
},
{
name: "index out of range",
fdNames: "web:web",
socketName: "web:2",
expectError: true,
},
{
name: "negative index",
fdNames: "web",
socketName: "web:-1",
expectError: true,
},
{
name: "invalid index format",
fdNames: "web",
socketName: "web:abc",
expectError: true,
},
{
name: "too many colons",
fdNames: "web",
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")
}
// Test the function
fd, err := getFdByName(tc.socketName)
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")
defer func() {
if originalFdNames != "" {
os.Setenv("LISTEN_FDNAMES", originalFdNames)
} else {
os.Unsetenv("LISTEN_FDNAMES")
}
}()
// Set up test environment
os.Setenv("LISTEN_FDNAMES", "http:https:dns")
tests := []struct {
input string
expectAddr NetworkAddress
expectErr bool
}{
{
input: "fdname/http",
expectAddr: NetworkAddress{
Network: "fd",
Host: "3",
},
},
{
input: "fdname/https",
expectAddr: NetworkAddress{
Network: "fd",
Host: "4",
},
},
{
input: "fdname/dns",
expectAddr: NetworkAddress{
Network: "fd",
Host: "5",
},
},
{
input: "fdname/http:0",
expectAddr: NetworkAddress{
Network: "fd",
Host: "3",
},
},
{
input: "fdname/https:0",
expectAddr: NetworkAddress{
Network: "fd",
Host: "4",
},
},
{
input: "fdgramname/http",
expectAddr: NetworkAddress{
Network: "fdgram",
Host: "3",
},
},
{
input: "fdgramname/https",
expectAddr: NetworkAddress{
Network: "fdgram",
Host: "4",
},
},
{
input: "fdgramname/http:0",
expectAddr: NetworkAddress{
Network: "fdgram",
Host: "3",
},
},
{
input: "fdname/nonexistent",
expectErr: true,
},
{
input: "fdgramname/nonexistent",
expectErr: true,
},
{
input: "fdname/http:99",
expectErr: true,
},
{
input: "fdname/invalid:abc",
expectErr: true,
},
// Test that old fd/N syntax still works
{
input: "fd/7",
expectAddr: NetworkAddress{
Network: "fd",
Host: "7",
},
},
{
input: "fdgram/8",
expectAddr: NetworkAddress{
Network: "fdgram",
Host: "8",
},
},
}
for i, tc := range tests {
actualAddr, err := ParseNetworkAddress(tc.input)
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.expectAddr, actualAddr) {
t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectAddr, actualAddr)
}
}
}

View file

@ -36,16 +36,12 @@ func NewReplacer() *Replacer {
static: make(map[string]any),
mapMutex: &sync.RWMutex{},
}
rep.providers = []replacementProvider{
globalDefaultReplacementProvider{},
fileReplacementProvider{},
ReplacerFunc(rep.fromStatic),
}
rep.providers = append(globalReplacementProviders, ReplacerFunc(rep.fromStatic))
return rep
}
// NewEmptyReplacer returns a new Replacer,
// without the global default replacements.
// without the global replacements.
func NewEmptyReplacer() *Replacer {
rep := &Replacer{
static: make(map[string]any),
@ -360,12 +356,11 @@ func (f fileReplacementProvider) replace(key string) (any, bool) {
return string(body), true
}
// globalDefaultReplacementProvider handles replacements
// that can be used in any context, such as system variables,
// time, or environment variables.
type globalDefaultReplacementProvider struct{}
// defaultReplacementProvider handles replacements
// such as system variables, time, or environment variables.
type defaultReplacementProvider struct{}
func (f globalDefaultReplacementProvider) replace(key string) (any, bool) {
func (f defaultReplacementProvider) replace(key string) (any, bool) {
// check environment variable
const envPrefix = "env."
if strings.HasPrefix(key, envPrefix) {

8
replacer_nosystemd.go Normal file
View file

@ -0,0 +1,8 @@
//go:build !linux || nosystemd
package caddy
var globalReplacementProviders = []replacementProvider{
defaultReplacementProvider{},
fileReplacementProvider{},
}

123
replacer_systemd.go Normal file
View file

@ -0,0 +1,123 @@
//go:build linux && !nosystemd
package caddy
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"go.uber.org/zap"
)
func sdListenFds() (int, error) {
lnPid, ok := os.LookupEnv("LISTEN_PID")
if !ok {
return 0, errors.New("LISTEN_PID is unset")
}
pid, err := strconv.Atoi(lnPid)
if err != nil {
return 0, err
}
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 0, errors.New("LISTEN_FDS is unset")
}
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
}
lnFdnames, ok := os.LookupEnv("LISTEN_FDNAMES")
if !ok {
return nil, errors.New("LISTEN_FDNAMES is unset")
}
fdNames := strings.Split(lnFdnames, ":")
if fds != len(fdNames) {
return nil, fmt.Errorf("LISTEN_FDS does not match LISTEN_FDNAMES length: %d != %d", fds, len(fdNames))
}
nameToFiles := make(map[string][]uint, len(fdNames))
for index, name := range fdNames {
nameToFiles[name] = append(nameToFiles[name], lnFdsStart+uint(index))
}
return nameToFiles, nil
}
func getSdListenFd(nameToFiles map[string][]uint, nameOffset string) (uint, error) {
index := uint(0)
name, offset, found := strings.Cut(nameOffset, ":")
if found {
off, err := strconv.ParseUint(offset, 0, strconv.IntSize)
if err != nil {
return 0, err
}
index += uint(off)
}
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, initNameToFilesErr = sdListenFdsWithNames()
// systemdReplacementProvider handles {systemd.*} replacements
type systemdReplacementProvider struct{}
func (f systemdReplacementProvider) replace(key string) (any, bool) {
// check environment variable
const systemdListenPrefix = "systemd.listen."
if strings.HasPrefix(key, systemdListenPrefix) {
if initNameToFilesErr != nil {
Log().Error("unable to read LISTEN_FDNAMES", zap.Error(initNameToFilesErr))
return nil, false
}
fd, err := getSdListenFd(initNameToFiles, key[len(systemdListenPrefix):])
if err != nil {
Log().Error("unable to process {"+key+"}", zap.Error(err))
return nil, false
}
return fd, true
}
// TODO const systemdCredsPrefix = "systemd.creds."
return nil, false
}
var globalReplacementProviders = []replacementProvider{
defaultReplacementProvider{},
fileReplacementProvider{},
systemdReplacementProvider{},
}

View file

@ -374,10 +374,6 @@ func TestReplacerMap(t *testing.T) {
func TestReplacerNew(t *testing.T) {
repl := NewReplacer()
if len(repl.providers) != 3 {
t.Errorf("Expected providers length '%v' got length '%v'", 3, len(repl.providers))
}
// test if default global replacements are added as the first provider
hostname, _ := os.Hostname()
wd, _ := os.Getwd()

376
replacer_test_systemd.go Normal file
View file

@ -0,0 +1,376 @@
//go:build linux && !nosystemd
package caddy
import (
"os"
"reflect"
"strconv"
"testing"
)
// TestGetSdListenFd tests the getSdListenFd function for systemd socket activation.
func TestGetSdListenFd(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 = getSdListenFd(listenFdsWithNames, tc.socketName)
}
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)
}
}
})
}
}
// TestParseSystemdListenPlaceholder tests parsing of {systemd.listen.name} placeholders.
func TestParseSystemdListenPlaceholder(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: "fd/{systemd.listen.http}",
expectedAddr: NetworkAddress{
Network: "fd",
Host: "{systemd.listen.http}",
},
expectedFd: 3,
},
{
input: "fd/{systemd.listen.https}",
expectedAddr: NetworkAddress{
Network: "fd",
Host: "{systemd.listen.https}",
},
expectedFd: 4,
},
{
input: "fd/{systemd.listen.dns}",
expectedAddr: NetworkAddress{
Network: "fd",
Host: "{systemd.listen.dns}",
},
expectedFd: 5,
},
{
input: "fd/{systemd.listen.http:0}",
expectedAddr: NetworkAddress{
Network: "fd",
Host: "{systemd.listen.http:0}",
},
expectedFd: 3,
},
{
input: "fd/{systemd.listen.https:0}",
expectedAddr: NetworkAddress{
Network: "fd",
Host: "{systemd.listen.https:0}",
},
expectedFd: 4,
},
{
input: "fdgram/{systemd.listen.http}",
expectedAddr: NetworkAddress{
Network: "fdgram",
Host: "{systemd.listen.http}",
},
expectedFd: 3,
},
{
input: "fdgram/{systemd.listen.https}",
expectedAddr: NetworkAddress{
Network: "fdgram",
Host: "{systemd.listen.https}",
},
expectedFd: 4,
},
{
input: "fdgram/{systemd.listen.http:0}",
expectedAddr: NetworkAddress{
Network: "fdgram",
Host: "http:0",
},
expectedFd: 3,
},
{
input: "fd/{systemd.listen.nonexistent}",
expectErr: true,
},
{
input: "fdgram/{systemd.listen.nonexistent}",
expectErr: true,
},
{
input: "fd/{systemd.listen.http:99}",
expectErr: true,
},
{
input: "fd/{systemd.listen.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)
if err == nil {
var fd uint
fdWide, err := strconv.ParseUint(actualAddr.Host, 0, strconv.IntSize)
if err == nil {
fd = uint(fdWide)
}
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)
}
}
}
}