Add support for UDP routing in systemd socket activation
This commit is contained in:
parent
95dd17e020
commit
261e4395f3
9 changed files with 158 additions and 54 deletions
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -25,19 +25,32 @@ 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() {
|
||||||
if err != nil {
|
conn, err = socketActivation.getConn(name)
|
||||||
return nil, fmt.Errorf("starting listener: %w", err)
|
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 {
|
||||||
|
return nil, fmt.Errorf("starting listener: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 := &http3server{
|
h3 := &http3server{
|
||||||
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
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 {
|
||||||
|
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.
|
||||||
|
|
|
@ -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())
|
||||||
|
|
41
pkg/server/socket_activation.go
Normal file
41
pkg/server/socket_activation.go
Normal 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()
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,6 @@
|
||||||
|
|
||||||
package server
|
package server
|
||||||
|
|
||||||
func populateSocketActivationListeners() {}
|
func populateSocketActivationListeners() *SocketActivation {
|
||||||
|
return &SocketActivation{enabled: false}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue