Support systemd socket-activation

Co-authored-by: Michael <michael.matur@gmail.com>
This commit is contained in:
Julien Salleyron 2024-06-25 16:30:04 +02:00 committed by GitHub
parent 9e0800f938
commit b7de043991
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 91 additions and 20 deletions

View file

@ -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.

View file

@ -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())
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)
}

View file

@ -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{},

View file

@ -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{},

View file

@ -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]
}
}

View file

@ -0,0 +1,5 @@
//go:build windows
package server
func populateSocketActivationListeners() {}