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:
@@ -17,6 +17,7 @@ proxyproto {
|
||||
allow <CIDR...>
|
||||
default <use|ignore|reject|skip>
|
||||
udp_session_tracking <duration> [max_sessions]
|
||||
tlv_metadata
|
||||
}
|
||||
~~~
|
||||
|
||||
@@ -40,6 +41,30 @@ refreshed on each matching packet. The optional `max_sessions` argument caps the
|
||||
concurrent sessions in the LRU cache (default: 10240). This option has no effect for TCP
|
||||
connections.
|
||||
|
||||
The `tlv_metadata` option exposes the PROXY Protocol v2 [TLV](https://www.haproxy.org/download/2.3/doc/proxy-protocol.txt)
|
||||
(Type-Length-Value) vector from the header to downstream plugins via the
|
||||
[*metadata*](../metadata/README.md) plugin. When set, and the *metadata* plugin is enabled, the
|
||||
raw TLV vector is published under the label `proxyproto/tlv` as a hex-encoded string. Consumers
|
||||
decode the hex and split it with `proxyproto.SplitTLVs` from
|
||||
`github.com/pires/go-proxyproto` to read individual TLVs (e.g. the AWS VPC endpoint ID, SSL
|
||||
client CN, or vendor-specific custom TLVs). This option is opt-in: without it, TLVs are not
|
||||
captured and no extra per-connection processing occurs. It applies to all transports: `dns://`
|
||||
(UDP and TCP), `tls://` (DoT), `https://` (DoH), `grpc://`, `quic://` (DoQ), and `https3://`
|
||||
(DoH3).
|
||||
|
||||
For the QUIC-based transports (`quic://` and `https3://`), the PPv2 header arrives as a
|
||||
standalone first datagram ahead of the QUIC handshake, so TLV capture additionally requires
|
||||
`udp_session_tracking` to be enabled; the cached session links the subsequent headerless QUIC
|
||||
datagrams to the header's source and TLVs.
|
||||
|
||||
## Metadata
|
||||
|
||||
When the `tlv_metadata` option and the *metadata* plugin are both enabled, the following metadata
|
||||
label is made available:
|
||||
|
||||
- `proxyproto/tlv`: the raw PPv2 TLV vector, hex-encoded. Only set when the request's connection
|
||||
carried a PPv2 header with at least one TLV.
|
||||
|
||||
## Examples
|
||||
|
||||
In this configuration, we allow PROXY protocol connections from all IP addresses:
|
||||
@@ -101,3 +126,18 @@ In this configuration, the session cap is raised to 20480:
|
||||
forward . /etc/resolv.conf
|
||||
}
|
||||
~~~
|
||||
|
||||
In this configuration, PPv2 TLVs are exposed to downstream plugins as metadata. The *metadata*
|
||||
plugin must also be enabled. Here the *log* plugin records the raw TLV vector:
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
metadata
|
||||
proxyproto {
|
||||
allow 192.168.1.1/32
|
||||
tlv_metadata
|
||||
}
|
||||
forward . /etc/resolv.conf
|
||||
log . "{remote} {/proxyproto/tlv}"
|
||||
}
|
||||
~~~
|
||||
|
||||
50
plugin/proxyproto/metadata.go
Normal file
50
plugin/proxyproto/metadata.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package proxyproto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/metadata"
|
||||
cproxyproto "github.com/coredns/coredns/plugin/pkg/proxyproto"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// handler is a pass-through plugin that implements metadata.Provider to expose
|
||||
// the PPv2 TLV vector captured by the listener. It is only added to the chain
|
||||
// when the tlv_metadata option is set.
|
||||
type handler struct {
|
||||
Next plugin.Handler
|
||||
}
|
||||
|
||||
// Name implements plugin.Handler.
|
||||
func (h handler) Name() string { return "proxyproto" }
|
||||
|
||||
// ServeDNS implements plugin.Handler; it only passes the request on.
|
||||
func (h handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
// Metadata implements metadata.Provider. It publishes the raw PPv2 TLV vector
|
||||
// as the "proxyproto/tlv" label, hex-encoded, when the current request carries
|
||||
// TLVs. Consumers decode the hex and split it with proxyproto.SplitTLVs.
|
||||
func (h handler) Metadata(ctx context.Context, state request.Request) context.Context {
|
||||
a, ok := state.W.RemoteAddr().(*cproxyproto.Addr)
|
||||
if !ok {
|
||||
return ctx
|
||||
}
|
||||
raw := a.RawTLVs()
|
||||
if len(raw) == 0 {
|
||||
return ctx
|
||||
}
|
||||
v := hex.EncodeToString(raw)
|
||||
metadata.SetValueFunc(ctx, "proxyproto/tlv", func() string { return v })
|
||||
return ctx
|
||||
}
|
||||
|
||||
var (
|
||||
_ plugin.Handler = handler{}
|
||||
_ metadata.Provider = handler{}
|
||||
)
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ func setup(c *caddy.Controller) error {
|
||||
defaultSet bool
|
||||
sessionTrackingTTL time.Duration
|
||||
sessionTrackingMaxSessions int
|
||||
tlvMetadata bool
|
||||
)
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
@@ -79,6 +80,11 @@ func setup(c *caddy.Controller) error {
|
||||
}
|
||||
sessionTrackingMaxSessions = n
|
||||
}
|
||||
case "tlv_metadata":
|
||||
if len(c.RemainingArgs()) != 0 {
|
||||
return plugin.Error("proxyproto", c.ArgErr())
|
||||
}
|
||||
tlvMetadata = true
|
||||
default:
|
||||
return c.Errf("unknown option '%s'", c.Val())
|
||||
}
|
||||
@@ -105,5 +111,13 @@ func setup(c *caddy.Controller) error {
|
||||
}
|
||||
config.ProxyProtoUDPSessionTrackingTTL = sessionTrackingTTL
|
||||
config.ProxyProtoUDPSessionTrackingMaxSessions = sessionTrackingMaxSessions
|
||||
config.ProxyProtoTLVMetadata = tlvMetadata
|
||||
if tlvMetadata {
|
||||
// Add a pass-through handler that implements metadata.Provider so the
|
||||
// captured PPv2 TLVs are exposed as the "proxyproto/tlv" label.
|
||||
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
return handler{Next: next}
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,6 +88,40 @@ func TestSetup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLVMetadataFlag(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectTLV bool
|
||||
}{
|
||||
{"proxyproto {\ntlv_metadata\n}", false, true},
|
||||
{"proxyproto {\nallow 10.0.0.0/8\ntlv_metadata\n}", false, true},
|
||||
// absent -> disabled
|
||||
{"proxyproto {\nallow 10.0.0.0/8\n}", false, false},
|
||||
// takes no arguments
|
||||
{"proxyproto {\ntlv_metadata on\n}", true, false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.input)
|
||||
err := setup(c)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: expected error, got none for %q", i, test.input)
|
||||
continue
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: unexpected error for %q: %v", i, test.input, err)
|
||||
continue
|
||||
}
|
||||
if test.shouldErr {
|
||||
continue
|
||||
}
|
||||
cfg := dnsserver.GetConfig(c)
|
||||
if cfg.ProxyProtoTLVMetadata != test.expectTLV {
|
||||
t.Errorf("Test %d: ProxyProtoTLVMetadata = %v, want %v for %q", i, cfg.ProxyProtoTLVMetadata, test.expectTLV, test.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultPolicyWithoutAllow(t *testing.T) {
|
||||
c := caddy.NewTestController(
|
||||
"dns",
|
||||
|
||||
Reference in New Issue
Block a user