From b7de0439914a9d6f7598da77aa25f72858f8d5f9 Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Tue, 25 Jun 2024 16:30:04 +0200 Subject: [PATCH] Support systemd socket-activation Co-authored-by: Michael --- docs/content/routing/entrypoints.md | 22 +++++++++ pkg/server/server_entrypoint_tcp.go | 48 +++++++++++++------ .../server_entrypoint_tcp_http3_test.go | 2 +- pkg/server/server_entrypoint_tcp_test.go | 10 ++-- pkg/server/socket_activation_unix.go | 24 ++++++++++ pkg/server/socket_activation_windows.go | 5 ++ 6 files changed, 91 insertions(+), 20 deletions(-) create mode 100644 pkg/server/socket_activation_unix.go create mode 100644 pkg/server/socket_activation_windows.go diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index 0678e9a1d..4b4c9e57a 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 6a25953d4..248c3eb21 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/server_entrypoint_tcp_http3_test.go b/pkg/server/server_entrypoint_tcp_http3_test.go index 66aa48ce4..6e62892ae 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 9ec5d2686..a8f5719d7 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{}, diff --git a/pkg/server/socket_activation_unix.go b/pkg/server/socket_activation_unix.go new file mode 100644 index 000000000..450330981 --- /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 000000000..62b297be6 --- /dev/null +++ b/pkg/server/socket_activation_windows.go @@ -0,0 +1,5 @@ +//go:build windows + +package server + +func populateSocketActivationListeners() {}