mirror of
https://github.com/coredns/coredns.git
synced 2026-10-17 07:45:42 -04:00
plugin/proxyproto: expose PPv2 TLVs to downstream plugins as metadata (#8641)
The proxyproto plugin parsed PROXY protocol v2 headers only to recover the real client address; the TLV vector was discarded, so downstream plugins had no way to read values such as the AWS VPC endpoint ID, SSL client CN, or vendor-specific custom TLVs. Add an opt-in `tlv_metadata` option. When set, the connection's raw PPv2 TLV vector is captured and the plugin, acting as a metadata.Provider, publishes it under the `proxyproto/tlv` label as a hex-encoded string. Consumers decode and split it with proxyproto.SplitTLVs. The option is off by default, so existing deployments skip the extra per-connection processing and behave exactly as before. PROXY protocol v1 connections, which carry no TLVs, are unaffected. Capture spans all transports: dns:// (UDP and TCP), tls:// (DoT), https:// (DoH), grpc://, quic:// (DoQ), and https3:// (DoH3). QUIC-based transports require udp_session_tracking, since the PPv2 header arrives as a standalone first datagram ahead of the QUIC handshake. Includes unit tests for capture, the metadata provider (direct and through the real metadata chain), v1 compatibility, and option parsing, plus README and man page updates. Fixes #8633 Signed-off-by: Mekias Yohannes <mekias@amazon.com> Co-authored-by: Mekias Yohannes <mekias@amazon.com>
This commit is contained in:
@@ -106,6 +106,12 @@ type Config struct {
|
||||
// Has no effect unless ProxyProtoUDPSessionTrackingTTL is positive.
|
||||
ProxyProtoUDPSessionTrackingMaxSessions int
|
||||
|
||||
// ProxyProtoTLVMetadata, when true, captures the PPv2 TLV vector and exposes
|
||||
// it to downstream plugins as the "proxyproto/tlv" metadata label (raw hex).
|
||||
// Opt-in: off by default so existing listeners skip the extra TLV processing.
|
||||
// Has no effect unless ProxyProtoConnPolicy is also set.
|
||||
ProxyProtoTLVMetadata bool
|
||||
|
||||
// MaxGRPCStreams defines the maximum number of concurrent streams per gRPC connection.
|
||||
// This is nil if not specified, allowing for a default to be used.
|
||||
MaxGRPCStreams *int
|
||||
|
||||
@@ -84,6 +84,7 @@ type Server struct {
|
||||
connPolicy proxyproto.ConnPolicyFunc // Proxy Protocol connection policy function
|
||||
udpSessionTrackingTTL time.Duration // TTL for UDP PPv2 session tracking (0 = disabled)
|
||||
udpSessionTrackingMaxSessions int // LRU cap for UDP session tracking (0 = default)
|
||||
proxyProtoTLVMetadata bool // capture PPv2 TLVs and expose as metadata (opt-in)
|
||||
|
||||
server [2]*dns.Server // 0 is a net.Listener, 1 is a net.PacketConn (a *UDPConn) in our case.
|
||||
m sync.Mutex // protects the servers
|
||||
@@ -191,6 +192,9 @@ func NewServer(addr string, group []*Config) (*Server, error) {
|
||||
if site.ProxyProtoUDPSessionTrackingMaxSessions > 0 {
|
||||
s.udpSessionTrackingMaxSessions = site.ProxyProtoUDPSessionTrackingMaxSessions
|
||||
}
|
||||
if site.ProxyProtoTLVMetadata {
|
||||
s.proxyProtoTLVMetadata = true
|
||||
}
|
||||
if site.UDPDecorateWriterFunc != nil {
|
||||
s.udpDecorateWriterFunc = site.UDPDecorateWriterFunc
|
||||
}
|
||||
@@ -259,7 +263,11 @@ func (s *Server) Listen() (net.Listener, error) {
|
||||
return nil, err
|
||||
}
|
||||
if s.connPolicy != nil {
|
||||
l = &proxyproto.Listener{Listener: l, ConnPolicy: s.connPolicy}
|
||||
if s.proxyProtoTLVMetadata {
|
||||
l = cproxyproto.NewTLVListener(l, s.connPolicy)
|
||||
} else {
|
||||
l = &proxyproto.Listener{Listener: l, ConnPolicy: s.connPolicy}
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
@@ -276,7 +284,7 @@ func (s *Server) ListenPacket() (net.PacketConn, error) {
|
||||
return nil, err
|
||||
}
|
||||
if s.connPolicy != nil {
|
||||
p = &cproxyproto.PacketConn{PacketConn: p, ConnPolicy: s.connPolicy, UDPSessionTrackingTTL: s.udpSessionTrackingTTL, UDPSessionTrackingMaxSessions: s.udpSessionTrackingMaxSessions}
|
||||
p = &cproxyproto.PacketConn{PacketConn: p, ConnPolicy: s.connPolicy, UDPSessionTrackingTTL: s.udpSessionTrackingTTL, UDPSessionTrackingMaxSessions: s.udpSessionTrackingMaxSessions, CaptureTLVs: s.proxyProtoTLVMetadata}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/pb"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
cproxyproto "github.com/coredns/coredns/plugin/pkg/proxyproto"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
|
||||
@@ -134,7 +135,11 @@ func (s *ServergRPC) Listen() (net.Listener, error) {
|
||||
return nil, err
|
||||
}
|
||||
if s.connPolicy != nil {
|
||||
l = &proxyproto.Listener{Listener: l, ConnPolicy: s.connPolicy}
|
||||
if s.proxyProtoTLVMetadata {
|
||||
l = cproxyproto.NewTLVListener(l, s.connPolicy)
|
||||
} else {
|
||||
l = &proxyproto.Listener{Listener: l, ConnPolicy: s.connPolicy}
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
@@ -183,12 +188,21 @@ func (s *ServergRPC) Query(ctx context.Context, in *pb.DnsPacket) (*pb.DnsPacket
|
||||
return nil, errors.New("no peer in gRPC context")
|
||||
}
|
||||
|
||||
a, ok := p.Addr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
// The peer address is normally a *net.TCPAddr. When proxyproto tlv_metadata
|
||||
// is enabled, the TLV-capturing listener surfaces a *cproxyproto.Addr whose
|
||||
// RemoteAddr carries the PPv2 TLVs; accept it so the metadata provider can
|
||||
// read them. Validate the real client address is TCP in both cases.
|
||||
var remoteAddr net.Addr
|
||||
switch pa := p.Addr.(type) {
|
||||
case *cproxyproto.Addr:
|
||||
remoteAddr = pa
|
||||
case *net.TCPAddr:
|
||||
remoteAddr = pa
|
||||
default:
|
||||
return nil, fmt.Errorf("no TCP peer in gRPC context: %v", p.Addr)
|
||||
}
|
||||
|
||||
w := &gRPCresponse{localAddr: s.listenAddr, remoteAddr: a, Msg: msg}
|
||||
w := &gRPCresponse{localAddr: s.listenAddr, remoteAddr: remoteAddr, Msg: msg}
|
||||
|
||||
if tsig := msg.IsTsig(); tsig != nil {
|
||||
if s.TsigSecret == nil {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
"github.com/coredns/coredns/plugin/pkg/doh"
|
||||
clog "github.com/coredns/coredns/plugin/pkg/log"
|
||||
cproxyproto "github.com/coredns/coredns/plugin/pkg/proxyproto"
|
||||
"github.com/coredns/coredns/plugin/pkg/response"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
@@ -56,6 +57,10 @@ func (l *loggerAdapter) Write(p []byte) (n int, err error) {
|
||||
// Plugins can access the original HTTP request to retrieve headers, client IP, and metadata.
|
||||
type HTTPRequestKey struct{}
|
||||
|
||||
// connContextKey is the context key under which the accepted net.Conn is stashed
|
||||
// via http.Server.ConnContext, so ServeHTTP can recover PPv2 TLVs from it.
|
||||
type connContextKey struct{}
|
||||
|
||||
// NewServerHTTPS returns a new CoreDNS HTTPS server and compiles all plugins in to it.
|
||||
func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) {
|
||||
tlsConfig, err := sharedTLSConfig(addr, group)
|
||||
@@ -129,6 +134,14 @@ func NewServerHTTPS(addr string, group []*Config) (*ServerHTTPS, error) {
|
||||
}
|
||||
sh.httpsServer.Handler = sh
|
||||
|
||||
// When PPv2 TLV metadata is enabled, stash the accepted connection in the
|
||||
// request context so ServeHTTP can recover the TLVs captured by the listener.
|
||||
if s.proxyProtoTLVMetadata {
|
||||
sh.httpsServer.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
|
||||
return context.WithValue(ctx, connContextKey{}, c)
|
||||
}
|
||||
}
|
||||
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
@@ -163,7 +176,11 @@ func (s *ServerHTTPS) Listen() (net.Listener, error) {
|
||||
return nil, err
|
||||
}
|
||||
if s.connPolicy != nil {
|
||||
l = &proxyproto.Listener{Listener: l, ConnPolicy: s.connPolicy}
|
||||
if s.proxyProtoTLVMetadata {
|
||||
l = cproxyproto.NewTLVListener(l, s.connPolicy)
|
||||
} else {
|
||||
l = &proxyproto.Listener{Listener: l, ConnPolicy: s.connPolicy}
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
@@ -228,6 +245,14 @@ func (s *ServerHTTPS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
request: r,
|
||||
}
|
||||
|
||||
// If the listener captured PPv2 TLVs, surface them via RemoteAddr so the
|
||||
// proxyproto metadata provider exposes them, same as the other transports.
|
||||
if conn, ok := r.Context().Value(connContextKey{}).(net.Conn); ok {
|
||||
if a := cproxyproto.AddrFromConn(conn); a != nil {
|
||||
dw.raddr = a
|
||||
}
|
||||
}
|
||||
|
||||
if tsig := msg.IsTsig(); tsig != nil {
|
||||
if s.TsigSecret == nil {
|
||||
dw.tsigStatus = dns.ErrSecret
|
||||
|
||||
@@ -156,7 +156,7 @@ func (s *ServerHTTPS3) ListenPacket() (net.PacketConn, error) {
|
||||
return nil, err
|
||||
}
|
||||
if s.connPolicy != nil {
|
||||
p = &cproxyproto.PacketConn{PacketConn: p, ConnPolicy: s.connPolicy}
|
||||
p = &cproxyproto.PacketConn{PacketConn: p, ConnPolicy: s.connPolicy, UDPSessionTrackingTTL: s.udpSessionTrackingTTL, UDPSessionTrackingMaxSessions: s.udpSessionTrackingMaxSessions, CaptureTLVs: s.proxyProtoTLVMetadata}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -247,6 +247,13 @@ func (s *ServerHTTPS3) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
request: r,
|
||||
}
|
||||
|
||||
// If proxyproto captured PPv2 TLVs, http3 stores the QUIC connection's
|
||||
// RemoteAddr (our *Addr) under RemoteAddrContextKey; use it so the metadata
|
||||
// provider exposes the TLVs, same as the other transports.
|
||||
if ra, ok := r.Context().Value(http3.RemoteAddrContextKey).(*cproxyproto.Addr); ok {
|
||||
dw.raddr = ra
|
||||
}
|
||||
|
||||
if tsig := msg.IsTsig(); tsig != nil {
|
||||
if s.TsigSecret == nil {
|
||||
dw.tsigStatus = dns.ErrSecret
|
||||
|
||||
@@ -298,7 +298,7 @@ func (s *ServerQUIC) ListenPacket() (net.PacketConn, error) {
|
||||
}
|
||||
|
||||
if s.connPolicy != nil {
|
||||
p = &cproxyproto.PacketConn{PacketConn: p, ConnPolicy: s.connPolicy}
|
||||
p = &cproxyproto.PacketConn{PacketConn: p, ConnPolicy: s.connPolicy, UDPSessionTrackingTTL: s.udpSessionTrackingTTL, UDPSessionTrackingMaxSessions: s.udpSessionTrackingMaxSessions, CaptureTLVs: s.proxyProtoTLVMetadata}
|
||||
}
|
||||
|
||||
s.m.Lock()
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
cproxyproto "github.com/coredns/coredns/plugin/pkg/proxyproto"
|
||||
"github.com/coredns/coredns/plugin/pkg/reuseport"
|
||||
"github.com/coredns/coredns/plugin/pkg/transport"
|
||||
|
||||
@@ -77,7 +78,11 @@ func (s *ServerTLS) Listen() (net.Listener, error) {
|
||||
return nil, err
|
||||
}
|
||||
if s.connPolicy != nil {
|
||||
l = &proxyproto.Listener{Listener: l, ConnPolicy: s.connPolicy}
|
||||
if s.proxyProtoTLVMetadata {
|
||||
l = cproxyproto.NewTLVListener(l, s.connPolicy)
|
||||
} else {
|
||||
l = &proxyproto.Listener{Listener: l, ConnPolicy: s.connPolicy}
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user