feat(proxyproto): add proxy protocol support (#7738)

Signed-off-by: Adphi <philippe.adrien.nousse@gmail.com>
This commit is contained in:
Adphi
2026-02-11 02:14:05 +01:00
committed by GitHub
parent a100d0cca4
commit e9c0db32dc
15 changed files with 389 additions and 3 deletions

View File

@@ -0,0 +1,136 @@
package proxyproto
import (
"bufio"
"bytes"
"errors"
"fmt"
"net"
"time"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/pires/go-proxyproto"
)
var (
_ net.PacketConn = (*PacketConn)(nil)
_ net.Addr = (*Addr)(nil)
)
type PacketConn struct {
net.PacketConn
ConnPolicy proxyproto.ConnPolicyFunc
ValidateHeader proxyproto.Validator
ReadHeaderTimeout time.Duration
}
func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
for {
n, addr, err = c.PacketConn.ReadFrom(p)
if err != nil {
return n, addr, err
}
n, addr, err = c.readFrom(p[:n], addr)
if err != nil {
// drop invalid packet as returning error would cause the ReadFrom caller to exit
// which could result in DoS if an attacker sends intentional invalid packets
clog.Warningf("dropping invalid Proxy Protocol packet from %s: %v", addr.String(), err)
continue
}
return n, addr, nil
}
}
func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
if pa, ok := addr.(*Addr); ok {
addr = pa.u
}
return c.PacketConn.WriteTo(p, addr)
}
func (c *PacketConn) readFrom(p []byte, addr net.Addr) (_ int, _ net.Addr, err error) {
var policy proxyproto.Policy
if c.ConnPolicy != nil {
policy, err = c.ConnPolicy(proxyproto.ConnPolicyOptions{
Upstream: addr,
Downstream: c.LocalAddr(),
})
if err != nil {
return 0, nil, fmt.Errorf("applying Proxy Protocol connection policy: %w", err)
}
}
if policy == proxyproto.SKIP {
return len(p), addr, nil
}
header, payload, err := parseProxyProtocol(p)
if err != nil {
return 0, nil, err
}
if header != nil && c.ValidateHeader != nil {
if err := c.ValidateHeader(header); err != nil {
return 0, nil, fmt.Errorf("validating Proxy Protocol header: %w", err)
}
}
switch policy {
case proxyproto.REJECT:
if header != nil {
return 0, nil, errors.New("connection rejected by Proxy Protocol connection policy")
}
case proxyproto.REQUIRE:
if header == nil {
return 0, nil, errors.New("PROXY Protocol header required but not present")
}
fallthrough
case proxyproto.USE:
if header != nil {
srcAddr, _, _ := header.UDPAddrs()
addr = &Addr{u: addr, r: srcAddr}
}
default:
}
copy(p, payload)
return len(payload), addr, nil
}
type Addr struct {
u net.Addr
r net.Addr
}
func (a *Addr) Network() string {
return a.u.Network()
}
func (a *Addr) String() string {
return a.r.String()
}
func parseProxyProtocol(packet []byte) (*proxyproto.Header, []byte, error) {
reader := bufio.NewReader(bytes.NewReader(packet))
header, err := proxyproto.Read(reader)
if err != nil {
if errors.Is(err, proxyproto.ErrNoProxyProtocol) {
return nil, packet, nil
}
return nil, nil, fmt.Errorf("parsing Proxy Protocol header (packet size: %d): %w", len(packet), err)
}
if header.Version != 2 {
return nil, nil, fmt.Errorf("unsupported Proxy Protocol version %d (only v2 supported for UDP)", header.Version)
}
_, _, ok := header.UDPAddrs()
if !ok {
return nil, nil, fmt.Errorf("PROXY Protocol header is not UDP type (transport protocol: 0x%x)", header.TransportProtocol)
}
headerLen := len(packet) - reader.Buffered()
if headerLen < 0 || headerLen > len(packet) {
return nil, nil, fmt.Errorf("invalid header length: %d", headerLen)
}
payload := packet[headerLen:]
return header, payload, nil
}

View File

@@ -0,0 +1,63 @@
# proxyproto
## Name
*proxyproto* - add [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support.
## Description
This plugin adds support for the PROXY protocol version 1 and 2. It allows CoreDNS to receive
connections from a load balancer or proxy that uses the PROXY protocol to forward the original
client's IP address and port information.
## Syntax
~~~ txt
proxyproto {
allow <CIDR...>
default <use|ignore|reject|skip>
}
~~~
If `allow` is unspecified, PROXY protocol headers are accepted from all IP addresses.
The `default` option controls how connections from sources not listed in `allow` are handled.
If `default` is unspecified, it defaults to `ignore`.
The possible values are:
- `use`: accept and use PROXY protocol headers from these sources
- `ignore`: accept and ignore PROXY protocol headers from other sources
- `reject`: reject connections with PROXY protocol headers from other sources
- `skip`: skip PROXY protocol processing for connections from other sources, treating them as normal connections preserving the PROXY protocol headers.
## Examples
In this configuration, we allow PROXY protocol connections from all IP addresses:
~~~ corefile
. {
proxyproto
forward . /etc/resolv.conf
}
~~~
In this configuration, we only allow PROXY protocol connections from the specified CIDR ranges
and ignore proxy protocol headers from other sources:
~~~ corefile
. {
proxyproto {
allow 192.168.1.1/32 192.168.0.1/32
}
forward . /etc/resolv.conf
}
~~~
In this configuration, we only allow PROXY protocol headers from the specified CIDR ranges and reject
connections without valid PROXY protocol headers from those sources:
~~~ corefile
. {
proxyproto {
allow 192.168.1.1/32
default reject
}
forward . /etc/resolv.conf
}
~~~

View File

@@ -0,0 +1,81 @@
package proxyproto
import (
"errors"
"fmt"
"net"
"strings"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/pires/go-proxyproto"
)
func init() { plugin.Register("proxyproto", setup) }
func setup(c *caddy.Controller) error {
config := dnsserver.GetConfig(c)
if config.ProxyProtoConnPolicy != nil {
return plugin.Error("proxyproto", errors.New("proxy protocol already configured for this server instance"))
}
var (
allowedIPNets []*net.IPNet
policy = proxyproto.IGNORE
)
for c.Next() {
args := c.RemainingArgs()
if len(args) != 0 {
return plugin.Error("proxyproto", c.ArgErr())
}
for c.NextBlock() {
switch c.Val() {
case "allow":
for _, v := range c.RemainingArgs() {
_, ipnet, err := net.ParseCIDR(v)
if err != nil {
return plugin.Error("proxyproto", fmt.Errorf("%s: %w", v, err))
}
allowedIPNets = append(allowedIPNets, ipnet)
}
case "default":
v := c.RemainingArgs()
if len(v) != 1 {
return plugin.Error("proxyproto", c.ArgErr())
}
switch strings.ToLower(v[0]) {
case "use":
policy = proxyproto.USE
case "ignore":
policy = proxyproto.IGNORE
case "reject":
policy = proxyproto.REJECT
case "skip":
policy = proxyproto.SKIP
default:
return plugin.Error("proxyproto", c.ArgErr())
}
default:
return c.Errf("unknown option '%s'", c.Val())
}
}
}
config.ProxyProtoConnPolicy = func(connPolicyOptions proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) {
if len(allowedIPNets) == 0 {
return proxyproto.USE, nil
}
h, _, _ := net.SplitHostPort(connPolicyOptions.Upstream.String())
ip := net.ParseIP(h)
if ip == nil {
return proxyproto.REJECT, nil
}
for _, ipnet := range allowedIPNets {
if ipnet.Contains(ip) {
return proxyproto.USE, nil
}
}
return policy, nil
}
return nil
}

View File

@@ -0,0 +1,57 @@
package proxyproto
import (
"strings"
"testing"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
)
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
}{
// positive
{"proxyproto", false, "", "", true},
{"proxyproto {\nallow 127.0.0.1/8 ::1/128\n}", false, "", "", true},
{"proxyproto {\nallow 127.0.0.1/8 ::1/128\ndefault ignore\n}", false, "", "", true},
// Allow without any IPs is also valid
{"proxyproto {\nallow\n}", false, "", "", true},
// negative
{"proxyproto {\nunknown\n}", true, "", "unknown option", false},
{"proxyproto extra_arg", true, "", "Wrong argument", false},
{"proxyproto {\nallow invalid_ip\n}", true, "", "invalid CIDR address", false},
{"proxyproto {\nallow 127.0.0.1/8\ndefault invalid_policy\n}", true, "", "Wrong argument", false},
}
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 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)
}
}
}
}