Skip to content

Commit

Permalink
all: handle ptr, mx, https, and svcb dns rewrites
Browse files Browse the repository at this point in the history
  • Loading branch information
ainar-g committed Dec 22, 2020
1 parent d0665c1 commit bc2d41b
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 32 deletions.
80 changes: 80 additions & 0 deletions dnsrewrite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,12 @@ func TestDNSEngine_MatchRequest_dnsRewrite(t *testing.T) {
|refused_host^$dnsrewrite=REFUSED
|new_cname^$dnsrewrite=othercname
|new_mx^$dnsrewrite=NOERROR;MX;32 new_mx_host
|new_txt^$dnsrewrite=NOERROR;TXT;new_txtcontent
|1.2.3.4.in-addr.arpa^$dnsrewrite=NOERROR;PTR;new_ptr
|https_record^$dnsrewrite=NOERROR;HTTPS;32 https_record_host alpn=h3
|svcb_record^$dnsrewrite=NOERROR;SVCB;32 svcb_record_host alpn=h3
|https_type^$dnstype=HTTPS,dnsrewrite=REFUSED
Expand Down Expand Up @@ -318,6 +323,24 @@ func TestDNSEngine_MatchRequest_dnsRewrite(t *testing.T) {
}
})

t.Run("new_mx", func(t *testing.T) {
res, ok := dnsEngine.Match(path.Base(t.Name()))
assert.False(t, ok)

dnsr := res.DNSRewritesAll()
if assert.Len(t, dnsr, 1) {
nr := dnsr[0]
assert.Equal(t, dns.RcodeSuccess, nr.DNSRewrite.RCode)
assert.Equal(t, dns.TypeMX, nr.DNSRewrite.RRType)

mx := &rules.DNSMX{
Exchange: "new_mx_host",
Preference: 32,
}
assert.Equal(t, mx, nr.DNSRewrite.Value)
}
})

t.Run("new_txt", func(t *testing.T) {
res, ok := dnsEngine.Match(path.Base(t.Name()))
assert.False(t, ok)
Expand All @@ -331,6 +354,63 @@ func TestDNSEngine_MatchRequest_dnsRewrite(t *testing.T) {
}
})

t.Run("1.2.3.4.in-addr.arpa", func(t *testing.T) {
res, ok := dnsEngine.Match(path.Base(t.Name()))
assert.False(t, ok)

dnsr := res.DNSRewritesAll()
if assert.Len(t, dnsr, 1) {
nr := dnsr[0]
assert.Equal(t, dns.RcodeSuccess, nr.DNSRewrite.RCode)
assert.Equal(t, dns.TypePTR, nr.DNSRewrite.RRType)
assert.Equal(t, "new_ptr", nr.DNSRewrite.Value)
}
})

t.Run("https_record", func(t *testing.T) {
res, ok := dnsEngine.Match(path.Base(t.Name()))
assert.False(t, ok)

dnsr := res.DNSRewritesAll()
if assert.Len(t, dnsr, 1) {
nr := dnsr[0]
assert.Equal(t, dns.RcodeSuccess, nr.DNSRewrite.RCode)
assert.Equal(t, dns.TypeHTTPS, nr.DNSRewrite.RRType)

p := map[string]string{
"alpn": "h3",
}
svcb := &rules.DNSSVCB{
Params: p,
Target: "https_record_host",
Priority: 32,
}
assert.Equal(t, svcb, nr.DNSRewrite.Value)
}
})

t.Run("svcb_record", func(t *testing.T) {
res, ok := dnsEngine.Match(path.Base(t.Name()))
assert.False(t, ok)

dnsr := res.DNSRewritesAll()
if assert.Len(t, dnsr, 1) {
nr := dnsr[0]
assert.Equal(t, dns.RcodeSuccess, nr.DNSRewrite.RCode)
assert.Equal(t, dns.TypeSVCB, nr.DNSRewrite.RRType)

p := map[string]string{
"alpn": "h3",
}
svcb := &rules.DNSSVCB{
Params: p,
Target: "svcb_record_host",
Priority: 32,
}
assert.Equal(t, svcb, nr.DNSRewrite.Value)
}
})

t.Run("https_type", func(t *testing.T) {
r := DNSRequest{
Hostname: path.Base(t.Name()),
Expand Down
198 changes: 171 additions & 27 deletions rules/dnsrewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net"
"strconv"
"strings"

"github.com/miekg/dns"
Expand All @@ -16,25 +17,35 @@ type RCode = int
// type.
type RRType = uint16

// RRValue is the value of a resource record. If the coresponding RR is either
// dns.TypeA or dns.TypeAAAA, the underlying type of RRValue is net.IP. If the
// RR is dns.TypeTXT, the underlying type of Value is string. Otherwise,
// currently, it is nil. New types may be added in the future.
// RRValue is the value of a resource record.
//
// If the coresponding RRType is either dns.TypeA or dns.TypeAAAA, the
// underlying type of RRValue is net.IP.
//
// If the RRType is dns.TypeMX, the underlying value is a non-nil *DNSMX.
//
// If the RRType is either dns.TypePTR or dns.TypeTXT, the underlying type of
// Value is string.
//
// If the RRType is either dns.TypeHTTPS or dns.TypeSVCB, the underlying value
// is a non-nil *DNSSVCB.
//
// Otherwise, currently, it is nil. New types may be added in the future.
type RRValue = interface{}

// DNSRewrite is a DNS rewrite ($dnsrewrite) rule.
type DNSRewrite struct {
// RCode is the new DNS RCODE.
RCode RCode
// RRType is the new DNS resource record (RR) type. It is only non-zero
// if RCode is dns.RCodeSuccess.
RRType RRType
// Value is the value for the record. See the RRValue documentation for
// more details.
Value RRValue
// NewCNAME is the new CNAME. If set, clients must ignore other fields,
// resolve the CNAME, and set the new A and AAAA records accordingly.
NewCNAME string
// RCode is the new DNS RCODE.
RCode RCode
// RRType is the new DNS resource record (RR) type. It is only non-zero
// if RCode is dns.RCodeSuccess.
RRType RRType
}

// loadDNSRewrite loads the $dnsrewrite modifier.
Expand Down Expand Up @@ -107,27 +118,107 @@ func loadDNSRewriteShort(s string) (rewrite *DNSRewrite, err error) {
}, nil
}

// loadDNSRewritesNormal loads the normal version for of the $dnsrewrite
// modifier.
func loadDNSRewriteNormal(rcodeStr, rrStr, valStr string) (rewrite *DNSRewrite, err error) {
rcode, ok := dns.StringToRcode[strings.ToUpper(rcodeStr)]
if !ok {
return nil, fmt.Errorf("unknown rcode: %q", rcodeStr)
// DNSMX is the type of RRValue values returned for MX records in DNS rewrites.
type DNSMX struct {
Exchange string
Preference uint16
}

// DNSSVCB is the type of RRValue values returned for HTTPS and SVCB records in
// dns rewrites.
//
// See https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-01.
type DNSSVCB struct {
Params map[string]string
Target string
Priority uint16
}

// dnsRewriteRRHandler is a function that parses values for specific resource
// record types.
type dnsRewriteRRHandler func(rcode RCode, rr RRType, valStr string) (dnsr *DNSRewrite, err error)

// strDNSRewriteRRHandler is a simple DNS rewrite handler that returns
// a *DNSRewrite with Value st to valStr.
func strDNSRewriteRRHandler(rcode RCode, rr RRType, valStr string) (dnsr *DNSRewrite, err error) {
return &DNSRewrite{
RCode: rcode,
RRType: rr,
Value: valStr,
}, nil
}

// svcbDNSRewriteRRHandler is a DNS rewrite handler that parses SVCB and HTTPS
// rewrites.
//
// See https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-01.
//
// TODO(a.garipov): Currently, we only support the contiguous type of
// char-string values from the RFC.
func svcbDNSRewriteRRHandler(rcode RCode, rr RRType, valStr string) (dnsr *DNSRewrite, err error) {
var name string
switch rr {
case dns.TypeHTTPS:
name = "https"
case dns.TypeSVCB:
name = "svcb"
default:
return nil, fmt.Errorf("unsupported svcb-like rr type: %d", rr)
}

if rcode != dns.RcodeSuccess {
fields := strings.Split(valStr, " ")
if len(fields) < 2 {
return nil, fmt.Errorf("invalid %s %q: need at least two fields", name, valStr)
}

var prio64 uint64
prio64, err = strconv.ParseUint(fields[0], 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid %s priority: %w", name, err)
}

if len(fields) == 2 {
v := &DNSSVCB{
Priority: uint16(prio64),
Target: fields[1],
}

return &DNSRewrite{
RCode: rcode,
RCode: rcode,
RRType: rr,
Value: v,
}, nil
}

rr, err := strToRR(rrStr)
if err != nil {
return nil, err
params := make(map[string]string, len(fields)-2)
for i, pair := range fields[2:] {
kv := strings.Split(pair, "=")
if l := len(kv); l != 2 {
return nil, fmt.Errorf("invalid %s param at index %d: got %d fields", name, i, l)
}

// TODO(a.garipov): Validate for uniqueness? Validate against
// the currently specified list of params from the RFC?
params[kv[0]] = kv[1]
}

switch rr {
case dns.TypeA:
v := &DNSSVCB{
Priority: uint16(prio64),
Target: fields[1],
Params: params,
}

return &DNSRewrite{
RCode: rcode,
RRType: rr,
Value: v,
}, nil
}

// dnsRewriteRRHandlers are the supported resource record types' rewrite
// handlers.
var dnsRewriteRRHandlers = map[RRType]dnsRewriteRRHandler{
dns.TypeA: func(rcode RCode, rr RRType, valStr string) (dnsr *DNSRewrite, err error) {
ip := net.ParseIP(valStr)
if ip4 := ip.To4(); ip4 == nil {
return nil, fmt.Errorf("invalid ipv4: %q", valStr)
Expand All @@ -138,7 +229,9 @@ func loadDNSRewriteNormal(rcodeStr, rrStr, valStr string) (rewrite *DNSRewrite,
RRType: rr,
Value: ip,
}, nil
case dns.TypeAAAA:
},

dns.TypeAAAA: func(rcode RCode, rr RRType, valStr string) (dnsr *DNSRewrite, err error) {
ip := net.ParseIP(valStr)
if ip == nil {
return nil, fmt.Errorf("invalid ipv6: %q", valStr)
Expand All @@ -151,20 +244,71 @@ func loadDNSRewriteNormal(rcodeStr, rrStr, valStr string) (rewrite *DNSRewrite,
RRType: rr,
Value: ip,
}, nil
case dns.TypeCNAME:
},

dns.TypeCNAME: func(rcode RCode, rr RRType, valStr string) (dnsr *DNSRewrite, err error) {
return &DNSRewrite{
NewCNAME: valStr,
}, nil
case dns.TypeTXT:
},

dns.TypeMX: func(rcode RCode, rr RRType, valStr string) (dnsr *DNSRewrite, err error) {
parts := strings.SplitN(valStr, " ", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid mx: %q", valStr)
}

var pref64 uint64
pref64, err = strconv.ParseUint(parts[0], 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid mx preference: %w", err)
}

v := &DNSMX{
Exchange: parts[1],
Preference: uint16(pref64),
}

return &DNSRewrite{
RCode: rcode,
RRType: rr,
Value: valStr,
Value: v,
}, nil
default:
},

dns.TypePTR: strDNSRewriteRRHandler,
dns.TypeTXT: strDNSRewriteRRHandler,

dns.TypeHTTPS: svcbDNSRewriteRRHandler,
dns.TypeSVCB: svcbDNSRewriteRRHandler,
}

// loadDNSRewritesNormal loads the normal version for of the $dnsrewrite
// modifier.
func loadDNSRewriteNormal(rcodeStr, rrStr, valStr string) (rewrite *DNSRewrite, err error) {
rcode, ok := dns.StringToRcode[strings.ToUpper(rcodeStr)]
if !ok {
return nil, fmt.Errorf("unknown rcode: %q", rcodeStr)
}

if rcode != dns.RcodeSuccess {
return &DNSRewrite{
RCode: rcode,
}, nil
}

rr, err := strToRRType(rrStr)
if err != nil {
return nil, err
}

handler, ok := dnsRewriteRRHandlers[rr]
if !ok {
return &DNSRewrite{
RCode: rcode,
RRType: rr,
}, nil
}

return handler(rcode, rr, valStr)
}
Loading

0 comments on commit bc2d41b

Please sign in to comment.