Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Usage of ./dnsproxy:
If specified, dnsproxy will act as a DNS64 server.
--dns64-prefix=subnet
Prefix used to handle DNS64. If not specified, dnsproxy uses the 'Well-Known Prefix' 64:ff9b::. Can be specified multiple times.
--disable-dns-cookies
Disable DNS cookies support (which is enabled by default).
--dnscrypt-config=path/-g path
Path to a file with DNSCrypt configuration. You can generate one using https://github.com/ameshkov/dnscrypt.
--dnscrypt-port=port/-y port
Expand Down Expand Up @@ -304,6 +306,12 @@ Loads upstreams list from a file.
./dnsproxy -l 127.0.0.1 -p 5353 -u ./upstreams.txt
```

DNS cookies are enabled by default. Disable them if needed:

```shell
./dnsproxy --disable-dns-cookies
```

### DNS64 server

`dnsproxy` is capable of working as a DNS64 server.
Expand Down
16 changes: 16 additions & 0 deletions internal/cmd/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const (
httpsUserinfoIdx
dnsCryptConfigPathIdx
ednsAddrIdx
disableDNSCookiesIdx
dnsCookieSecretIdx
upstreamModeIdx
listenAddrsIdx
listenPortsIdx
Expand Down Expand Up @@ -132,6 +134,18 @@ var commandLineOptions = []*commandLineOption{
short: "",
valueType: "address",
},
disableDNSCookiesIdx: {
description: "Disable EDNS Cookies support.",
long: "disable-dns-cookies",
short: "",
valueType: "",
},
dnsCookieSecretIdx: {
description: "Hex-encoded 16-byte secret used to generate EDNS Cookies.",
long: "dns-cookie-secret",
short: "",
valueType: "hex",
},
upstreamModeIdx: {
description: "Defines the upstreams logic mode, possible values: load_balance, parallel, " +
"fastest_addr (default: load_balance).",
Expand Down Expand Up @@ -421,6 +435,8 @@ func parseCmdLineOptions(conf *configuration) (err error) {
httpsUserinfoIdx: &conf.HTTPSUserinfo,
dnsCryptConfigPathIdx: &conf.DNSCryptConfigPath,
ednsAddrIdx: &conf.EDNSAddr,
disableDNSCookiesIdx: &conf.DisableDNSCookies,
dnsCookieSecretIdx: &conf.DNSCookieSecret,
upstreamModeIdx: &conf.UpstreamMode,
listenAddrsIdx: &conf.ListenAddrs,
listenPortsIdx: &conf.ListenPorts,
Expand Down
7 changes: 7 additions & 0 deletions internal/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ type configuration struct {
// EDNSAddr is the custom EDNS Client Address to send.
EDNSAddr string `yaml:"edns-addr"`

// DisableDNSCookies strips EDNS Cookies from both requests and responses.
DisableDNSCookies bool `yaml:"disable_dns_cookies"`

// DNSCookieSecret is a hex-encoded 16-byte secret used to generate server
// cookies.
DNSCookieSecret string `yaml:"dns_cookie_secret"`

// UpstreamMode determines the logic through which upstreams will be used.
// If not specified the [proxy.UpstreamModeLoadBalance] is used.
UpstreamMode string `yaml:"upstream-mode"`
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func createProxyConfig(
netip.MustParsePrefix("0.0.0.0/0"),
netip.MustParsePrefix("::0/0"),
},
DisableDNSCookies: conf.DisableDNSCookies,
DNSCookieSecret: conf.DNSCookieSecret,
EnableEDNSClientSubnet: conf.EnableEDNSSubnet,
UDPBufferSize: conf.UDPBufferSize,
HTTPSServerName: conf.HTTPSServerName,
Expand Down
8 changes: 8 additions & 0 deletions proxy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ type Config struct {
// EDNSAddr is the ECS IP used in request.
EDNSAddr net.IP

// DisableDNSCookies strips EDNS Cookies from both requests and responses.
// If false, the proxy responds with EDNS Cookies per RFC 7873 and RFC 9018.
DisableDNSCookies bool `yaml:"disable_dns_cookies" json:"disable_dns_cookies"`

// DNSCookieSecret is a hex-encoded 16-byte secret used to generate server
// cookies. If empty, a random secret is generated on start.
DNSCookieSecret string `yaml:"dns_cookie_secret" json:"dns_cookie_secret"`

// TODO(s.chzhen): Extract ratelimit settings to a separate structure.

// RatelimitSubnetLenIPv4 is a subnet length for IPv4 addresses used for
Expand Down
247 changes: 247 additions & 0 deletions proxy/cookie.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package proxy

import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/netip"

"github.com/miekg/dns"
)

const (
// clientCookieLen is the length of the client cookie in bytes.
clientCookieLen = 8

// serverCookieLen is the length of the server cookie in bytes.
serverCookieLen = 16

// cookieSecretLen is the size of the secret used to generate server
// cookies.
cookieSecretLen = 16

// doBitMask is the mask of the DO bit in the TTL field of an OPT record.
doBitMask = uint32(1 << 15)
)

// parseCookie returns the client and server cookies from m if present. The
// client cookie is required to be at least eight bytes long.
func parseCookie(m *dns.Msg) (client, server []byte) {
if m == nil {
return nil, nil
}

opt := m.IsEdns0()
if opt == nil {
return nil, nil
}

for _, o := range opt.Option {
c, ok := o.(*dns.EDNS0_COOKIE)
if !ok {
continue
}

raw, err := hex.DecodeString(c.Cookie)
if err != nil || len(raw) < clientCookieLen {
continue
}
Comment thread
0ln marked this conversation as resolved.

client = raw[:clientCookieLen]
if len(raw) > clientCookieLen {
server = raw[clientCookieLen:]
}

return client, server
}

return nil, nil
}

// stripCookie removes any EDNS cookie options from m.
func stripCookie(m *dns.Msg) {
if m == nil {
return
}

opt := m.IsEdns0()
if opt == nil {
return
}

opts := opt.Option[:0]
for _, o := range opt.Option {
if o.Option() == dns.EDNS0COOKIE {
continue
}

opts = append(opts, o)
}

opt.Option = opts
}

// handleRequestCookies parses client cookies and strips cookie options before
// forwarding requests upstream.
func (p *Proxy) handleRequestCookies(dctx *DNSContext) {
if dctx == nil || dctx.Req == nil {
return
}

if !p.DisableDNSCookies {
if client, _ := parseCookie(dctx.Req); len(client) > 0 {
dctx.ReqClientCookie = client
}

stripCookie(dctx.Req)

return
}

stripCookie(dctx.Req)
}

// handleResponseCookies sets the response cookie if needed or strips cookie
// options when cookies are disabled.
func (p *Proxy) handleResponseCookies(dctx *DNSContext) {
if dctx == nil || dctx.Res == nil {
return
}

if p.DisableDNSCookies {
stripCookie(dctx.Res)

return
}

if len(dctx.ReqClientCookie) == 0 {
stripCookie(dctx.Res)

return
}

udpSize := dctx.udpSize
if udpSize == 0 {
udpSize = defaultUDPBufSize
}

stripCookie(dctx.Res)

server := p.serverCookie(dctx.Addr.Addr(), dctx.ReqClientCookie)
if len(server) == 0 {
return
}

setCookie(dctx.Res, dctx.ReqClientCookie, server, udpSize, dctx.doBit)
}

// serverCookie returns the server cookie for the provided address and client
// cookie using an HMAC-SHA256.
func (p *Proxy) serverCookie(ip netip.Addr, client []byte) (server []byte) {
if len(client) < clientCookieLen || !ip.IsValid() {
return nil
}

p.cookieMu.RLock()
secret := p.cookieSecret
p.cookieMu.RUnlock()

if len(secret) != cookieSecretLen {
return nil
}

h := hmac.New(sha256.New, secret)
_, _ = h.Write(client)
_, _ = h.Write(ip.AsSlice())
Comment thread
0ln marked this conversation as resolved.

// Truncate to sixteen bytes.
return h.Sum(nil)[:serverCookieLen]
}

// setCookie ensures msg has the OPT record and sets the cookie option to
// client+server. udpSize is the UDP buffer size advertised to the client.
func setCookie(msg *dns.Msg, client, server []byte, udpSize uint16, do bool) {
if msg == nil || len(client) == 0 || len(server) == 0 {
return
}

opt := msg.IsEdns0()
if opt == nil {
msg.SetEdns0(udpSize, do)
opt = msg.IsEdns0()
} else {
opt.SetUDPSize(udpSize)
if do {
opt.SetDo()
} else {
opt.Hdr.Ttl &^= doBitMask
}
}

stripCookie(msg)

cookie := make([]byte, 0, len(client)+len(server))
cookie = append(cookie, client...)
cookie = append(cookie, server...)

opt = msg.IsEdns0()
opt.Option = append(opt.Option, &dns.EDNS0_COOKIE{
Code: dns.EDNS0COOKIE,
Cookie: hex.EncodeToString(cookie),
})
}

// decodeCookieSecret decodes hexSecret and validates its length.
func decodeCookieSecret(hexSecret string) (secret []byte, err error) {
secret, err = hex.DecodeString(hexSecret)
if err != nil {
return nil, fmt.Errorf("decoding dns cookie secret: %w", err)
}

if len(secret) != cookieSecretLen {
return nil, fmt.Errorf(
"decoding dns cookie secret: invalid length %d, want %d",
len(secret),
cookieSecretLen,
)
}

return secret, nil
}

// generateCookieSecret returns a new random secret suitable for cookie
// generation.
func generateCookieSecret() (secret []byte, err error) {
secret = make([]byte, cookieSecretLen)
_, err = rand.Read(secret)
if err != nil {
return nil, fmt.Errorf("reading random secret: %w", err)
}

return secret, nil
}

// initCookieSecret prepares the cookie secret depending on the configuration.
func (p *Proxy) initCookieSecret() (err error) {
if p.DisableDNSCookies {
return nil
}

var secret []byte
if p.DNSCookieSecret != "" {
secret, err = decodeCookieSecret(p.DNSCookieSecret)
} else {
secret, err = generateCookieSecret()
}
if err != nil {
return fmt.Errorf("creating dns cookie secret: %w", err)
}

p.cookieMu.Lock()
p.cookieSecret = secret
p.cookieMu.Unlock()

return nil
}
Loading