Repository: traefik/traefik (language: go)

Issue:
Add systemd socket activation support
**Description**

The systemd project supports [socket-activation](https://www.freedesktop.org/software/systemd/man/systemd-socket-activate.html), for services allowing systemd to listen on the socket initially. When the first connection comes in, systemd starts the service and passes in any listening sockets as file descriptors. 

This technique is also useful for containers, for example when running non-root podman it uses netavark for the network stack. This has the disadvantage that container processes are unable to see the real source ip of incoming connections. One solution can be to use [podman socket_activation](https://github.com/containers/podman/blob/main/docs/tutorials/socket_activation.md).  Currently socket_activation doesn't seem to be supported for traefik. 

**What I have tried**

`systemd-socket-activate -l 80 -l 443 podman run --rm docker.io/library/traefik:latest `

Here i would expect traefik to use this sockets when configured to use port 80, 443. Using the same pattern for httpd just works. 




Discussion / hints:
Hey @lalbers!

Thanks for your suggestion.
We are interested in this issue but are unsure about the use case and the traction it will receive, so we are going to leave the status as "kind/proposal" to give the community time to let us know that they would like this.
We will reevaluate as people respond.

I love to see this feature to be added to Traefik
I have a use case for this, too. I am trying to run traefik in a **rootless podman** stack with crowdsec. With the rootlesskit network mode the real IP address of clients is not preserved. With slirp4netns:porthandler=slirp4netns it is, but there is no name resolution, a performance hit and network separation is not possible (no user generated networks with this mode). Socket activation would be a nice feature to work arround the limitation. 
That being said, the issue is known and since the new network mode "pasta" is able to preserve the real IP address while using rootlesskit to keep performance up, this might be resolved when pasta becomes the default network mode (I think it's on the roadmap for podman 6).
Pasta is indeed a lot faster than slirp4netns, but from my experience slightly unstable (but that might improve in the future) and still slower than "native" performance (which socket activation should have). Since Traefik is a reverse proxy and users will most likely be routing almost all of their traffic through it I think it would still be great to have this feature.
also, pasta network mode cannot make traefik docker provider discover the container IP
any updates? socket activation is really a good need for rootless podman.

Fix patch:
diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md
index 0678e9a1d7..4b4c9e57aa 100644
--- a/docs/content/routing/entrypoints.md
+++ b/docs/content/routing/entrypoints.md
@@ -1175,3 +1175,25 @@ entryPoints:
 ```
 
 {!traefik-for-business-applications.md!}
+
+## Systemd Socket Activation
+
+Traefik supports [systemd socket activation](https://www.freedesktop.org/software/systemd/man/latest/systemd-socket-activate.html).
+
+When a socket activation file descriptor name matches an EntryPoint name, the corresponding file descriptor will be used as the TCP listener for the matching EntryPoint.
+
+```bash
+systemd-socket-activate -l 80 -l 443 --fdname web:websecure  ./traefik --entrypoints.web --entrypoints.websecure
+```
+
+!!! warning "EntryPoint Address"
+
+    When a socket activation file descriptor name matches an EntryPoint name its address configuration is ignored.     
+
+!!! warning "TCP Only"
+
+    Socket activation is not yet supported with UDP entryPoints.
+
+!!! warning "Docker Support"
+
+    Socket activation is not supported by Docker but works with Podman containers.
diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go
index 6a25953d4c..248c3eb21b 100644
--- a/pkg/server/server_entrypoint_tcp.go
+++ b/pkg/server/server_entrypoint_tcp.go
@@ -48,8 +48,15 @@ const (
 var (
 	clientConnectionStates   = map[string]*connState{}
 	clientConnectionStatesMu = sync.RWMutex{}
+
+	socketActivationListeners map[string]net.Listener
 )
 
+func init() {
+	// Populates pre-defined socketActivationListeners by socket activation.
+	populateSocketActivationListeners()
+}
+
 type connState struct {
 	State            string
 	KeepAliveState   string
@@ -96,6 +103,7 @@ func NewTCPEntryPoints(entryPointsConfig static.EntryPoints, hostResolverConfig
 			return clientConnectionStates
 		}))
 	}
+
 	serverEntryPointsTCP := make(TCPEntryPoints)
 	for entryPointName, config := range entryPointsConfig {
 		protocol, err := config.GetProtocol()
@@ -113,7 +121,7 @@ func NewTCPEntryPoints(entryPointsConfig static.EntryPoints, hostResolverConfig
 			OpenConnectionsGauge().
 			With("entrypoint", entryPointName, "protocol", "TCP")
 
-		serverEntryPointsTCP[entryPointName], err = NewTCPEntryPoint(ctx, config, hostResolverConfig, openConnectionsGauge)
+		serverEntryPointsTCP[entryPointName], err = NewTCPEntryPoint(ctx, entryPointName, config, hostResolverConfig, openConnectionsGauge)
 		if err != nil {
 			return nil, fmt.Errorf("error while building entryPoint %s: %w", entryPointName, err)
 		}
@@ -169,10 +177,10 @@ type TCPEntryPoint struct {
 }
 
 // NewTCPEntryPoint creates a new TCPEntryPoint.
-func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hostResolverConfig *types.HostResolverConfig, openConnectionsGauge gokitmetrics.Gauge) (*TCPEntryPoint, error) {
+func NewTCPEntryPoint(ctx context.Context, name string, config *static.EntryPoint, hostResolverConfig *types.HostResolverConfig, openConnectionsGauge gokitmetrics.Gauge) (*TCPEntryPoint, error) {
 	tracker := newConnectionTracker(openConnectionsGauge)
 
-	listener, err := buildListener(ctx, configuration)
+	listener, err := buildListener(ctx, name, config)
 	if err != nil {
 		return nil, fmt.Errorf("error preparing server: %w", err)
 	}
@@ -181,19 +189,19 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
 
 	reqDecorator := requestdecorator.New(hostResolverConfig)
 
-	httpServer, err := createHTTPServer(ctx, listener, configuration, true, reqDecorator)
+	httpServer, err := createHTTPServer(ctx, listener, config, true, reqDecorator)
 	if err != nil {
 		return nil, fmt.Errorf("error preparing http server: %w", err)
 	}
 
 	rt.SetHTTPForwarder(httpServer.Forwarder)
 
-	httpsServer, err := createHTTPServer(ctx, listener, configuration, false, reqDecorator)
+	httpsServer, err := createHTTPServer(ctx, listener, config, false, reqDecorator)
 	if err != nil {
 		return nil, fmt.Errorf("error preparing https server: %w", err)
 	}
 
-	h3Server, err := newHTTP3Server(ctx, configuration, httpsServer)
+	h3Server, err := newHTTP3Server(ctx, config, httpsServer)
 	if err != nil {
 		return nil, fmt.Errorf("error preparing http3 server: %w", err)
 	}
@@ -206,7 +214,7 @@ func NewTCPEntryPoint(ctx context.Context, configuration *static.EntryPoint, hos
 	return &TCPEntryPoint{
 		listener:               listener,
 		switcher:               tcpSwitcher,
-		transportConfiguration: configuration.Transport,
+		transportConfiguration: config.Transport,
 		tracker:                tracker,
 		httpServer:             httpServer,
 		httpsServer:            httpsServer,
@@ -460,17 +468,29 @@ func buildProxyProtocolListener(ctx context.Context, entryPoint *static.EntryPoi
 	return proxyListener, nil
 }
 
-func buildListener(ctx context.Context, entryPoint *static.EntryPoint) (net.Listener, error) {
-	listenConfig := newListenConfig(entryPoint)
-	listener, err := listenConfig.Listen(ctx, "tcp", entryPoint.GetAddress())
-	if err != nil {
-		return nil, fmt.Errorf("error opening listener: %w", err)
+func buildListener(ctx context.Context, name string, config *static.EntryPoint) (net.Listener, error) {
+	var listener net.Listener
+	var err error
+
+	// if we have predefined listener from socket activation
+	if ln, ok := socketActivationListeners[name]; ok {
+		listener = ln
+	} else {
+		if len(socketActivationListeners) > 0 {
+			log.Warn().Str("name", name).Msg("Unable to find socket activation listener for entryPoint")
+		}
+
+		listenConfig := newListenConfig(config)
+		listener, err = listenConfig.Listen(ctx, "tcp", config.GetAddress())
+		if err != nil {
+			return nil, fmt.Errorf("error opening listener: %w", err)
+		}
 	}
 
 	listener = tcpKeepAliveListener{listener.(*net.TCPListener)}
 
-	if entryPoint.ProxyProtocol != nil {
-		listener, err = buildProxyProtocolListener(ctx, entryPoint, listener)
+	if config.ProxyProtocol != nil {
+		listener, err = buildProxyProtocolListener(ctx, config, listener)
 		if err != nil {
 			return nil, fmt.Errorf("error creating proxy protocol listener: %w", err)
 		}
diff --git a/pkg/server/socket_activation_unix.go b/pkg/server/socket_activation_unix.go
new file mode 100644
index 0000000000..450330981e
--- /dev/null
+++ b/pkg/server/socket_activation_unix.go
@@ -0,0 +1,24 @@
+//go:build !windows
+
+package server
+
+import (
+	"net"
+
+	"github.com/coreos/go-systemd/activation"
+	"github.com/rs/zerolog/log"
+)
+
+func populateSocketActivationListeners() {
+	listenersWithName, _ := activation.ListenersWithNames()
+
+	socketActivationListeners = make(map[string]net.Listener)
+	for name, lns := range listenersWithName {
+		if len(lns) != 1 {
+			log.Error().Str("listenersName", name).Msg("Socket activation listeners must have one and only one listener per name")
+			continue
+		}
+
+		socketActivationListeners[name] = lns[0]
+	}
+}
diff --git a/pkg/server/socket_activation_windows.go b/pkg/server/socket_activation_windows.go
new file mode 100644
index 0000000000..62b297be6f
--- /dev/null
+++ b/pkg/server/socket_activation_windows.go
@@ -0,0 +1,5 @@
+//go:build windows
+
+package server
+
+func populateSocketActivationListeners() {}


Test patch:
diff --git a/pkg/server/server_entrypoint_tcp_http3_test.go b/pkg/server/server_entrypoint_tcp_http3_test.go
index 66aa48ce46..6e62892ae3 100644
--- a/pkg/server/server_entrypoint_tcp_http3_test.go
+++ b/pkg/server/server_entrypoint_tcp_http3_test.go
@@ -85,7 +85,7 @@ func TestHTTP3AdvertisedPort(t *testing.T) {
 	epConfig := &static.EntryPointsTransport{}
 	epConfig.SetDefaults()
 
-	entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
+	entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
 		Address:          "127.0.0.1:8090",
 		Transport:        epConfig,
 		ForwardedHeaders: &static.ForwardedHeaders{},
diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go
index 9ec5d2686b..a8f5719d70 100644
--- a/pkg/server/server_entrypoint_tcp_test.go
+++ b/pkg/server/server_entrypoint_tcp_test.go
@@ -72,7 +72,7 @@ func testShutdown(t *testing.T, router *tcprouter.Router) {
 	epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(5 * time.Second)
 	epConfig.RespondingTimeouts.WriteTimeout = ptypes.Duration(5 * time.Second)
 
-	entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
+	entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
 		// We explicitly use an IPV4 address because on Alpine, with an IPV6 address
 		// there seems to be shenanigans related to properly cleaning up file descriptors
 		Address:          "127.0.0.1:0",
@@ -159,7 +159,7 @@ func TestReadTimeoutWithoutFirstByte(t *testing.T) {
 	epConfig.SetDefaults()
 	epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(2 * time.Second)
 
-	entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
+	entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
 		Address:          ":0",
 		Transport:        epConfig,
 		ForwardedHeaders: &static.ForwardedHeaders{},
@@ -196,7 +196,7 @@ func TestReadTimeoutWithFirstByte(t *testing.T) {
 	epConfig.SetDefaults()
 	epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(2 * time.Second)
 
-	entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
+	entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
 		Address:          ":0",
 		Transport:        epConfig,
 		ForwardedHeaders: &static.ForwardedHeaders{},
@@ -236,7 +236,7 @@ func TestKeepAliveMaxRequests(t *testing.T) {
 	epConfig.SetDefaults()
 	epConfig.KeepAliveMaxRequests = 3
 
-	entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
+	entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
 		Address:          ":0",
 		Transport:        epConfig,
 		ForwardedHeaders: &static.ForwardedHeaders{},
@@ -282,7 +282,7 @@ func TestKeepAliveMaxTime(t *testing.T) {
 	epConfig.SetDefaults()
 	epConfig.KeepAliveMaxTime = ptypes.Duration(time.Millisecond)
 
-	entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
+	entryPoint, err := NewTCPEntryPoint(context.Background(), "", &static.EntryPoint{
 		Address:          ":0",
 		Transport:        epConfig,
 		ForwardedHeaders: &static.ForwardedHeaders{},