Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Listeners receive queries over any supported protocol. Routers, groups and modif
**Deployment**
- Linux network namespace support — listen in one netns, resolve in another
- Firewall mark (fwmark) and interface binding (SO_BINDTODEVICE) for policy routing and VRF
- PROXY protocol v1/v2 support for preserving client IPs behind load balancers
- Admin listener with expvar metrics (Prometheus-compatible)
- Query/response logging, syslog integration
- Platform independent — written in Go
Expand Down
1 change: 1 addition & 0 deletions adminlistener.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func (s *AdminListener) startTCP() error {
if err != nil {
return err
}
ln = proxyProtocolListener(ln, s.opt.ProxyProtocol)
defer ln.Close()
return s.httpServer.ServeTLS(ln, "", "")
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/routedns/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ type listener struct {
AllowDoH bool `toml:"allow-doh"` // Allow ODoH listeners to also handle DoH queries to /dns-query
NetNS string `toml:"netns"` // Linux network namespace name or absolute path
FWMark uint32 `toml:"fwmark"` // Linux firewall mark (SO_MARK) for the listening socket
BindInterface string `toml:"bind-if"` // Linux network interface to bind the socket to (SO_BINDTODEVICE)
BindInterface string `toml:"bind-if"` // Linux network interface to bind the socket to (SO_BINDTODEVICE)
ProxyProtocol bool `toml:"proxy-protocol"` // Enable PROXY protocol v1/v2 header parsing on TCP listeners
Frontend dohFrontend
}

Expand Down
1 change: 1 addition & 0 deletions cmd/routedns/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ func start(opt options, args []string) error {
AllowedNet: allowedNet,
NetNS: netns,
SocketOptions: rdns.SocketOptions{FWMark: l.FWMark, BindInterface: l.BindInterface},
ProxyProtocol: l.ProxyProtocol,
}

switch l.Protocol {
Expand Down
7 changes: 5 additions & 2 deletions dnslistener.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type ListenOptions struct {

// Linux socket options for fwmark and interface binding.
SocketOptions SocketOptions

// Enable PROXY protocol v1/v2 header parsing on incoming TCP connections.
ProxyProtocol bool
}

// NewDNSListener returns an instance of either a UDP or TCP DNS listener.
Expand All @@ -46,7 +49,7 @@ func NewDNSListener(id, addr, net string, opt ListenOptions, resolver Resolver)
// Start the DNS listener.
func (s DNSListener) Start() error {
Log.Info("starting listener", "id", s.id, "protocol", s.Net, "addr", s.Addr)
if (s.opt.NetNS != nil && s.opt.NetNS.Name != "") || s.opt.SocketOptions.active() {
if (s.opt.NetNS != nil && s.opt.NetNS.Name != "") || s.opt.SocketOptions.active() || (s.opt.ProxyProtocol && strings.HasPrefix(s.Net, "tcp")) {
return s.startWithSocketSetup()
}
return s.ListenAndServe()
Expand All @@ -59,7 +62,7 @@ func (s DNSListener) startWithSocketSetup() error {
if err != nil {
return err
}
s.Server.Listener = ln
s.Server.Listener = proxyProtocolListener(ln, s.opt.ProxyProtocol)
case strings.HasPrefix(s.Net, "udp"):
pc, err := ListenPacketInNetNS(context.Background(), s.opt.NetNS, s.Net, s.Addr, s.opt.SocketOptions)
if err != nil {
Expand Down
44 changes: 44 additions & 0 deletions doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
- [SOCKS5 Proxy Support](#socks5-proxy-support)
- [Network Namespace Support](#network-namespace-support)
- [Firewall Mark and Interface Binding](#firewall-mark-and-interface-binding)
- [PROXY Protocol Support](#proxy-protocol-support)
- [Templates](#templates)

## Overview
Expand Down Expand Up @@ -134,6 +135,7 @@ Common options for all listeners:
- `netns` - Linux network namespace for the listening socket. Can be a name (looked up in `/var/run/netns/`) or an absolute path (e.g. `/proc/PID/ns/net`). Optional, Linux only. See [Network Namespace Support](#network-namespace-support).
- `fwmark` - Linux firewall mark (`SO_MARK`) to set on the listening socket. Used for netfilter matching and policy routing. Optional, Linux only, integer. See [Firewall Mark and Interface Binding](#firewall-mark-and-interface-binding).
- `bind-if` - Bind the listening socket to a specific network interface (`SO_BINDTODEVICE`). Useful for VRFs or restricting a listener to one interface. Optional, Linux only. See [Firewall Mark and Interface Binding](#firewall-mark-and-interface-binding).
- `proxy-protocol` - Enable PROXY protocol v1/v2 header parsing on incoming connections. When enabled, the real client IP from the PROXY header (sent by an upstream load balancer) is used instead of the direct connection's remote address. Only applies to TCP-based listeners (`tcp`, `dot`, `doh` with TCP transport). Optional, defaults to `false`. See [PROXY Protocol Support](#proxy-protocol-support).

Secure listeners, such as DNS-over-TLS, DNS-over-HTTPS, DNS-over-DTLS, DNS-over-QUIC and Admin support additional options to configure certificates, keys and peer validation.

Expand Down Expand Up @@ -2234,6 +2236,48 @@ fwmark = 12

Example config files: [fwmark-bind-if.toml](../cmd/routedns/example-config/fwmark-bind-if.toml)

### PROXY Protocol Support

When RouteDNS is deployed behind a load balancer (e.g., HAProxy, NGINX, or cloud load balancers), the real client IP is normally lost because all connections appear to come from the load balancer. The [PROXY protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) solves this by having the load balancer prepend a header with the original client IP to each TCP connection.

Setting `proxy-protocol = true` on a listener enables parsing of PROXY protocol v1 (text) and v2 (binary) headers. The real client IP from the header is then used throughout the pipeline — in logging, ACLs, routing, and any other component that uses the client IP.

This option only applies to TCP-based listeners: `tcp`, `dot`, `doh` (with TCP transport), `admin` (with TCP transport), and `odoh`. UDP-based listeners (`udp`, `doq`, `dtls`, DoH with QUIC transport) are not affected.

**TCP listener behind a load balancer:**

```toml
[listeners.tcp-behind-lb]
address = ":53"
protocol = "tcp"
resolver = "upstream"
proxy-protocol = true
```

**DoT listener behind a load balancer:**

```toml
[listeners.dot-behind-lb]
address = ":853"
protocol = "dot"
resolver = "upstream"
server-crt = "/path/to/server.crt"
server-key = "/path/to/server.key"
proxy-protocol = true
```

**DoH listener behind a load balancer:**

```toml
[listeners.doh-behind-lb]
address = ":443"
protocol = "doh"
resolver = "upstream"
server-crt = "/path/to/server.crt"
server-key = "/path/to/server.key"
proxy-protocol = true
```

## Templates

Some groups support templates, i.e. allow placeholder in text fields that will be populated at runtime with data from a query. This can for example be used in the extended error text returned from a blocklist. In that case, the configuration would set a text with placeholders like this `"Blocked {{ .Question }} with ID {{ .ID }} because reasons"`. The placeholders in between `{{` and `}}` would then be replaced with data from the query when a query is blocked and the response returned. The template syntax is explained in more detail [here](https://pkg.go.dev/text/template).
Expand Down
1 change: 1 addition & 0 deletions dohlistener.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ func (s *DoHListener) startTCP() error {
if err != nil {
return err
}
ln = proxyProtocolListener(ln, s.opt.ProxyProtocol)
defer ln.Close()
if s.opt.NoTLS {
return s.httpServer.Serve(ln)
Expand Down
4 changes: 2 additions & 2 deletions dotlistener.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ func (s DoTListener) Start() error {
"id", s.id,
"protocol", "dot",
"addr", s.Addr)
if (s.opt.NetNS != nil && s.opt.NetNS.Name != "") || s.opt.SocketOptions.active() {
if (s.opt.NetNS != nil && s.opt.NetNS.Name != "") || s.opt.SocketOptions.active() || s.opt.ProxyProtocol {
ln, err := ListenInNetNS(context.Background(), s.opt.NetNS, "tcp", s.Addr, s.opt.SocketOptions)
if err != nil {
return err
}
s.Server.Listener = tls.NewListener(ln, s.Server.TLSConfig)
s.Server.Listener = tls.NewListener(proxyProtocolListener(ln, s.opt.ProxyProtocol), s.Server.TLSConfig)
return s.ActivateAndServe()
}
return s.ListenAndServe()
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/miekg/dns v1.1.69
github.com/oschwald/maxminddb-golang v1.13.1
github.com/pion/dtls/v3 v3.1.2
github.com/pires/go-proxyproto v0.11.0
github.com/pkg/errors v0.9.1
github.com/quic-go/quic-go v0.57.1
github.com/redis/go-redis/v9 v9.17.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
5 changes: 3 additions & 2 deletions odohlistener.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ func NewODoHListener(id, addr string, opt ODoHListenerOptions, resolver Resolver
}

dohOpt := DoHListenerOptions{
TLSConfig: opt.TLSConfig,
customMux: mux,
ListenOptions: opt.ListenOptions,
TLSConfig: opt.TLSConfig,
customMux: mux,
}
dohListen, err := NewDoHListener(id, addr, dohOpt, resolver)
if err != nil {
Expand Down
18 changes: 18 additions & 0 deletions proxyproto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package rdns

import (
"net"

proxyproto "github.com/pires/go-proxyproto"
)

// proxyProtocolListener wraps a net.Listener with PROXY protocol v1/v2
// header parsing if enabled. When enabled, accepted connections return
// the real client IP from RemoteAddr() as conveyed by an upstream load
// balancer via the PROXY protocol header.
func proxyProtocolListener(ln net.Listener, enabled bool) net.Listener {
if !enabled {
return ln
}
return &proxyproto.Listener{Listener: ln}
}
Loading