From 8f35270033a168d3d7ca5e500d03aa86b0a7096f Mon Sep 17 00:00:00 2001 From: Peter Verraedt Date: Sat, 15 Nov 2025 17:10:13 +0100 Subject: [PATCH 1/2] Integrate proxyproto for TCP and TLS listeners If the connection is made from one of the trusted proxies ip addresses, it is allowed that TCP and TLS connections contain a proxyprotocol header to pass source connection information. This in particular allows dns over tls behind a load balancer, while keeping source ip address information. Signed-off-by: Peter Verraedt --- go.mod | 1 + go.sum | 2 ++ proxy/servertcp.go | 34 ++++++++++++++++++++++++++++++---- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 918f47b86..d622f2d4b 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect github.com/jstemmer/go-junit-report/v2 v2.1.0 // indirect github.com/kisielk/errcheck v1.9.0 // indirect + github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 60fd3ba19..2415e1f4c 100644 --- a/go.sum +++ b/go.sum @@ -84,6 +84,8 @@ github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= +github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= diff --git a/proxy/servertcp.go b/proxy/servertcp.go index 8cb9fe655..3b1f1aee6 100644 --- a/proxy/servertcp.go +++ b/proxy/servertcp.go @@ -16,12 +16,14 @@ import ( "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/syncutil" "github.com/miekg/dns" + + proxyproto "github.com/pires/go-proxyproto" ) // initTCPListeners initializes TCP listeners with configured addresses. func (p *Proxy) initTCPListeners(ctx context.Context) (err error) { for _, addr := range p.TCPListenAddr { - var ln *net.TCPListener + var ln net.Listener ln, err = p.listenTCP(ctx, addr) if err != nil { return fmt.Errorf("listening on tcp addr %s: %w", addr, err) @@ -34,7 +36,7 @@ func (p *Proxy) initTCPListeners(ctx context.Context) (err error) { } // listenTCP returns a new TCP listener listening on addr. -func (p *Proxy) listenTCP(ctx context.Context, addr *net.TCPAddr) (ln *net.TCPListener, err error) { +func (p *Proxy) listenTCP(ctx context.Context, addr *net.TCPAddr) (ln net.Listener, err error) { addrStr := addr.String() p.logger.InfoContext(ctx, "creating tcp server socket", "addr", addrStr) @@ -60,7 +62,29 @@ func (p *Proxy) listenTCP(ctx context.Context, addr *net.TCPAddr) (ln *net.TCPLi p.logger.InfoContext(ctx, "listening to tcp", "addr", ln.Addr()) - return ln, nil + return p.wrapProxyListener(ln), nil +} + +// wrapProxyListener wraps a net.Listener with a proxyproto.Listener that +// implements the ConnPolicy callback. If the upstream address is in +// p.TrustedProxies, it returns proxyproto.USE; otherwise, it returns +// proxyproto.REJECT. +func (p *Proxy) wrapProxyListener(ln net.Listener) net.Listener { + return &proxyproto.Listener{ + Listener: ln, + ConnPolicy: func(options proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) { + if p.TrustedProxies != nil && p.TrustedProxies.Contains(netutil.NetAddrToAddrPort(options.Upstream).Addr()) { + // If a proxyproto header is present, use it to determine the + // upstream address. + return proxyproto.USE, nil + } + + // Reject connections if the proxyproto header is present, + // with reason (will be logged): + // proxyproto: upstream connection sent PROXY header but isn't allowed to send one + return proxyproto.REJECT, nil + }, + } } // initTLSListeners initializes TLS listeners with configured addresses. @@ -78,7 +102,9 @@ func (p *Proxy) initTLSListeners(ctx context.Context) (err error) { return fmt.Errorf("listening on tls addr %s: %w", addr, err) } - l := tls.NewListener(tcpListen, p.TLSConfig) + proxyListen := p.wrapProxyListener(tcpListen) + + l := tls.NewListener(proxyListen, p.TLSConfig) p.tlsListen = append(p.tlsListen, l) p.logger.InfoContext(ctx, "listening to tls", "addr", l.Addr()) From 3566fc8e3edd103759deeac89ae73abab3e9f0ac Mon Sep 17 00:00:00 2001 From: Peter Verraedt Date: Fri, 27 Mar 2026 09:39:49 +0100 Subject: [PATCH 2/2] Add support for proxy protocol for UDP Signed-off-by: Peter Verraedt --- proxy/serverudp.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/proxy/serverudp.go b/proxy/serverudp.go index f9e1c3d1d..665b197d9 100644 --- a/proxy/serverudp.go +++ b/proxy/serverudp.go @@ -1,8 +1,11 @@ package proxy import ( + "bufio" + "bytes" "context" "fmt" + "io" "log/slog" "net" "net/netip" @@ -14,6 +17,7 @@ import ( "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/syncutil" "github.com/miekg/dns" + proxyproto "github.com/pires/go-proxyproto" ) // initUDPListeners initializes UDP listeners with configured addresses. @@ -136,6 +140,8 @@ func (p *Proxy) udpHandlePacket( ) { p.logger.Debug("handling new udp packet", "raddr", remoteAddr) + packet, remoteAddr = p.parseUDPProxyHeader(packet, remoteAddr) + req := &dns.Msg{} err := req.Unpack(packet) if err != nil { @@ -154,6 +160,42 @@ func (p *Proxy) udpHandlePacket( } } +// parseUDPProxyHeader attempts to parse a proxy protocol header from a UDP +// packet. If the remote address is in p.TrustedProxies and a valid proxy +// protocol header is present, it returns the remaining packet data and the +// source address from the header. Otherwise, it returns the original packet +// and remote address unchanged. +func (p *Proxy) parseUDPProxyHeader(packet []byte, remoteAddr *net.UDPAddr) ([]byte, *net.UDPAddr) { + if p.TrustedProxies == nil || !p.TrustedProxies.Contains(netutil.NetAddrToAddrPort(remoteAddr).Addr()) { + return packet, remoteAddr + } + + reader := bufio.NewReader(bytes.NewReader(packet)) + header, err := proxyproto.Read(reader) + if err != nil { + // No proxy protocol header found; return packet as-is. + return packet, remoteAddr + } + + // Read the remaining bytes after the proxy protocol header; these are the + // actual DNS payload. + remaining, err := io.ReadAll(reader) + if err != nil { + p.logger.Error("reading remaining udp data after proxy header", slogutil.KeyError, err) + + return packet, remoteAddr + } + + srcUDPAddr, ok := header.SourceAddr.(*net.UDPAddr) + if ok { + return remaining, srcUDPAddr + } + + p.logger.Debug("proxy protocol header has unsupported source address type", "addr", header.SourceAddr) + + return remaining, remoteAddr +} + // Writes a response to the UDP client func (p *Proxy) respondUDP(d *DNSContext) error { resp := d.Res