mirror of
https://github.com/coredns/coredns.git
synced 2026-10-16 23:35:43 -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:
158
plugin/proxyproto/metadata_test.go
Normal file
158
plugin/proxyproto/metadata_test.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package proxyproto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metadata"
|
||||
cproxyproto "github.com/coredns/coredns/plugin/pkg/proxyproto"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
proxyproto "github.com/pires/go-proxyproto"
|
||||
)
|
||||
|
||||
// TestMetadataThroughChain drives metadata.Collect + the chain with the real
|
||||
// handler as Provider, asserting a downstream plugin reads proxyproto/tlv.
|
||||
func TestMetadataThroughChain(t *testing.T) {
|
||||
tlvs := []proxyproto.TLV{{Type: 0xEA, Value: []byte{0xca, 0xfe}}}
|
||||
raw, err := proxyproto.JoinTLVs(tlvs)
|
||||
if err != nil {
|
||||
t.Fatalf("JoinTLVs: %v", err)
|
||||
}
|
||||
addr := cproxyproto.NewAddr(
|
||||
&net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53},
|
||||
&net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000},
|
||||
raw,
|
||||
)
|
||||
w := &tlvResponseWriter{addr: addr}
|
||||
|
||||
// Terminal plugin captures what it reads from the metadata label.
|
||||
var gotLabel string
|
||||
var gotPresent bool
|
||||
terminal := plugin.HandlerFunc(func(ctx context.Context, _ dns.ResponseWriter, _ *dns.Msg) (int, error) {
|
||||
if f := metadata.ValueFunc(ctx, "proxyproto/tlv"); f != nil {
|
||||
gotPresent = true
|
||||
gotLabel = f()
|
||||
}
|
||||
return dns.RcodeSuccess, nil
|
||||
})
|
||||
|
||||
// proxyproto handler sits between metadata and the terminal, and is also the
|
||||
// Provider the metadata plugin collects from.
|
||||
h := handler{Next: terminal}
|
||||
meta := metadata.Metadata{
|
||||
Zones: []string{"."},
|
||||
Providers: []metadata.Provider{h},
|
||||
Next: h,
|
||||
}
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("example.org.", dns.TypeA)
|
||||
ctx := meta.Collect(context.Background(), request.Request{W: w, Req: m})
|
||||
if _, err := meta.ServeDNS(ctx, w, m); err != nil {
|
||||
t.Fatalf("ServeDNS: %v", err)
|
||||
}
|
||||
|
||||
if !gotPresent {
|
||||
t.Fatal("downstream plugin did not see proxyproto/tlv label")
|
||||
}
|
||||
if want := hex.EncodeToString(raw); gotLabel != want {
|
||||
t.Fatalf("proxyproto/tlv = %q, want %q", gotLabel, want)
|
||||
}
|
||||
}
|
||||
|
||||
// nextHandler records that it was called and returns a sentinel rcode.
|
||||
type nextHandler struct{ called bool }
|
||||
|
||||
func (h *nextHandler) Name() string { return "next" }
|
||||
func (h *nextHandler) ServeDNS(_ context.Context, _ dns.ResponseWriter, _ *dns.Msg) (int, error) {
|
||||
h.called = true
|
||||
return dns.RcodeSuccess, nil
|
||||
}
|
||||
|
||||
func TestHandlerName(t *testing.T) {
|
||||
if got := (handler{}).Name(); got != "proxyproto" {
|
||||
t.Fatalf("Name = %q, want %q", got, "proxyproto")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerServeDNSPassthrough(t *testing.T) {
|
||||
next := &nextHandler{}
|
||||
h := handler{Next: next}
|
||||
rc, err := h.ServeDNS(context.Background(), &test.ResponseWriter{}, new(dns.Msg))
|
||||
if err != nil {
|
||||
t.Fatalf("ServeDNS err = %v", err)
|
||||
}
|
||||
if rc != dns.RcodeSuccess {
|
||||
t.Fatalf("rcode = %d, want %d", rc, dns.RcodeSuccess)
|
||||
}
|
||||
if !next.called {
|
||||
t.Fatal("expected next handler to be called")
|
||||
}
|
||||
}
|
||||
|
||||
// tlvResponseWriter is a test ResponseWriter whose RemoteAddr returns a
|
||||
// cproxyproto.Addr carrying the given raw TLV bytes.
|
||||
type tlvResponseWriter struct {
|
||||
test.ResponseWriter
|
||||
addr net.Addr
|
||||
}
|
||||
|
||||
func (w *tlvResponseWriter) RemoteAddr() net.Addr { return w.addr }
|
||||
|
||||
func TestMetadataPublishesTLV(t *testing.T) {
|
||||
// Build raw TLVs the way the listener would capture them.
|
||||
tlvs := []proxyproto.TLV{{Type: 0xEA, Value: []byte{0x01, 0x02}}}
|
||||
raw, err := proxyproto.JoinTLVs(tlvs)
|
||||
if err != nil {
|
||||
t.Fatalf("JoinTLVs: %v", err)
|
||||
}
|
||||
addr := cproxyproto.NewAddr(
|
||||
&net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53},
|
||||
&net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000},
|
||||
raw,
|
||||
)
|
||||
|
||||
w := &tlvResponseWriter{addr: addr}
|
||||
ctx := metadata.ContextWithMetadata(context.Background())
|
||||
h := handler{}
|
||||
ctx = h.Metadata(ctx, request.Request{W: w, Req: new(dns.Msg)})
|
||||
|
||||
f := metadata.ValueFunc(ctx, "proxyproto/tlv")
|
||||
if f == nil {
|
||||
t.Fatal("expected proxyproto/tlv metadata to be set")
|
||||
}
|
||||
if got, want := f(), hex.EncodeToString(raw); got != want {
|
||||
t.Fatalf("proxyproto/tlv = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataNoTLVNoLabel(t *testing.T) {
|
||||
// RemoteAddr is a plain addr (not cproxyproto.Addr): no label published.
|
||||
w := &tlvResponseWriter{addr: &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 1234}}
|
||||
ctx := metadata.ContextWithMetadata(context.Background())
|
||||
ctx = handler{}.Metadata(ctx, request.Request{W: w, Req: new(dns.Msg)})
|
||||
if f := metadata.ValueFunc(ctx, "proxyproto/tlv"); f != nil {
|
||||
t.Fatalf("expected no label for plain addr, got %q", f())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataAddrWithoutTLVNoLabel(t *testing.T) {
|
||||
// cproxyproto.Addr present but carrying no TLVs: no label.
|
||||
addr := cproxyproto.NewAddr(
|
||||
&net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 53},
|
||||
&net.TCPAddr{IP: net.ParseIP("192.168.1.10"), Port: 40000},
|
||||
nil,
|
||||
)
|
||||
w := &tlvResponseWriter{addr: addr}
|
||||
ctx := metadata.ContextWithMetadata(context.Background())
|
||||
ctx = handler{}.Metadata(ctx, request.Request{W: w, Req: new(dns.Msg)})
|
||||
if f := metadata.ValueFunc(ctx, "proxyproto/tlv"); f != nil {
|
||||
t.Fatalf("expected no label when Addr has no TLVs, got %q", f())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user