mirror of
https://github.com/coredns/coredns.git
synced 2026-04-06 04:05:33 -04:00
proxyproto: add UDP session tracking for Spectrum PPv2 (#7967)
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
|
||||
"github.com/hashicorp/golang-lru/v2/expirable"
|
||||
"github.com/pires/go-proxyproto"
|
||||
)
|
||||
|
||||
@@ -18,11 +19,49 @@ var (
|
||||
_ net.Addr = (*Addr)(nil)
|
||||
)
|
||||
|
||||
// errHeaderOnly is a sentinel used internally to signal that the datagram
|
||||
// contained only a PROXY Protocol header with no DNS payload. It is never
|
||||
// returned to callers of ReadFrom.
|
||||
var errHeaderOnly = errors.New("header-only datagram; no payload")
|
||||
|
||||
// PacketConn wraps a net.PacketConn and strips PROXY Protocol v2 headers from
|
||||
// incoming UDP datagrams.
|
||||
//
|
||||
// When UDPSessionTrackingTTL is greater than zero the connection implements
|
||||
// Cloudflare Spectrum's PPv2-over-UDP behavior: the PROXY header arrives in
|
||||
// the very first datagram of a session (which may carries an empty payload)
|
||||
// while all subsequent datagrams carry real DNS payload without any header.
|
||||
// The real source address parsed from the first datagram is cached keyed by
|
||||
// the Spectrum-side remote address and applied to every headerless datagram
|
||||
// that arrives from the same remote address within UDPSessionTrackingTTL.
|
||||
//
|
||||
// The session cache is a fixed-capacity LRU (capped at udpSessionMaxEntries)
|
||||
// so that memory usage is bounded regardless of the number of distinct remote
|
||||
// addresses seen.
|
||||
type PacketConn struct {
|
||||
net.PacketConn
|
||||
ConnPolicy proxyproto.ConnPolicyFunc
|
||||
ValidateHeader proxyproto.Validator
|
||||
ReadHeaderTimeout time.Duration
|
||||
|
||||
// UDPSessionTrackingTTL enables per-remote-address session state for UDP
|
||||
// when set to a positive duration. A header-only datagram (valid PPv2
|
||||
// header with or without payload) causes the parsed source address to be
|
||||
// cached for this duration. Subsequent datagrams from the same remote
|
||||
// address that carry no PPv2 header are assigned the cached source
|
||||
// address. The TTL is refreshed on every matching packet. A zero or
|
||||
// negative value disables session tracking entirely.
|
||||
UDPSessionTrackingTTL time.Duration
|
||||
|
||||
// UDPSessionTrackingMaxSessions is the maximum number of concurrent UDP
|
||||
// sessions held in the LRU cache. Zero or negative means use the default
|
||||
// (udpSessionMaxEntries). Has no effect unless UDPSessionTrackingTTL is
|
||||
// positive.
|
||||
UDPSessionTrackingMaxSessions int
|
||||
|
||||
// sessionCache is a thread-safe expirable LRU; lazily initialized on
|
||||
// first use when UDPSessionTrackingTTL > 0.
|
||||
sessionCache *expirable.LRU[string, *proxyproto.Header]
|
||||
}
|
||||
|
||||
func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
@@ -33,6 +72,12 @@ func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
}
|
||||
n, addr, err = c.readFrom(p[:n], addr)
|
||||
if err != nil {
|
||||
if errors.Is(err, errHeaderOnly) {
|
||||
// Header-only datagram with no DNS payload (Spectrum PPv2 UDP
|
||||
// session establishment). Silently discard and wait for the
|
||||
// next datagram.
|
||||
continue
|
||||
}
|
||||
// drop invalid packet as returning error would cause the ReadFrom caller to exit
|
||||
// which could result in DoS if an attacker sends intentional invalid packets
|
||||
clog.Warningf("dropping invalid Proxy Protocol packet from %s: %v", addr.String(), err)
|
||||
@@ -84,8 +129,26 @@ func (c *PacketConn) readFrom(p []byte, addr net.Addr) (_ int, _ net.Addr, err e
|
||||
fallthrough
|
||||
case proxyproto.USE:
|
||||
if header != nil {
|
||||
srcAddr, _, _ := header.UDPAddrs()
|
||||
addr = &Addr{u: addr, r: srcAddr}
|
||||
addr = &Addr{u: addr, r: header.SourceAddr}
|
||||
|
||||
if c.UDPSessionTrackingTTL > 0 {
|
||||
// Cache the real source address for subsequent headerless datagrams.
|
||||
// Spectrum sends the header in a standalone datagram with no DNS
|
||||
// payload; refresh or insert the entry either way so that the TTL
|
||||
// resets on every header packet.
|
||||
c.storeSession(addr.(*Addr).u, header)
|
||||
|
||||
if len(payload) == 0 {
|
||||
// Header-only datagram: no DNS payload to return; loop back
|
||||
// to read the next datagram.
|
||||
return 0, nil, errHeaderOnly
|
||||
}
|
||||
}
|
||||
} else if c.UDPSessionTrackingTTL > 0 {
|
||||
// No header present – look for a cached header for this remote.
|
||||
if cachedHeader, ok := c.lookupSession(addr); ok {
|
||||
addr = &Addr{u: addr, r: cachedHeader.SourceAddr}
|
||||
}
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user