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:
myohannes
2026-10-15 22:28:43 -04:00
committed by GitHub
parent 21660a66b5
commit 3909cc187c
18 changed files with 1209 additions and 20 deletions

View File

@@ -110,12 +110,26 @@ func (r *Request) RemoteAddr() string { return r.W.RemoteAddr().String() }
// LocalAddr returns the net.Addr of the server handling the current request.
func (r *Request) LocalAddr() string { return r.W.LocalAddr().String() }
// unwrapAddr returns the underlying net.Addr for addresses that wrap a real
// transport address (implementing Unwrap() net.Addr), so transport/family
// classification sees the concrete *net.TCPAddr or *net.UDPAddr. Plain
// addresses are returned unchanged.
func unwrapAddr(a net.Addr) net.Addr {
if u, ok := a.(interface{ Unwrap() net.Addr }); ok {
if inner := u.Unwrap(); inner != nil {
return inner
}
}
return a
}
// Proto gets the protocol used as the transport. This will be udp or tcp.
func (r *Request) Proto() string {
if _, ok := r.W.RemoteAddr().(*net.UDPAddr); ok {
ra := unwrapAddr(r.W.RemoteAddr())
if _, ok := ra.(*net.UDPAddr); ok {
return "udp"
}
if _, ok := r.W.RemoteAddr().(*net.TCPAddr); ok {
if _, ok := ra.(*net.TCPAddr); ok {
return "tcp"
}
return "udp"
@@ -128,7 +142,7 @@ func (r *Request) Family() int {
}
var a net.IP
ip := r.W.RemoteAddr()
ip := unwrapAddr(r.W.RemoteAddr())
if i, ok := ip.(*net.UDPAddr); ok {
a = i.IP
}

View File

@@ -2,6 +2,7 @@ package request
import (
"fmt"
"net"
"testing"
"github.com/coredns/coredns/plugin/test"
@@ -73,6 +74,88 @@ func TestRequestProto(t *testing.T) {
}
}
// unwrapAddrWriter is a ResponseWriter whose RemoteAddr wraps a real transport
// address behind Unwrap(), mirroring the proxyproto TLV address.
type unwrapAddrWriter struct {
test.ResponseWriter
addr net.Addr
}
func (w *unwrapAddrWriter) RemoteAddr() net.Addr { return w.addr }
// wrappedAddr implements net.Addr + Unwrap() net.Addr.
type wrappedAddr struct{ inner net.Addr }
func (a wrappedAddr) Network() string { return a.inner.Network() }
func (a wrappedAddr) String() string { return a.inner.String() }
func (a wrappedAddr) Unwrap() net.Addr { return a.inner }
// TestRequestProtoFamilyUnwrap ensures a RemoteAddr that wraps a real transport
// address (via Unwrap) is still classified by its underlying type, so a TCP
// connection is not misread as UDP (which would impose the 512-byte limit).
func TestRequestProtoFamilyUnwrap(t *testing.T) {
tcp4 := &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 1234}
w := &unwrapAddrWriter{addr: wrappedAddr{inner: tcp4}}
m := new(dns.Msg)
m.SetQuestion("example.com.", dns.TypeA)
req := Request{W: w, Req: m}
if got := req.Proto(); got != "tcp" {
t.Errorf("Proto() = %q, want tcp (wrapped TCP addr misclassified)", got)
}
if got := req.Family(); got != 1 {
t.Errorf("Family() = %d, want 1 (IPv4)", got)
}
}
// TestRequestScrubTCPWrappedAddr is the P1 regression: a large reply over a
// connection whose RemoteAddr wraps a TCP address (as the proxyproto TLV
// address does) must NOT be truncated — the wrapped addr must still classify as
// TCP, not fall through to the UDP 512-byte limit. A UDP-wrapped addr still
// truncates, proving the transport is honored rather than bypassed.
func TestRequestScrubTCPWrappedAddr(t *testing.T) {
largeReply := func(m *dns.Msg) *dns.Msg {
reply := new(dns.Msg)
reply.SetReply(m)
for i := 1; i < 200; i++ {
reply.Answer = append(reply.Answer, test.SRV(
fmt.Sprintf("large.example.com. 10 IN SRV 0 0 80 10-0-0-%d.default.pod.k8s.example.com.", i)))
}
return reply
}
tcp := &net.TCPAddr{IP: net.ParseIP("10.240.0.1"), Port: 40212}
udp := &net.UDPAddr{IP: net.ParseIP("10.240.0.1"), Port: 40212}
tests := []struct {
name string
w dns.ResponseWriter
wantTruncated bool
}{
{"plain tcp", &test.ResponseWriter{TCP: true}, false},
{"tlv-wrapped tcp", &unwrapAddrWriter{addr: wrappedAddr{inner: tcp}}, false},
{"tlv-wrapped udp", &unwrapAddrWriter{addr: wrappedAddr{inner: udp}}, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
m := new(dns.Msg)
m.SetQuestion("large.example.com.", dns.TypeSRV)
req := Request{W: tc.w, Req: m}
reply := largeReply(m)
req.Scrub(reply)
if reply.Truncated != tc.wantTruncated {
t.Errorf("Truncated = %v, want %v", reply.Truncated, tc.wantTruncated)
}
if !tc.wantTruncated && len(reply.Answer) != 199 {
t.Errorf("TCP reply lost answers: got %d, want 199 (unexpected truncation)", len(reply.Answer))
}
})
}
}
// TestRequestSizeAndDo tests the SizeAndDo method
func TestRequestSizeAndDo(t *testing.T) {
st := testRequest()