MINOR: systemd: support for systemd socket activation via setenv-sd-listen-fds

Adds the setenv-sd-listen-fds configuration option to automatically set
or clear environment variables based on systemd's LISTEN_FDS and
LISTEN_FDNAMES. When enabled, HAProxy will expose file descriptors using
environment variable names prefixed with FD_ followed by the uppercase
FileDescriptorName defined in the systemd socket unit.

This feature simplifies socket activation in containerized environments
and allows binding to inherited file descriptors without relying on NAT
or network initialization.

Default behavior is off, which ensures the environment variables are
unset.
This commit is contained in:
William Lallemand 2025-10-19 12:51:13 +02:00
parent d48585199b
commit 61937cf2e1
4 changed files with 136 additions and 0 deletions

View File

@ -1775,6 +1775,7 @@ The following keywords are supported in the "global" section :
- set-dumpable
- set-var
- setenv
- setenv-sd-listen-fds
- ssl-default-bind-ciphers
- ssl-default-bind-ciphersuites
- ssl-default-bind-client-sigalgs
@ -3120,6 +3121,49 @@ setenv <name> <value>
the configuration file sees the new value. See also "presetenv", "resetenv",
and "unsetenv".
setenv-sd-listen-fds [ on | off ]
Sets environment variables automatically using the LISTEN_FDS and
LISTEN_FDNAMES environment variables described in the systemd documentation.
These 2 variables are set by systemd.socket. Systemd is able to listen to a
TCP socket by itself and pass the socket to the service using the
ListenStream keyword. When using FileDescriptorName, it is possible to assign
a name to the corresponding FD which will be inherited by haproxy.
When set to "on", environment variables are created with the prefix "FD_",
followed by the uppercase version of FileDescriptorName from systemd.
When set to off, the FD_ environment variables derived from
FileDescriptorName are unset.
Limitations:
- FileDescriptorName may contain alphanumerical characters and underscores.
- variables name cannot exceed 254 characters
This feature is particularly useful when running HAProxy in a container, as
it allows you to avoid constraints from NATing or start containers without
network access.
Example:
haproxy.socket:
[Socket]
ListenStream=80
FileDescriptorName=http1
ListenStream=443
FileDescriptorName=https1
haproxy.cfg:
global
setenv-sd-listen-fds on
frontend
bind fd@${FD_HTTP1}
bind fd@${FD_HTTPS1} ssl ...
Default value is off.
shm-stats-file <name> [ EXPERIMENTAL ]
When this directive is set, it enables the use of shared memory for storing
stats counters. <name> is used as argument to shm_open() to open the shared

View File

@ -7,4 +7,7 @@ int sd_notifyf(int unset_environment, const char *format, ...);
int sd_listen_fds_with_names(int unset_environment, char ***names);
int sd_listen_fds(int unset_environment);
void setenv_listen_fds();
void unsetenv_listen_fds();
#endif

View File

@ -26,6 +26,7 @@
#include <haproxy/protocol.h>
#include <haproxy/stats-file.h>
#include <haproxy/stress.h>
#include <haproxy/systemd.h>
#include <haproxy/tools.h>
int cluster_secret_isset;
@ -1594,6 +1595,33 @@ static int cfg_parse_global_env_opts(char **args, int section_type,
return 0;
}
static int cfg_parse_global_listen_fds(char **args, int section_type,
struct proxy *curpx, const struct proxy *defpx,
const char *file, int line, char **err)
{
if (strcmp(args[0], "setenv-sd-listen-fds") == 0) {
if (too_many_args(1, args, err, NULL))
return -1;
if (*(args[1]) == 0) {
memprintf(err, "'%s' expects a 'on' or 'off' argument.\n",
args[0]);
return -1;
}
if (strcmp(args[1], "on") == 0) {
setenv_listen_fds();
} else if (strcmp(args[1], "off") == 0) {
unsetenv_listen_fds();
} else {
memprintf(err, "'%s' expects 'on' or 'off' as argument.\n", args[0]);
return -1;
}
}
return 0;
}
static int cfg_parse_global_shm_stats_file(char **args, int section_type,
struct proxy *curpx, const struct proxy *defpx,
const char *file, int line, char **err)
@ -1852,6 +1880,7 @@ static struct cfg_kw_list cfg_kws = {ILH, {
{ CFG_GLOBAL, "quiet", cfg_parse_global_mode, KWF_DISCOVERY },
{ CFG_GLOBAL, "resetenv", cfg_parse_global_env_opts, KWF_DISCOVERY },
{ CFG_GLOBAL, "setenv", cfg_parse_global_env_opts, KWF_DISCOVERY },
{ CFG_GLOBAL, "setenv-sd-listen-fds", cfg_parse_global_listen_fds },
{ CFG_GLOBAL, "shm-stats-file", cfg_parse_global_shm_stats_file },
{ CFG_GLOBAL, "shm-stats-file-max-objects", cfg_parse_global_shm_stats_file_max_objects },
{ CFG_GLOBAL, "stress-level", cfg_parse_global_stress_level },

View File

@ -255,3 +255,63 @@ end:
return ret;
}
/*
* Automatically sets environment variables based on LISTEN_FDS and LISTEN_FDNAMES.
*
* When cleanup is set, unset those environment variables instead.
*/
static void __setenv_listen_fds(int cleanup)
{
char **names = NULL;
char **p;
char d[255] = "FD_";
int fd = 3;
const char *fdstr = NULL;
int i;
sd_listen_fds_with_names(0, &names);
p = names;
while (names && *names) {
char *n;
/* start to write after FD_ (i = 3) */
for (n = *names, i = 3; *n && i < sizeof(d); i++, n++) {
if (!isalnum((int)*n) && *n != '_')
break;
d[i] = toupper(*n);
}
d[i] = '\0';
if (!cleanup)
fdstr = ultoa(fd);
if (cleanup)
unsetenv(d);
else
setenv(d, fdstr, 1);
names++;
fd++;
}
/* free names allocated by sd_listen_fds_with_names() */
names = p;
while (names && *names) {
free(*names);
names++;
}
free(p);
}
/* Automatically sets environment variables based on LISTEN_FDS and LISTEN_FDNAMES. */
void setenv_listen_fds()
{
__setenv_listen_fds(0);
}
/* Automatically UNsets environment variables based on LISTEN_FDS and LISTEN_FDNAMES. */
void unsetenv_listen_fds()
{
__setenv_listen_fds(1);
}