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

@@ -0,0 +1,518 @@
package proxyproto
import (
"bytes"
"io"
"net"
"testing"
"time"
proxyproto "github.com/pires/go-proxyproto"
)
// TestV1HeaderNoTLVs verifies PROXY protocol v1 (which has no TLVs) is handled
// cleanly: capture is a no-op, with no error or panic.
func TestV1HeaderNoTLVs(t *testing.T) {
h := &proxyproto.Header{
Version: 1,
Command: proxyproto.PROXY,
TransportProtocol: proxyproto.TCPv4,
SourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000},
DestinationAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53},
}
if got := rawTLVs(h); got != nil {
t.Fatalf("rawTLVs(v1) = %v, want nil", got)
}
pc := &PacketConn{CaptureTLVs: true}
if got := pc.tlvsFor(h); got != nil {
t.Fatalf("tlvsFor(v1) = %v, want nil", got)
}
if tlvs, err := h.TLVs(); err != nil || len(tlvs) != 0 {
t.Fatalf("v1 TLVs() = %v, %v; want empty, nil", tlvs, err)
}
}
// TestAddrFromConnCommand covers both PROXY and LOCAL commands: PROXY publishes
// the TLVs, LOCAL must not (the PROXY protocol spec requires discarding the
// protocol block for LOCAL, even though go-proxyproto retains the parsed TLVs).
func TestAddrFromConnCommand(t *testing.T) {
tlvs := []proxyproto.TLV{{Type: 0xEA, Value: []byte{0x01, 0x02}}}
tests := []struct {
name string
command proxyproto.ProtocolVersionAndCommand
wantTLV bool
}{
{"PROXY", proxyproto.PROXY, true},
{"LOCAL", proxyproto.LOCAL, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := &proxyproto.Header{
Version: 2,
Command: tc.command,
TransportProtocol: proxyproto.TCPv4,
SourceAddr: &net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000},
DestinationAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53},
}
if err := h.SetTLVs(tlvs); err != nil {
t.Fatalf("SetTLVs: %v", err)
}
raw, _ := h.Format()
pc := proxyproto.NewConn(&preloadedConn{buf: raw})
defer pc.Close()
a := AddrFromConn(pc)
if tc.wantTLV {
if a == nil || len(a.RawTLVs()) == 0 {
t.Fatalf("AddrFromConn(%s) did not return TLVs", tc.name)
}
} else if a != nil {
t.Fatalf("AddrFromConn(%s) = %v, want nil (TLVs must not be published)", tc.name, a)
}
})
}
}
// fakeListener returns a preset sequence of conns from Accept.
type fakeListener struct {
conns []net.Conn
i int
addr net.Addr
closed bool
}
func (l *fakeListener) Accept() (net.Conn, error) {
if l.i >= len(l.conns) {
return nil, net.ErrClosed
}
c := l.conns[l.i]
l.i++
return c, nil
}
func (l *fakeListener) Close() error { l.closed = true; return nil }
func (l *fakeListener) Addr() net.Addr {
if l.addr != nil {
return l.addr
}
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}
}
func TestNewTLVListenerAcceptWrapsProxyConn(t *testing.T) {
src := &net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000}
dst := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53}
h := &proxyproto.Header{
Version: 2, Command: proxyproto.PROXY, TransportProtocol: proxyproto.TCPv4,
SourceAddr: src, DestinationAddr: dst,
}
if err := h.SetTLVs([]proxyproto.TLV{{Type: 0xEA, Value: []byte{0x01, 0x02}}}); err != nil {
t.Fatalf("SetTLVs: %v", err)
}
raw, _ := h.Format()
pc := proxyproto.NewConn(&preloadedConn{buf: raw})
// Inject a fake inner listener that hands back the *proxyproto.Conn, so we
// test TLVListener.Accept's wrapping without driving the real proxyproto
// Listener's own header/policy handling.
l := &TLVListener{inner: &fakeListener{conns: []net.Conn{pc}}}
defer l.Close()
_ = l.Addr()
c, err := l.Accept()
if err != nil {
t.Fatalf("Accept: %v", err)
}
a, ok := c.RemoteAddr().(*Addr)
if !ok {
t.Fatalf("Accept conn RemoteAddr type = %T, want *Addr", c.RemoteAddr())
}
got, err := proxyproto.SplitTLVs(a.RawTLVs())
if err != nil || len(got) != 1 || !bytes.Equal(got[0].Value, []byte{0x01, 0x02}) {
t.Fatalf("TLV mismatch: %+v err=%v", got, err)
}
c.Close()
}
func TestTLVListenerAcceptPassesThroughNonProxyConn(t *testing.T) {
// A non-*proxyproto.Conn is returned unchanged.
plain := plainRemoteConn{src: &net.TCPAddr{IP: net.ParseIP("198.51.100.1"), Port: 7000}}
l := &TLVListener{inner: &fakeListener{conns: []net.Conn{plain}}}
c, err := l.Accept()
if err != nil {
t.Fatalf("Accept: %v", err)
}
if _, ok := c.RemoteAddr().(*Addr); ok {
t.Fatal("non-proxy conn should pass through without *Addr")
}
}
func TestNewTLVListenerConstructor(t *testing.T) {
// NewTLVListener returns a *TLVListener wrapping a proxyproto.Listener.
base := &fakeListener{addr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9953}}
l := NewTLVListener(base, nil)
if l.Addr().String() != base.Addr().String() {
t.Fatalf("Addr = %s, want %s", l.Addr(), base.Addr())
}
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
if !base.closed {
t.Fatal("expected inner listener to be closed")
}
}
func TestNewAddr(t *testing.T) {
u := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53}
r := &net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000}
a := NewAddr(u, r, []byte{0xaa})
if a.Network() != u.Network() {
t.Errorf("Network = %q, want %q", a.Network(), u.Network())
}
if a.String() != r.String() {
t.Errorf("String = %q, want %q", a.String(), r.String())
}
if !bytes.Equal(a.RawTLVs(), []byte{0xaa}) {
t.Errorf("RawTLVs = %v, want [aa]", a.RawTLVs())
}
}
// tlsLikeConn wraps a net.Conn and exposes NetConn, mimicking *tls.Conn so the
// netConner branch of underlyingProxyConn is exercised.
type tlsLikeConn struct {
net.Conn
inner net.Conn
}
func (c *tlsLikeConn) NetConn() net.Conn { return c.inner }
func TestAddrFromConnUnwrapsTLSLayer(t *testing.T) {
src := &net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000}
dst := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53}
h := &proxyproto.Header{
Version: 2, Command: proxyproto.PROXY, TransportProtocol: proxyproto.TCPv4,
SourceAddr: src, DestinationAddr: dst,
}
if err := h.SetTLVs([]proxyproto.TLV{{Type: 0xEA, Value: []byte{0x07}}}); err != nil {
t.Fatalf("SetTLVs: %v", err)
}
raw, _ := h.Format()
pc := proxyproto.NewConn(&preloadedConn{buf: raw})
// tlsLike -> *proxyproto.Conn. RemoteAddr on the plain tls-like wrapper does
// NOT promote the proxy header, so AddrFromConn must fall through to the
// NetConn-unwrap path to reach the *proxyproto.Conn.
wrapped := &tlsLikeConn{Conn: plainRemoteConn{src: dst}, inner: pc}
a := AddrFromConn(wrapped)
if a == nil {
t.Fatal("AddrFromConn did not unwrap tls-like layer")
}
got, err := proxyproto.SplitTLVs(a.RawTLVs())
if err != nil || len(got) != 1 || !bytes.Equal(got[0].Value, []byte{0x07}) {
t.Fatalf("TLV mismatch after unwrap: %+v err=%v", got, err)
}
pc.Close()
}
func TestAddrFromConnUnknownConn(t *testing.T) {
// A conn that is neither *proxyproto.Conn, *tlvConn, nor a netConner, and
// whose RemoteAddr is a plain addr: AddrFromConn returns nil.
if a := AddrFromConn(plainRemoteConn{src: &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5}}); a != nil {
t.Fatalf("AddrFromConn(unknown) = %v, want nil", a)
}
}
// plainRemoteConn is a net.Conn whose RemoteAddr is a plain *net.TCPAddr.
type plainRemoteConn struct {
net.Conn
src net.Addr
}
func (c plainRemoteConn) RemoteAddr() net.Addr { return c.src }
// preloadedConn is a net.Conn whose Read drains a fixed byte buffer (the PPv2
// header), avoiding net.Pipe synchronization in unit tests.
type preloadedConn struct {
net.Conn
buf []byte
off int
remote net.Addr
}
func (c *preloadedConn) Read(p []byte) (int, error) {
if c.off >= len(c.buf) {
return 0, io.EOF
}
n := copy(p, c.buf[c.off:])
c.off += n
return n, nil
}
func (c *preloadedConn) Write(p []byte) (int, error) { return len(p), nil }
func (c *preloadedConn) Close() error { return nil }
func (c *preloadedConn) RemoteAddr() net.Addr {
if c.remote != nil {
return c.remote
}
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1}
}
func (c *preloadedConn) LocalAddr() net.Addr {
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53}
}
func (c *preloadedConn) SetDeadline(time.Time) error { return nil }
func (c *preloadedConn) SetReadDeadline(time.Time) error { return nil }
func (c *preloadedConn) SetWriteDeadline(time.Time) error { return nil }
// ppv2UDPPacketWithTLVs builds a valid PPv2 UDP datagram carrying the given
// TLVs followed by the DNS payload, using the library's own formatter.
func ppv2UDPPacketWithTLVs(t *testing.T, src, dst *net.UDPAddr, tlvs []proxyproto.TLV, payload []byte) []byte {
t.Helper()
h := &proxyproto.Header{
Version: 2,
Command: proxyproto.PROXY,
TransportProtocol: proxyproto.UDPv4,
SourceAddr: src,
DestinationAddr: dst,
}
if len(tlvs) > 0 {
if err := h.SetTLVs(tlvs); err != nil {
t.Fatalf("SetTLVs: %v", err)
}
}
raw, err := h.Format()
if err != nil {
t.Fatalf("Format: %v", err)
}
return append(raw, payload...)
}
func usePolicy(proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) {
return proxyproto.USE, nil
}
func TestReadFromCapturesTLVsWhenEnabled(t *testing.T) {
src := udpAddr("192.168.1.10", 40000)
dst := udpAddr("10.0.0.1", 53)
payload := []byte("dns-payload")
tlvs := []proxyproto.TLV{
{Type: proxyproto.PP2_TYPE_AUTHORITY, Value: []byte("example.org")},
{Type: 0xEA, Value: []byte{0x01, 0x02, 0x03}},
}
packet := ppv2UDPPacketWithTLVs(t, src, dst, tlvs, payload)
pc := &PacketConn{PacketConn: &singlePacketConn{}, ConnPolicy: usePolicy, CaptureTLVs: true}
n, addr, err := pc.readFrom(packet, udpAddr("203.0.113.9", 5000))
if err != nil {
t.Fatalf("readFrom: %v", err)
}
if !bytes.Equal(packet[:n], payload) {
t.Fatalf("payload = %q, want %q", packet[:n], payload)
}
a, ok := addr.(*Addr)
if !ok {
t.Fatalf("addr type = %T, want *Addr", addr)
}
raw := a.RawTLVs()
if len(raw) == 0 {
t.Fatal("expected raw TLVs to be captured")
}
// Round-trip: the captured bytes must re-split to the TLVs we sent.
got, err := proxyproto.SplitTLVs(raw)
if err != nil {
t.Fatalf("SplitTLVs: %v", err)
}
if len(got) != len(tlvs) {
t.Fatalf("got %d TLVs, want %d", len(got), len(tlvs))
}
for i, want := range tlvs {
if got[i].Type != want.Type || !bytes.Equal(got[i].Value, want.Value) {
t.Errorf("TLV %d = {%#x,%v}, want {%#x,%v}", i, got[i].Type, got[i].Value, want.Type, want.Value)
}
}
}
func TestReadFromSkipsTLVsWhenDisabled(t *testing.T) {
src := udpAddr("192.168.1.10", 40000)
dst := udpAddr("10.0.0.1", 53)
tlvs := []proxyproto.TLV{{Type: 0xEA, Value: []byte{0x09}}}
packet := ppv2UDPPacketWithTLVs(t, src, dst, tlvs, []byte("x"))
pc := &PacketConn{PacketConn: &singlePacketConn{}, ConnPolicy: usePolicy} // CaptureTLVs defaults to false
_, addr, err := pc.readFrom(packet, udpAddr("203.0.113.9", 5000))
if err != nil {
t.Fatalf("readFrom: %v", err)
}
a, ok := addr.(*Addr)
if !ok {
t.Fatalf("addr type = %T, want *Addr", addr)
}
if a.RawTLVs() != nil {
t.Fatalf("expected no TLVs captured when disabled, got %v", a.RawTLVs())
}
}
func TestReadFromNoTLVsPresent(t *testing.T) {
src := udpAddr("192.168.1.10", 40000)
dst := udpAddr("10.0.0.1", 53)
packet := ppv2UDPPacketWithTLVs(t, src, dst, nil, []byte("x"))
pc := &PacketConn{PacketConn: &singlePacketConn{}, ConnPolicy: usePolicy, CaptureTLVs: true}
_, addr, err := pc.readFrom(packet, udpAddr("203.0.113.9", 5000))
if err != nil {
t.Fatalf("readFrom: %v", err)
}
a, ok := addr.(*Addr)
if !ok {
t.Fatalf("addr type = %T, want *Addr", addr)
}
if a.RawTLVs() != nil {
t.Fatalf("expected nil TLVs when header has none, got %v", a.RawTLVs())
}
}
func TestRawTLVsHelper(t *testing.T) {
// nil header -> nil
if got := rawTLVs(nil); got != nil {
t.Fatalf("rawTLVs(nil) = %v, want nil", got)
}
// header with no TLVs -> nil
h := &proxyproto.Header{Version: 2}
if got := rawTLVs(h); got != nil {
t.Fatalf("rawTLVs(no tlvs) = %v, want nil", got)
}
// header with TLVs -> re-splittable bytes
tlvs := []proxyproto.TLV{{Type: proxyproto.PP2_TYPE_UNIQUE_ID, Value: []byte("abc")}}
if err := h.SetTLVs(tlvs); err != nil {
t.Fatalf("SetTLVs: %v", err)
}
raw := rawTLVs(h)
got, err := proxyproto.SplitTLVs(raw)
if err != nil {
t.Fatalf("SplitTLVs: %v", err)
}
if len(got) != 1 || got[0].Type != proxyproto.PP2_TYPE_UNIQUE_ID || !bytes.Equal(got[0].Value, []byte("abc")) {
t.Fatalf("round-trip mismatch: %+v", got)
}
}
func TestTLVConnRemoteAddrCarriesTLVs(t *testing.T) {
src := &net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000}
dst := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53}
tlvs := []proxyproto.TLV{{Type: 0xEA, Value: []byte{0xaa, 0xbb}}}
h := &proxyproto.Header{
Version: 2,
Command: proxyproto.PROXY,
TransportProtocol: proxyproto.TCPv4,
SourceAddr: src,
DestinationAddr: dst,
}
if err := h.SetTLVs(tlvs); err != nil {
t.Fatalf("SetTLVs: %v", err)
}
raw, err := h.Format()
if err != nil {
t.Fatalf("Format: %v", err)
}
client, server := net.Pipe()
go func() {
client.Write(raw)
// Keep the pipe open; ProxyHeader only needs the header bytes.
}()
pc := proxyproto.NewConn(server)
tc := &tlvConn{Conn: pc, pc: pc}
addr := tc.RemoteAddr()
a, ok := addr.(*Addr)
if !ok {
t.Fatalf("RemoteAddr type = %T, want *Addr", addr)
}
if a.String() != src.String() {
t.Errorf("RemoteAddr = %s, want %s", a.String(), src)
}
got, err := proxyproto.SplitTLVs(a.RawTLVs())
if err != nil {
t.Fatalf("SplitTLVs: %v", err)
}
if len(got) != 1 || got[0].Type != 0xEA || !bytes.Equal(got[0].Value, []byte{0xaa, 0xbb}) {
t.Fatalf("TLV round-trip mismatch: %+v", got)
}
tc.Close()
}
func TestTLVConnRemoteAddrNoHeader(t *testing.T) {
// A connection with no PROXY header: RemoteAddr falls back to the plain
// address, not an *Addr.
client, server := net.Pipe()
go func() {
client.Write([]byte("not a proxy header"))
}()
pc := proxyproto.NewConn(server)
tc := &tlvConn{Conn: pc, pc: pc}
if _, ok := tc.RemoteAddr().(*Addr); ok {
t.Fatal("expected plain addr when no PROXY header present")
}
tc.Close()
}
// delegatingConn embeds a net.Conn and promotes its RemoteAddr, mimicking the
// wrapper chain (tls.Conn, connection limiter) the DoH path sees.
type delegatingConn struct {
net.Conn
}
func TestAddrFromConnDirect(t *testing.T) {
src := &net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000}
dst := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53}
h := &proxyproto.Header{
Version: 2, Command: proxyproto.PROXY, TransportProtocol: proxyproto.TCPv4,
SourceAddr: src, DestinationAddr: dst,
}
if err := h.SetTLVs([]proxyproto.TLV{{Type: 0xEA, Value: []byte{0x01}}}); err != nil {
t.Fatalf("SetTLVs: %v", err)
}
raw, _ := h.Format()
client, server := net.Pipe()
go func() { client.Write(raw) }()
pc := proxyproto.NewConn(server)
// Direct *proxyproto.Conn.
if a := AddrFromConn(pc); a == nil || len(a.RawTLVs()) == 0 {
t.Fatal("AddrFromConn(*proxyproto.Conn) did not recover TLVs")
}
pc.Close()
}
func TestAddrFromConnThroughWrapper(t *testing.T) {
src := &net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000}
dst := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53}
h := &proxyproto.Header{
Version: 2, Command: proxyproto.PROXY, TransportProtocol: proxyproto.TCPv4,
SourceAddr: src, DestinationAddr: dst,
}
if err := h.SetTLVs([]proxyproto.TLV{{Type: 0xEA, Value: []byte{0x02, 0x03}}}); err != nil {
t.Fatalf("SetTLVs: %v", err)
}
raw, _ := h.Format()
client, server := net.Pipe()
go func() { client.Write(raw) }()
pc := proxyproto.NewConn(server)
tc := &tlvConn{Conn: pc, pc: pc}
// Wrap in a conn that only promotes RemoteAddr (like tls.Conn / limiter).
wrapped := &delegatingConn{Conn: tc}
a := AddrFromConn(wrapped)
if a == nil {
t.Fatal("AddrFromConn through wrapper returned nil")
}
got, err := proxyproto.SplitTLVs(a.RawTLVs())
if err != nil || len(got) != 1 || !bytes.Equal(got[0].Value, []byte{0x02, 0x03}) {
t.Fatalf("TLV mismatch through wrapper: %+v err=%v", got, err)
}
tc.Close()
}
func TestAddrFromConnNil(t *testing.T) {
if a := AddrFromConn(nil); a != nil {
t.Fatalf("AddrFromConn(nil) = %v, want nil", a)
}
}