Add support for UDP routing in systemd socket activation

This commit is contained in:
tsiid 2025-01-21 11:38:09 +03:00 committed by GitHub
parent 95dd17e020
commit 261e4395f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 158 additions and 54 deletions

View file

@ -1240,7 +1240,7 @@ entryPoints:
Traefik supports [systemd socket activation](https://www.freedesktop.org/software/systemd/man/latest/systemd-socket-activate.html). 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. When a socket activation file descriptor name matches an EntryPoint name, the corresponding file descriptor will be used as the TCP/UDP listener for the matching EntryPoint.
```bash ```bash
systemd-socket-activate -l 80 -l 443 --fdname web:websecure ./traefik --entrypoints.web --entrypoints.websecure systemd-socket-activate -l 80 -l 443 --fdname web:websecure ./traefik --entrypoints.web --entrypoints.websecure
@ -1248,16 +1248,16 @@ systemd-socket-activate -l 80 -l 443 --fdname web:websecure ./traefik --entrypo
!!! warning "EntryPoint Address" !!! warning "EntryPoint Address"
When a socket activation file descriptor name matches an EntryPoint name its address configuration is ignored. When a socket activation file descriptor name matches an EntryPoint name its address configuration is ignored. For support UDP routing, address must have /udp suffix (--entrypoints.my-udp-entrypoint.address=/udp)
!!! warning "TCP Only"
Socket activation is not yet supported with UDP entryPoints.
!!! warning "Docker Support" !!! warning "Docker Support"
Socket activation is not supported by Docker but works with Podman containers. Socket activation is not supported by Docker but works with Podman containers.
!!! warning "Multiple listeners in socket file"
Each systemd socket file must contain only one Listen directive, except in the case of HTTP/3, where the file must include both ListenStream and ListenDatagram directives. To set up TCP and UDP listeners on the same port, use multiple socket files with different entrypoints names.
## Observability Options ## Observability Options
This section is dedicated to options to control observability for an EntryPoint. This section is dedicated to options to control observability for an EntryPoint.

View file

@ -48,15 +48,8 @@ const (
var ( var (
clientConnectionStates = map[string]*connState{} clientConnectionStates = map[string]*connState{}
clientConnectionStatesMu = sync.RWMutex{} clientConnectionStatesMu = sync.RWMutex{}
socketActivationListeners map[string]net.Listener
) )
func init() {
// Populates pre-defined socketActivationListeners by socket activation.
populateSocketActivationListeners()
}
type connState struct { type connState struct {
State string State string
KeepAliveState string KeepAliveState string
@ -204,7 +197,7 @@ func NewTCPEntryPoint(ctx context.Context, name string, config *static.EntryPoin
return nil, fmt.Errorf("error preparing https server: %w", err) return nil, fmt.Errorf("error preparing https server: %w", err)
} }
h3Server, err := newHTTP3Server(ctx, config, httpsServer) h3Server, err := newHTTP3Server(ctx, name, config, httpsServer)
if err != nil { if err != nil {
return nil, fmt.Errorf("error preparing http3 server: %w", err) return nil, fmt.Errorf("error preparing http3 server: %w", err)
} }
@ -476,13 +469,14 @@ func buildListener(ctx context.Context, name string, config *static.EntryPoint)
var err error var err error
// if we have predefined listener from socket activation // if we have predefined listener from socket activation
if ln, ok := socketActivationListeners[name]; ok { if socketActivation.isEnabled() {
listener = ln listener, err = socketActivation.getListener(name)
} else { if err != nil {
if len(socketActivationListeners) > 0 { log.Ctx(ctx).Warn().Err(err).Str("name", name).Msg("Unable to use socket activation for entrypoint")
log.Warn().Str("name", name).Msg("Unable to find socket activation listener for entryPoint") }
} }
if listener == nil {
listenConfig := newListenConfig(config) listenConfig := newListenConfig(config)
listener, err = listenConfig.Listen(ctx, "tcp", config.GetAddress()) listener, err = listenConfig.Listen(ctx, "tcp", config.GetAddress())
if err != nil { if err != nil {

View file

@ -25,20 +25,33 @@ type http3server struct {
getter func(info *tls.ClientHelloInfo) (*tls.Config, error) getter func(info *tls.ClientHelloInfo) (*tls.Config, error)
} }
func newHTTP3Server(ctx context.Context, configuration *static.EntryPoint, httpsServer *httpServer) (*http3server, error) { func newHTTP3Server(ctx context.Context, name string, config *static.EntryPoint, httpsServer *httpServer) (*http3server, error) {
if configuration.HTTP3 == nil { var conn net.PacketConn
var err error
if config.HTTP3 == nil {
return nil, nil return nil, nil
} }
if configuration.HTTP3.AdvertisedPort < 0 { if config.HTTP3.AdvertisedPort < 0 {
return nil, errors.New("advertised port must be greater than or equal to zero") return nil, errors.New("advertised port must be greater than or equal to zero")
} }
listenConfig := newListenConfig(configuration) // if we have predefined connections from socket activation
conn, err := listenConfig.ListenPacket(ctx, "udp", configuration.GetAddress()) if socketActivation.isEnabled() {
conn, err = socketActivation.getConn(name)
if err != nil {
log.Ctx(ctx).Warn().Err(err).Str("name", name).Msg("Unable to use socket activation for entrypoint")
}
}
if conn == nil {
listenConfig := newListenConfig(config)
conn, err = listenConfig.ListenPacket(ctx, "udp", config.GetAddress())
if err != nil { if err != nil {
return nil, fmt.Errorf("starting listener: %w", err) return nil, fmt.Errorf("starting listener: %w", err)
} }
}
h3 := &http3server{ h3 := &http3server{
http3conn: conn, http3conn: conn,
@ -48,8 +61,8 @@ func newHTTP3Server(ctx context.Context, configuration *static.EntryPoint, https
} }
h3.Server = &http3.Server{ h3.Server = &http3.Server{
Addr: configuration.GetAddress(), Addr: config.GetAddress(),
Port: configuration.HTTP3.AdvertisedPort, Port: config.HTTP3.AdvertisedPort,
Handler: httpsServer.Server.(*http.Server).Handler, Handler: httpsServer.Server.(*http.Server).Handler,
TLSConfig: &tls.Config{GetConfigForClient: h3.getGetConfigForClient}, TLSConfig: &tls.Config{GetConfigForClient: h3.getGetConfigForClient},
QUICConfig: &quic.Config{ QUICConfig: &quic.Config{

View file

@ -16,9 +16,9 @@ import (
type UDPEntryPoints map[string]*UDPEntryPoint type UDPEntryPoints map[string]*UDPEntryPoint
// NewUDPEntryPoints returns all the UDP entry points, keyed by name. // NewUDPEntryPoints returns all the UDP entry points, keyed by name.
func NewUDPEntryPoints(cfg static.EntryPoints) (UDPEntryPoints, error) { func NewUDPEntryPoints(config static.EntryPoints) (UDPEntryPoints, error) {
entryPoints := make(UDPEntryPoints) entryPoints := make(UDPEntryPoints)
for entryPointName, entryPoint := range cfg { for entryPointName, entryPoint := range config {
protocol, err := entryPoint.GetProtocol() protocol, err := entryPoint.GetProtocol()
if err != nil { if err != nil {
return nil, fmt.Errorf("error while building entryPoint %s: %w", entryPointName, err) return nil, fmt.Errorf("error while building entryPoint %s: %w", entryPointName, err)
@ -28,7 +28,7 @@ func NewUDPEntryPoints(cfg static.EntryPoints) (UDPEntryPoints, error) {
continue continue
} }
ep, err := NewUDPEntryPoint(entryPoint) ep, err := NewUDPEntryPoint(entryPoint, entryPointName)
if err != nil { if err != nil {
return nil, fmt.Errorf("error while building entryPoint %s: %w", entryPointName, err) return nil, fmt.Errorf("error while building entryPoint %s: %w", entryPointName, err)
} }
@ -85,14 +85,33 @@ type UDPEntryPoint struct {
} }
// NewUDPEntryPoint returns a UDP entry point. // NewUDPEntryPoint returns a UDP entry point.
func NewUDPEntryPoint(cfg *static.EntryPoint) (*UDPEntryPoint, error) { func NewUDPEntryPoint(config *static.EntryPoint, name string) (*UDPEntryPoint, error) {
listenConfig := newListenConfig(cfg) var listener *udp.Listener
listener, err := udp.Listen(listenConfig, "udp", cfg.GetAddress(), time.Duration(cfg.UDP.Timeout)) var err error
timeout := time.Duration(config.UDP.Timeout)
// if we have predefined connections from socket activation
if socketActivation.isEnabled() {
if conn, err := socketActivation.getConn(name); err == nil {
listener, err = udp.ListenPacketConn(conn, timeout)
if err != nil { if err != nil {
return nil, err log.Warn().Err(err).Str("name", name).Msg("Unable to create socket activation listener")
}
} else {
log.Warn().Err(err).Str("name", name).Msg("Unable to use socket activation for entrypoint")
}
} }
return &UDPEntryPoint{listener: listener, switcher: &udp.HandlerSwitcher{}, transportConfiguration: cfg.Transport}, nil if listener == nil {
listenConfig := newListenConfig(config)
listener, err = udp.Listen(listenConfig, "udp", config.GetAddress(), timeout)
if err != nil {
return nil, fmt.Errorf("error creating listener: %w", err)
}
}
return &UDPEntryPoint{listener: listener, switcher: &udp.HandlerSwitcher{}, transportConfiguration: config.Transport}, nil
} }
// Start commences the listening for ep. // Start commences the listening for ep.

View file

@ -24,7 +24,7 @@ func TestShutdownUDPConn(t *testing.T) {
} }
ep.SetDefaults() ep.SetDefaults()
entryPoint, err := NewUDPEntryPoint(&ep) entryPoint, err := NewUDPEntryPoint(&ep, "")
require.NoError(t, err) require.NoError(t, err)
go entryPoint.Start(context.Background()) go entryPoint.Start(context.Background())

View file

@ -0,0 +1,41 @@
package server
import (
"errors"
"net"
)
type SocketActivation struct {
enabled bool
listeners map[string]net.Listener
conns map[string]net.PacketConn
}
func (s *SocketActivation) isEnabled() bool {
return s.enabled
}
func (s *SocketActivation) getListener(name string) (net.Listener, error) {
listener, ok := s.listeners[name]
if !ok {
return nil, errors.New("unable to find socket activation TCP listener for entryPoint")
}
return listener, nil
}
func (s *SocketActivation) getConn(name string) (net.PacketConn, error) {
conn, ok := s.conns[name]
if !ok {
return nil, errors.New("unable to find socket activation UDP listener for entryPoint")
}
return conn, nil
}
var socketActivation *SocketActivation
func init() {
// Populates pre-defined TCP and UDP listeners provided by systemd socket activation.
socketActivation = populateSocketActivationListeners()
}

View file

@ -9,16 +9,36 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func populateSocketActivationListeners() { func populateSocketActivationListeners() *SocketActivation {
listenersWithName, _ := activation.ListenersWithNames() // We use Files api due to activation not providing method for get PacketConn with names
files := activation.Files(true)
sa := &SocketActivation{enabled: false}
sa.listeners = make(map[string]net.Listener)
sa.conns = make(map[string]net.PacketConn)
socketActivationListeners = make(map[string]net.Listener) if len(files) > 0 {
for name, lns := range listenersWithName { sa.enabled = true
if len(lns) != 1 {
log.Error().Str("listenersName", name).Msg("Socket activation listeners must have one and only one listener per name") for _, f := range files {
continue if lc, err := net.FileListener(f); err == nil {
_, ok := sa.listeners[f.Name()]
if ok {
log.Error().Str("listenersName", f.Name()).Msg("Socket activation TCP listeners must have one and only one listener per name")
} else {
sa.listeners[f.Name()] = lc
}
f.Close()
} else if pc, err := net.FilePacketConn(f); err == nil {
_, ok := sa.conns[f.Name()]
if ok {
log.Error().Str("listenersName", f.Name()).Msg("Socket activation UDP listeners must have one and only one listener per name")
} else {
sa.conns[f.Name()] = pc
}
f.Close()
}
}
} }
socketActivationListeners[name] = lns[0] return sa
}
} }

View file

@ -2,4 +2,6 @@
package server package server
func populateSocketActivationListeners() {} func populateSocketActivationListeners() *SocketActivation {
return &SocketActivation{enabled: false}
}

View file

@ -34,16 +34,12 @@ type Listener struct {
timeout time.Duration timeout time.Duration
} }
// Listen creates a new listener. // Creates a new listener from PacketConn.
func Listen(listenConfig net.ListenConfig, network, address string, timeout time.Duration) (*Listener, error) { func ListenPacketConn(packetConn net.PacketConn, timeout time.Duration) (*Listener, error) {
if timeout <= 0 { if timeout <= 0 {
return nil, errors.New("timeout should be greater than zero") return nil, errors.New("timeout should be greater than zero")
} }
packetConn, err := listenConfig.ListenPacket(context.Background(), network, address)
if err != nil {
return nil, fmt.Errorf("listen packet: %w", err)
}
pConn, ok := packetConn.(*net.UDPConn) pConn, ok := packetConn.(*net.UDPConn)
if !ok { if !ok {
return nil, errors.New("packet conn is not an UDPConn") return nil, errors.New("packet conn is not an UDPConn")
@ -62,6 +58,25 @@ func Listen(listenConfig net.ListenConfig, network, address string, timeout time
return l, nil return l, nil
} }
// Listen creates a new listener.
func Listen(listenConfig net.ListenConfig, network, address string, timeout time.Duration) (*Listener, error) {
if timeout <= 0 {
return nil, errors.New("timeout should be greater than zero")
}
packetConn, err := listenConfig.ListenPacket(context.Background(), network, address)
if err != nil {
return nil, fmt.Errorf("listen packet: %w", err)
}
l, err := ListenPacketConn(packetConn, timeout)
if err != nil {
return nil, fmt.Errorf("listen packet conn: %w", err)
}
return l, nil
}
// Accept waits for and returns the next connection to the listener. // Accept waits for and returns the next connection to the listener.
func (l *Listener) Accept() (*Conn, error) { func (l *Listener) Accept() (*Conn, error) {
c := <-l.acceptCh c := <-l.acceptCh