Files
coredns/plugin/proxyproto/setup_test.go
myohannes 3909cc187c 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>
2026-10-15 19:28:43 -07:00

153 lines
5.4 KiB
Go

package proxyproto
import (
"net"
"strings"
"testing"
"time"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
proxyprotoLib "github.com/pires/go-proxyproto"
)
func TestSetup(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedRoot string // expected root, set to the controller. Empty for negative cases.
expectedErrContent string // substring from the expected error. Empty for positive cases.
config bool
sessionTrackingTTL time.Duration
sessionTrackingMaxSessions int
}{
// positive
{"proxyproto", false, "", "", true, 0, 0},
{"proxyproto {\nallow 127.0.0.1/8 ::1/128\n}", false, "", "", true, 0, 0},
{"proxyproto {\nallow 127.0.0.1/8 ::1/128\ndefault ignore\n}", false, "", "", true, 0, 0},
// Allow without any IPs is also valid
{"proxyproto {\nallow\n}", false, "", "", true, 0, 0},
// udp_session_tracking with TTL only (max sessions gonna use package default)
{"proxyproto {\nudp_session_tracking 28s\n}", false, "", "", true, 28 * time.Second, 0},
{"proxyproto {\nallow 10.0.0.0/8\nudp_session_tracking 1m\n}", false, "", "", true, time.Minute, 0},
// udp_session_tracking with explicit max sessions
{"proxyproto {\nudp_session_tracking 28s 20000\n}", false, "", "", true, 28 * time.Second, 20000},
{"proxyproto {\nallow 10.0.0.0/8\nudp_session_tracking 1m 500\n}", false, "", "", true, time.Minute, 500},
// negative
{"proxyproto {\nunknown\n}", true, "", "unknown option", false, 0, 0},
{"proxyproto extra_arg", true, "", "Wrong argument", false, 0, 0},
{"proxyproto {\nallow invalid_ip\n}", true, "", "invalid CIDR address", false, 0, 0},
{"proxyproto {\nallow 127.0.0.1/8\ndefault invalid_policy\n}", true, "", "Wrong argument", false, 0, 0},
// udp_session_tracking: missing TTL
{"proxyproto {\nudp_session_tracking\n}", true, "", "Wrong argument", false, 0, 0},
// udp_session_tracking: too many arguments
{"proxyproto {\nudp_session_tracking 28s 100 extra\n}", true, "", "Wrong argument", false, 0, 0},
// udp_session_tracking: bad TTL
{"proxyproto {\nudp_session_tracking notaduration\n}", true, "", "invalid duration", false, 0, 0},
// udp_session_tracking: bad max sessions
{"proxyproto {\nudp_session_tracking 28s notanumber\n}", true, "", "invalid max sessions", false, 0, 0},
{"proxyproto {\nudp_session_tracking 28s 0\n}", true, "", "invalid max sessions", false, 0, 0},
{"proxyproto {\nudp_session_tracking 28s -1\n}", true, "", "invalid max sessions", false, 0, 0},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
err := setup(c)
cfg := dnsserver.GetConfig(c)
if test.config && cfg.ProxyProtoConnPolicy == nil {
t.Errorf("Test %d: Expected ProxyProtoConnPolicy to be configured for input %s", i, test.input)
}
if !test.config && cfg.ProxyProtoConnPolicy != nil {
t.Errorf("Test %d: Expected ProxyProtoConnPolicy to NOT be configured for input %s", i, test.input)
}
if cfg.ProxyProtoUDPSessionTrackingTTL != test.sessionTrackingTTL {
t.Errorf("Test %d: Expected ProxyProtoUDPSessionTrackingTTL %v, got %v for input %s",
i, test.sessionTrackingTTL, cfg.ProxyProtoUDPSessionTrackingTTL, test.input)
}
if cfg.ProxyProtoUDPSessionTrackingMaxSessions != test.sessionTrackingMaxSessions {
t.Errorf("Test %d: Expected ProxyProtoUDPSessionTrackingMaxSessions %d, got %d for input %s",
i, test.sessionTrackingMaxSessions, cfg.ProxyProtoUDPSessionTrackingMaxSessions, test.input)
}
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErrContent) {
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input)
}
}
}
}
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",
"proxyproto {\ndefault reject\n}",
)
if err := setup(c); err != nil {
t.Fatal(err)
}
cfg := dnsserver.GetConfig(c)
policy, err := cfg.ProxyProtoConnPolicy(
proxyprotoLib.ConnPolicyOptions{
Upstream: &net.TCPAddr{
IP: net.ParseIP("192.0.2.1"),
Port: 53,
},
},
)
if err != nil {
t.Fatal(err)
}
if policy != proxyprotoLib.REJECT {
t.Fatalf("Expected REJECT, got %v", policy)
}
}