From 968493a8f26e79c6bd755c80349702ccd6b0a9c1 Mon Sep 17 00:00:00 2001 From: vnxme <46669194+vnxme@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:08:59 +0300 Subject: [PATCH 1/2] packet_conn_wrappers: Initial changes --- caddyconfig/httpcaddyfile/httptype.go | 14 ++++++ caddyconfig/httpcaddyfile/serveroptions.go | 56 +++++++++++++++------- listeners.go | 20 +++++++- modules/caddyhttp/app.go | 14 ++++++ modules/caddyhttp/server.go | 9 +++- 5 files changed, 93 insertions(+), 20 deletions(-) diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 3dcd3ea5b62..49cf404975d 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -851,6 +851,20 @@ func (st *ServerType) serversFromPairings( srv.ListenerWrappersRaw = append(srv.ListenerWrappersRaw, jsonListenerWrapper) } + // Look for any config values that provide packet conn wrappers on the server block + for _, listenerConfig := range sblock.pile["packet_conn_wrapper"] { + packetConnWrapper, ok := listenerConfig.Value.(caddy.PacketConnWrapper) + if !ok { + return nil, fmt.Errorf("config for a packet conn wrapper did not provide a value that implements caddy.PacketConnWrapper") + } + jsonPacketConnWrapper := caddyconfig.JSONModuleObject( + packetConnWrapper, + "wrapper", + packetConnWrapper.(caddy.Module).CaddyModule().ID.Name(), + warnings) + srv.PacketConnWrappersRaw = append(srv.PacketConnWrappersRaw, jsonPacketConnWrapper) + } + // set up each handler directive, making sure to honor directive order dirRoutes := sblock.pile["route"] siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true) diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go index d60ce51a94c..bfe4a54504a 100644 --- a/caddyconfig/httpcaddyfile/serveroptions.go +++ b/caddyconfig/httpcaddyfile/serveroptions.go @@ -35,23 +35,24 @@ type serverOptions struct { ListenerAddress string // These will all map 1:1 to the caddyhttp.Server struct - Name string - ListenerWrappersRaw []json.RawMessage - ReadTimeout caddy.Duration - ReadHeaderTimeout caddy.Duration - WriteTimeout caddy.Duration - IdleTimeout caddy.Duration - KeepAliveInterval caddy.Duration - MaxHeaderBytes int - EnableFullDuplex bool - Protocols []string - StrictSNIHost *bool - TrustedProxiesRaw json.RawMessage - TrustedProxiesStrict int - ClientIPHeaders []string - ShouldLogCredentials bool - Metrics *caddyhttp.Metrics - Trace bool // TODO: EXPERIMENTAL + Name string + ListenerWrappersRaw []json.RawMessage + PacketConnWrappersRaw []json.RawMessage + ReadTimeout caddy.Duration + ReadHeaderTimeout caddy.Duration + WriteTimeout caddy.Duration + IdleTimeout caddy.Duration + KeepAliveInterval caddy.Duration + MaxHeaderBytes int + EnableFullDuplex bool + Protocols []string + StrictSNIHost *bool + TrustedProxiesRaw json.RawMessage + TrustedProxiesStrict int + ClientIPHeaders []string + ShouldLogCredentials bool + Metrics *caddyhttp.Metrics + Trace bool // TODO: EXPERIMENTAL } func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { @@ -95,6 +96,26 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper) } + case "packet_conn_wrappers": + for nesting := d.Nesting(); d.NextBlock(nesting); { + modID := "caddy.packetconns." + d.Val() + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return nil, err + } + packetConnWrapper, ok := unm.(caddy.PacketConnWrapper) + if !ok { + return nil, fmt.Errorf("module %s (%T) is not a packet conn wrapper", modID, unm) + } + jsonPacketConnWrapper := caddyconfig.JSONModuleObject( + packetConnWrapper, + "wrapper", + packetConnWrapper.(caddy.Module).CaddyModule().ID.Name(), + nil, + ) + serverOpts.PacketConnWrappersRaw = append(serverOpts.PacketConnWrappersRaw, jsonPacketConnWrapper) + } + case "timeouts": for nesting := d.Nesting(); d.NextBlock(nesting); { switch d.Val() { @@ -304,6 +325,7 @@ func applyServerOptions( // set all the options server.ListenerWrappersRaw = opts.ListenerWrappersRaw + server.PacketConnWrappersRaw = opts.PacketConnWrappersRaw server.ReadTimeout = opts.ReadTimeout server.ReadHeaderTimeout = opts.ReadHeaderTimeout server.WriteTimeout = opts.WriteTimeout diff --git a/listeners.go b/listeners.go index 01adc615d0c..949ed9c53ee 100644 --- a/listeners.go +++ b/listeners.go @@ -431,7 +431,7 @@ func JoinNetworkAddress(network, host, port string) string { // // NOTE: This API is EXPERIMENTAL and may be changed or removed. // NOTE: user should close the returned listener twice, once to stop accepting new connections, the second time to free up the packet conn. -func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config) (http3.QUICListener, error) { +func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config, pcWrappers []PacketConnWrapper) (http3.QUICListener, error) { lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset)) sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { @@ -452,6 +452,11 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config } } + // wrap packet conn before QUIC + for _, pcWrapper := range pcWrappers { + h3ln = pcWrapper.WrapPacketConn(h3ln) + } + sqs := newSharedQUICState(tlsConf) // http3.ConfigureTLSConfig only uses this field and tls App sets this field as well //nolint:gosec @@ -695,6 +700,19 @@ type ListenerWrapper interface { WrapListener(net.Listener) net.Listener } +// PacketConnWrapper is a type that wraps a packet conn +// so it can modify the input packet conn methods. +// Modules that implement this interface are found +// in the caddy.packetconns namespace. Usually, to +// wrap a packet conn, you will define your own struct +// type that embeds the input packet conn, then +// implement your own methods that you want to wrap, +// calling the underlying packet conn methods where +// appropriate. +type PacketConnWrapper interface { + WrapPacketConn(net.PacketConn) net.PacketConn +} + // listenerPool stores and allows reuse of active listeners. var listenerPool = NewUsagePool() diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index afa6cd0f075..08502889ecf 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -360,6 +360,20 @@ func (app *App) Provision(ctx caddy.Context) error { srv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...) } } + + // set up each packet conn modifier + if srv.PacketConnWrappersRaw != nil { + vals, err := ctx.LoadModule(srv, "PacketConnWrappersRaw") + if err != nil { + return fmt.Errorf("loading packet conn wrapper modules: %v", err) + } + // if any wrappers were configured, they come before the QUIC handshake; + // unlike TLS above, there is no QUIC placeholder + for _, val := range vals.([]any) { + srv.packetConnWrappers = append(srv.packetConnWrappers, val.(caddy.PacketConnWrapper)) + } + } + // pre-compile the primary handler chain, and be sure to wrap it in our // route handler so that important security checks are done, etc. primaryRoute := emptyHandler diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 069807b2251..df9a6f0fde0 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -55,6 +55,10 @@ type Server struct { // of the base listener. They are applied in the given order. ListenerWrappersRaw []json.RawMessage `json:"listener_wrappers,omitempty" caddy:"namespace=caddy.listeners inline_key=wrapper"` + // A list of packet conn wrapper modules, which can modify the behavior + // of the base packet conn. They are applied in the given order. + PacketConnWrappersRaw []json.RawMessage `json:"packet_conn_wrappers,omitempty" caddy:"namespace=caddy.packetconns inline_key=wrapper"` + // How long to allow a read from a client's upload. Setting this // to a short, non-zero value can mitigate slowloris attacks, but // may also affect legitimately slow clients. @@ -235,7 +239,8 @@ type Server struct { primaryHandlerChain Handler errorHandlerChain Handler listenerWrappers []caddy.ListenerWrapper - listeners []net.Listener // stdlib http.Server will close these + packetConnWrappers []caddy.PacketConnWrapper + listeners []net.Listener quicListeners []http3.QUICListener // http3 now leave the quic.Listener management to us tlsApp *caddytls.TLS @@ -608,7 +613,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err) } addr.Network = h3net - h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg) + h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg, s.packetConnWrappers) if err != nil { return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err) } From 985e05d95dee6281fe4084aed066fec03bd9c6d0 Mon Sep 17 00:00:00 2001 From: vnxme <46669194+vnxme@users.noreply.github.com> Date: Wed, 13 Aug 2025 18:08:55 +0300 Subject: [PATCH 2/2] packet_conn_wrappers: Unwrap a packet conn only if there are no wrappers --- listeners.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/listeners.go b/listeners.go index 949ed9c53ee..32ca8399949 100644 --- a/listeners.go +++ b/listeners.go @@ -443,18 +443,20 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config ln := lnAny.(net.PacketConn) h3ln := ln - for { - // retrieve the underlying socket, so quic-go can optimize. - if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok { - h3ln = unwrapper.Unwrap() - } else { - break + if len(pcWrappers) == 0 { + for { + // retrieve the underlying socket, so quic-go can optimize. + if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok { + h3ln = unwrapper.Unwrap() + } else { + break + } + } + } else { + // wrap packet conn before QUIC + for _, pcWrapper := range pcWrappers { + h3ln = pcWrapper.WrapPacketConn(h3ln) } - } - - // wrap packet conn before QUIC - for _, pcWrapper := range pcWrappers { - h3ln = pcWrapper.WrapPacketConn(h3ln) } sqs := newSharedQUICState(tlsConf)