Rewrite edns0 (#561)

* Add edns0 code rewrite

* check arg count

* change `new`; set EDNS0 if request doesn't have it set

* change set to replace_or_append

* change to append_or_replace

* return error in new

* update documents

* fixt UT

* return error

* go fmt

* Rework for more general EDNS0 use

Also changed how rules are created and validated. Implements
EDNS0 NSID in addition to local.

* go fmt

* README updates, NSID tests and fixes

* gofmt -s -w

* Fix tests for rewrite syntax change

* Add tests, fix error message

* Review nits

* Missed on nit

* More tests, integration test, fix edns0 parse issue

* Fix README, use RewriteIgnored

* go fmt
This commit is contained in:
John Belamaric 2017-03-06 16:32:17 -05:00 committed by Miek Gieben
parent d1bb4ea130
commit ef315ef3e2
13 changed files with 564 additions and 262 deletions

View file

@ -23,79 +23,37 @@ rewritten; e.g., to rewrite CH queries to IN use `rewrite class CH IN`.
When the FIELD is `name` the query name in the message is rewritten; this When the FIELD is `name` the query name in the message is rewritten; this
needs to be a full match of the name, e.g., `rewrite name miek.nl example.org`. needs to be a full match of the name, e.g., `rewrite name miek.nl example.org`.
When the FIELD is `edns0` an EDNS0 option can be appended to the request as described below.
If you specify multiple rules and an incoming query matches on multiple (simple) rules, only If you specify multiple rules and an incoming query matches on multiple (simple) rules, only
the first rewrite is applied. the first rewrite is applied.
## EDNS0 Options
> Everything below this line has not been implemented, yet. Using FIELD edns0, you can set, append, or replace specific EDNS0 options on the request.
* `replace` will modify any matching (what that means may vary based on EDNS0 type) option with the specified option
* `append` will add the option regardless of what options already exist
* `set` will modify a matching option or add one if none is found
Currently supported are `EDNS0_LOCAL` and `EDNS0_NSID`.
### `EDNS0_LOCAL`
This has two fields, code and data. A match is defined as having the same code. Data may be a string, or if
it starts with `0x` it will be treated as hex. Example:
~~~ ~~~
rewrite [basename] { rewrite edns0 local set 0xffee 0x61626364
regexp pattern
ext extensions...
if a cond b
status code
to destinations...
}
~~~ ~~~
* basepath is the base path to match before rewriting with a regular expression. Default is /. rewrites the first local option with code 0xffee, setting the data to "abcd". Equivalent:
* regexp (shorthand: r) will match the path with the given regular expression pattern. Extremely high-load servers should avoid using regular expressions.
* extensions... is a space-separated list of file extensions to include or ignore. Prefix an extension with ! to exclude an extension. The forward slash / symbol matches paths without file extensions.
* if specifies a rewrite condition. Multiple ifs are AND-ed together. a and b are any string and may use request placeholders. cond is the condition, with possible values explained below.
* status will respond with the given status code instead of performing a rewrite. In other words, use either "status" or "to" in your rule, but not both. The code must be a number in the format 2xx or 4xx.
* destinations... is one or more space-separated paths to rewrite to, with support for request placeholders as well as numbered regular expression captures such as {1}, {2}, etc. Rewrite will check each destination in order and rewrite to the first destination that exists. Each one is checked as a file or, if it ends with /, as a directory. The last destination will act as the default if no other destination exists.
"if" Conditions
The if keyword is a powerful way to describe your rule. It takes the format a cond b, where the values a and b are separated by cond, a condition. The condition can be any of these:
~~~ ~~~
is = a equals b rewrite edns0 local set 0xffee abcd
not = a does NOT equal b
has = a has b as a substring (b is a substring of a)
not_has = b is NOT a substring of a
starts_with = b is a prefix of a
ends_with = b is a suffix of a
match = a matches b, where b is a regular expression
not_match = a does NOT match b, where b is a regular expression
~~~ ~~~
## Examples ### `EDNS0_NSID`
When requests come in for /mobile, actually serve /mobile/index. This has no fields; it will add an NSID option with an empty string for the NSID. If the option already exists
rewrite /mobile /mobile/index and the action is `replace` or `set`, then the NSID in the option will be set to the empty string.
If the file is not favicon.ico and it is not a valid file or directory, serve the maintenance page if present, or finally, rewrite to index.php.
~~~
rewrite {
if {file} not favicon.ico
to {path} {path}/ /maintenance.html /index.php
}
~~~
If user agent includes "mobile" and path is not a valid file/directory, rewrite to the mobile index page.
~~~
rewrite {
if if {>User-agent} has mobile
to {path} {path}/ /mobile/index.php
}
~~~
If the request path starts with /source, respond with HTTP 403 Forbidden.
~~~
rewrite {
regexp ^/source
status 403
}
~~~
Rewrite /app to /index with a query string. {1} is the matched group (.*).
~~~
rewrite /app {
r (.*)
to /index?path={1}
}
~~~

View file

@ -1,26 +1,30 @@
// Package rewrite is middleware for rewriting requests internally to something different.
package rewrite package rewrite
import ( import (
"fmt"
"strings" "strings"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// ClassRule is a class rewrite rule. type classRule struct {
type ClassRule struct {
fromClass, toClass uint16 fromClass, toClass uint16
} }
// New initializes rule. func newClassRule(fromS, toS string) (Rule, error) {
func (rule ClassRule) New(args ...string) Rule { var from, to uint16
from, to := args[0], strings.Join(args[1:], " ") var ok bool
return &ClassRule{dns.StringToClass[from], dns.StringToClass[to]} if from, ok = dns.StringToClass[strings.ToUpper(fromS)]; !ok {
return nil, fmt.Errorf("invalid class %q", strings.ToUpper(fromS))
}
if to, ok = dns.StringToClass[strings.ToUpper(toS)]; !ok {
return nil, fmt.Errorf("invalid class %q", strings.ToUpper(toS))
}
return &classRule{fromClass: from, toClass: to}, nil
} }
// Rewrite rewrites the the current request. // Rewrite rewrites the the current request.
func (rule ClassRule) Rewrite(r *dns.Msg) Result { func (rule *classRule) Rewrite(r *dns.Msg) Result {
if rule.fromClass > 0 && rule.toClass > 0 { if rule.fromClass > 0 && rule.toClass > 0 {
if r.Question[0].Qclass == rule.fromClass { if r.Question[0].Qclass == rule.fromClass {
r.Question[0].Qclass = rule.toClass r.Question[0].Qclass = rule.toClass

141
middleware/rewrite/edns0.go Normal file
View file

@ -0,0 +1,141 @@
// Package rewrite is middleware for rewriting requests internally to something different.
package rewrite
import (
"encoding/hex"
"fmt"
"strconv"
"strings"
"github.com/miekg/dns"
)
// edns0LocalRule is a rewrite rule for EDNS0_LOCAL options
type edns0LocalRule struct {
action string
code uint16
data []byte
}
// ends0NsidRule is a rewrite rule for EDNS0_NSID options
type edns0NsidRule struct {
action string
}
// setupEdns0Opt will retrieve the EDNS0 OPT or create it if it does not exist
func setupEdns0Opt(r *dns.Msg) *dns.OPT {
o := r.IsEdns0()
if o == nil {
r.SetEdns0(4096, true)
o = r.IsEdns0()
}
return o
}
// Rewrite will alter the request EDNS0 NSID option
func (rule *edns0NsidRule) Rewrite(r *dns.Msg) Result {
result := RewriteIgnored
o := setupEdns0Opt(r)
found := false
for _, s := range o.Option {
switch e := s.(type) {
case *dns.EDNS0_NSID:
if rule.action == "replace" || rule.action == "set" {
e.Nsid = "" // make sure it is empty for request
result = RewriteDone
}
found = true
break
}
}
// add option if not found
if !found && (rule.action == "append" || rule.action == "set") {
o.SetDo(true)
o.Option = append(o.Option, &dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""})
result = RewriteDone
}
return result
}
// Rewrite will alter the request EDNS0 local options
func (rule *edns0LocalRule) Rewrite(r *dns.Msg) Result {
result := RewriteIgnored
o := setupEdns0Opt(r)
found := false
for _, s := range o.Option {
switch e := s.(type) {
case *dns.EDNS0_LOCAL:
if rule.code == e.Code {
if rule.action == "replace" || rule.action == "set" {
e.Data = rule.data
result = RewriteDone
}
found = true
break
}
}
}
// add option if not found
if !found && (rule.action == "append" || rule.action == "set") {
o.SetDo(true)
var opt dns.EDNS0_LOCAL
opt.Code = rule.code
opt.Data = rule.data
o.Option = append(o.Option, &opt)
result = RewriteDone
}
return result
}
// newEdns0Rule creates an EDNS0 rule of the appropriate type based on the args
func newEdns0Rule(args ...string) (Rule, error) {
if len(args) < 2 {
return nil, fmt.Errorf("Too few arguments for an EDNS0 rule")
}
ruleType := strings.ToLower(args[0])
action := strings.ToLower(args[1])
switch action {
case "append":
case "replace":
case "set":
default:
return nil, fmt.Errorf("invalid action: %q", action)
}
switch ruleType {
case "local":
if len(args) != 4 {
return nil, fmt.Errorf("EDNS0 local rules require exactly three args")
}
return newEdns0LocalRule(action, args[2], args[3])
case "nsid":
if len(args) != 2 {
return nil, fmt.Errorf("EDNS0 NSID rules do not accept args")
}
return &edns0NsidRule{action: action}, nil
default:
return nil, fmt.Errorf("invalid rule type %q", ruleType)
}
}
func newEdns0LocalRule(action, code, data string) (*edns0LocalRule, error) {
c, err := strconv.ParseUint(code, 0, 16)
if err != nil {
return nil, err
}
decoded := []byte(data)
if strings.HasPrefix(data, "0x") {
decoded, err = hex.DecodeString(data[2:])
if err != nil {
return nil, err
}
}
return &edns0LocalRule{action: action, code: uint16(c), data: decoded}, nil
}

View file

@ -1,11 +0,0 @@
// Package rewrite is middleware for rewriting requests internally to something different.
package rewrite
// Fields defines additional FIELD keywords may be implemented to support more rewrite use-cases.
// New Rule types must be added to the Fields map.
// The type must implement `New` and `Rewrite` functions.
var Fields = map[string]Rule{
"name": NameRule{},
"type": TypeRule{},
"class": ClassRule{},
}

View file

@ -1,26 +1,21 @@
// Package rewrite is middleware for rewriting requests internally to something different.
package rewrite package rewrite
import ( import (
"strings"
"github.com/coredns/coredns/middleware" "github.com/coredns/coredns/middleware"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// NameRule is a name rewrite rule. type nameRule struct {
type NameRule struct {
From, To string From, To string
} }
// New initializes a new rule. func newNameRule(from, to string) (Rule, error) {
func (rule NameRule) New(args ...string) Rule { return &nameRule{middleware.Name(from).Normalize(), middleware.Name(to).Normalize()}, nil
from, to := args[0], strings.Join(args[1:], " ")
return &NameRule{middleware.Name(from).Normalize(), middleware.Name(to).Normalize()}
} }
// Rewrite rewrites the the current request. // Rewrite rewrites the the current request.
func (rule NameRule) Rewrite(r *dns.Msg) Result { func (rule *nameRule) Rewrite(r *dns.Msg) Result {
if rule.From == r.Question[0].Name { if rule.From == r.Question[0].Name {
r.Question[0].Name = rule.To r.Question[0].Name = rule.To
return RewriteDone return RewriteDone

View file

@ -1,9 +1,13 @@
// Package rewrite is middleware for rewriting requests internally to something different.
package rewrite package rewrite
import ( import (
"fmt"
"strings"
"github.com/coredns/coredns/middleware" "github.com/coredns/coredns/middleware"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
@ -52,10 +56,31 @@ func (rw Rewrite) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg
// Name implements the Handler interface. // Name implements the Handler interface.
func (rw Rewrite) Name() string { return "rewrite" } func (rw Rewrite) Name() string { return "rewrite" }
// Rule describes an internal location rewrite rule. // Rule describes a rewrite rule.
type Rule interface { type Rule interface {
// Rewrite rewrites the internal location of the current request. // Rewrite rewrites the current request.
Rewrite(*dns.Msg) Result Rewrite(*dns.Msg) Result
// New returns a new rule. }
New(...string) Rule
func newRule(args ...string) (Rule, error) {
if len(args) == 0 {
return nil, fmt.Errorf("No rule type specified for rewrite")
}
ruleType := strings.ToLower(args[0])
if ruleType != "edns0" && len(args) != 3 {
return nil, fmt.Errorf("%s rules must have exactly two arguments", ruleType)
}
switch ruleType {
case "name":
return newNameRule(args[1], args[2])
case "class":
return newClassRule(args[1], args[2])
case "type":
return newTypeRule(args[1], args[2])
case "edns0":
return newEdns0Rule(args[1:]...)
default:
return nil, fmt.Errorf("invalid rule type %q", args[0])
}
} }

View file

@ -1,6 +1,8 @@
package rewrite package rewrite
import ( import (
"bytes"
"reflect"
"testing" "testing"
"github.com/coredns/coredns/middleware" "github.com/coredns/coredns/middleware"
@ -16,14 +18,73 @@ func msgPrinter(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, err
return 0, nil return 0, nil
} }
func TestNewRule(t *testing.T) {
tests := []struct {
args []string
shouldError bool
expType reflect.Type
}{
{[]string{}, true, nil},
{[]string{"foo"}, true, nil},
{[]string{"name"}, true, nil},
{[]string{"name", "a.com"}, true, nil},
{[]string{"name", "a.com", "b.com", "c.com"}, true, nil},
{[]string{"name", "a.com", "b.com"}, false, reflect.TypeOf(&nameRule{})},
{[]string{"type"}, true, nil},
{[]string{"type", "a"}, true, nil},
{[]string{"type", "any", "a", "a"}, true, nil},
{[]string{"type", "any", "a"}, false, reflect.TypeOf(&typeRule{})},
{[]string{"type", "XY", "WV"}, true, nil},
{[]string{"type", "ANY", "WV"}, true, nil},
{[]string{"class"}, true, nil},
{[]string{"class", "IN"}, true, nil},
{[]string{"class", "ch", "in", "in"}, true, nil},
{[]string{"class", "ch", "in"}, false, reflect.TypeOf(&classRule{})},
{[]string{"class", "XY", "WV"}, true, nil},
{[]string{"class", "IN", "WV"}, true, nil},
{[]string{"edns0"}, true, nil},
{[]string{"edns0", "local"}, true, nil},
{[]string{"edns0", "local", "set"}, true, nil},
{[]string{"edns0", "local", "set", "0xffee"}, true, nil},
{[]string{"edns0", "local", "set", "65518", "abcdefg"}, false, reflect.TypeOf(&edns0LocalRule{})},
{[]string{"edns0", "local", "set", "0xffee", "abcdefg"}, false, reflect.TypeOf(&edns0LocalRule{})},
{[]string{"edns0", "local", "append", "0xffee", "abcdefg"}, false, reflect.TypeOf(&edns0LocalRule{})},
{[]string{"edns0", "local", "replace", "0xffee", "abcdefg"}, false, reflect.TypeOf(&edns0LocalRule{})},
{[]string{"edns0", "local", "foo", "0xffee", "abcdefg"}, true, nil},
{[]string{"edns0", "local", "set", "0xffee", "0xabcdefg"}, true, nil},
{[]string{"edns0", "nsid", "set", "junk"}, true, nil},
{[]string{"edns0", "nsid", "set"}, false, reflect.TypeOf(&edns0NsidRule{})},
{[]string{"edns0", "nsid", "append"}, false, reflect.TypeOf(&edns0NsidRule{})},
{[]string{"edns0", "nsid", "replace"}, false, reflect.TypeOf(&edns0NsidRule{})},
{[]string{"edns0", "nsid", "foo"}, true, nil},
}
for i, tc := range tests {
r, err := newRule(tc.args...)
if err == nil && tc.shouldError {
t.Errorf("Test %d: expected error but got success", i)
} else if err != nil && !tc.shouldError {
t.Errorf("Test %d: expected success but got error: %s", i, err)
}
if !tc.shouldError && reflect.TypeOf(r) != tc.expType {
t.Errorf("Test %d: expected %q but got %q", i, tc.expType, r)
}
}
}
func TestRewrite(t *testing.T) { func TestRewrite(t *testing.T) {
rules := []Rule{}
r, _ := newNameRule("from.nl.", "to.nl.")
rules = append(rules, r)
r, _ = newClassRule("CH", "IN")
rules = append(rules, r)
r, _ = newTypeRule("ANY", "HINFO")
rules = append(rules, r)
rw := Rewrite{ rw := Rewrite{
Next: middleware.HandlerFunc(msgPrinter), Next: middleware.HandlerFunc(msgPrinter),
Rules: []Rule{ Rules: rules,
Fields["name"].New("from.nl.", "to.nl."),
Fields["class"].New("CH", "IN"),
Fields["type"].New("ANY", "HINFO"),
},
noRevert: true, noRevert: true,
} }
@ -56,7 +117,7 @@ func TestRewrite(t *testing.T) {
resp := rec.Msg resp := rec.Msg
if resp.Question[0].Name != tc.to { if resp.Question[0].Name != tc.to {
t.Errorf("Test %d: Expected Name to be '%s' but was '%s'", i, tc.to, resp.Question[0].Name) t.Errorf("Test %d: Expected Name to be %q but was %q", i, tc.to, resp.Question[0].Name)
} }
if resp.Question[0].Qtype != tc.toT { if resp.Question[0].Qtype != tc.toT {
t.Errorf("Test %d: Expected Type to be '%d' but was '%d'", i, tc.toT, resp.Question[0].Qtype) t.Errorf("Test %d: Expected Type to be '%d' but was '%d'", i, tc.toT, resp.Question[0].Qtype)
@ -65,79 +126,162 @@ func TestRewrite(t *testing.T) {
t.Errorf("Test %d: Expected Class to be '%d' but was '%d'", i, tc.toC, resp.Question[0].Qclass) t.Errorf("Test %d: Expected Class to be '%d' but was '%d'", i, tc.toC, resp.Question[0].Qclass)
} }
} }
}
/* func TestRewriteEDNS0Local(t *testing.T) {
regexps := [][]string{ rw := Rewrite{
{"/reg/", ".*", "/to", ""}, Next: middleware.HandlerFunc(msgPrinter),
{"/r/", "[a-z]+", "/toaz", "!.html|"}, noRevert: true,
{"/url/", "a([a-z0-9]*)s([A-Z]{2})", "/to/{path}", ""},
{"/ab/", "ab", "/ab?{query}", ".txt|"},
{"/ab/", "ab", "/ab?type=html&{query}", ".html|"},
{"/abc/", "ab", "/abc/{file}", ".html|"},
{"/abcd/", "ab", "/a/{dir}/{file}", ".html|"},
{"/abcde/", "ab", "/a#{fragment}", ".html|"},
{"/ab/", `.*\.jpg`, "/ajpg", ""},
{"/reggrp", `/ad/([0-9]+)([a-z]*)`, "/a{1}/{2}", ""},
{"/reg2grp", `(.*)`, "/{1}", ""},
{"/reg3grp", `(.*)/(.*)/(.*)`, "/{1}{2}{3}", ""},
} }
for _, regexpRule := range regexps { tests := []struct {
var ext []string fromOpts []dns.EDNS0
if s := strings.Split(regexpRule[3], "|"); len(s) > 1 { args []string
ext = s[:len(s)-1] toOpts []dns.EDNS0
}
rule, err := NewComplexRule(regexpRule[0], regexpRule[1], regexpRule[2], 0, ext, nil)
if err != nil {
t.Fatal(err)
}
rw.Rules = append(rw.Rules, rule)
}
*/
/*
statusTests := []struct {
status int
base string
to string
regexp string
statusExpected bool
}{ }{
{400, "/status", "", "", true}, {
{400, "/ignore", "", "", false}, []dns.EDNS0{},
{400, "/", "", "^/ignore", false}, []string{"local", "set", "0xffee", "0xabcdef"},
{400, "/", "", "(.*)", true}, []dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte{0xab, 0xcd, 0xef}}},
{400, "/status", "", "", true}, },
{
[]dns.EDNS0{},
[]string{"local", "append", "0xffee", "abcdefghijklmnop"},
[]dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("abcdefghijklmnop")}},
},
{
[]dns.EDNS0{},
[]string{"local", "replace", "0xffee", "abcdefghijklmnop"},
[]dns.EDNS0{},
},
{
[]dns.EDNS0{},
[]string{"nsid", "set"},
[]dns.EDNS0{&dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}},
},
{
[]dns.EDNS0{},
[]string{"nsid", "append"},
[]dns.EDNS0{&dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}},
},
{
[]dns.EDNS0{},
[]string{"nsid", "replace"},
[]dns.EDNS0{},
},
} }
for i, s := range statusTests { ctx := context.TODO()
urlPath := fmt.Sprintf("/status%d", i) for i, tc := range tests {
rule, err := NewComplexRule(s.base, s.regexp, s.to, s.status, nil, nil) m := new(dns.Msg)
m.SetQuestion("example.com.", dns.TypeA)
m.Question[0].Qclass = dns.ClassINET
r, err := newEdns0Rule(tc.args...)
if err != nil { if err != nil {
t.Fatalf("Test %d: No error expected for rule but found %v", i, err) t.Errorf("Error creating test rule: %s", err)
continue
} }
rw.Rules = []Rule{rule} rw.Rules = []Rule{r}
req, err := http.NewRequest("GET", urlPath, nil)
if err != nil { rec := dnsrecorder.New(&test.ResponseWriter{})
t.Fatalf("Test %d: Could not create HTTP request: %v", i, err) rw.ServeDNS(ctx, rec, m)
resp := rec.Msg
o := resp.IsEdns0()
if o == nil {
t.Errorf("Test %d: EDNS0 options not set", i)
continue
}
if !optsEqual(o.Option, tc.toOpts) {
t.Errorf("Test %d: Expected %v but got %v", i, tc.toOpts, o)
}
}
}
func TestEdns0LocalMultiRule(t *testing.T) {
rules := []Rule{}
r, _ := newEdns0Rule("local", "replace", "0xffee", "abcdef")
rules = append(rules, r)
r, _ = newEdns0Rule("local", "set", "0xffee", "fedcba")
rules = append(rules, r)
rw := Rewrite{
Next: middleware.HandlerFunc(msgPrinter),
Rules: rules,
noRevert: true,
} }
rec := httptest.NewRecorder() tests := []struct {
code, err := rw.ServeHTTP(rec, req) fromOpts []dns.EDNS0
if err != nil { toOpts []dns.EDNS0
t.Fatalf("Test %d: No error expected for handler but found %v", i, err) }{
{
nil,
[]dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("fedcba")}},
},
{
[]dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("foobar")}},
[]dns.EDNS0{&dns.EDNS0_LOCAL{Code: 0xffee, Data: []byte("abcdef")}},
},
} }
if s.statusExpected {
if rec.Body.String() != "" { ctx := context.TODO()
t.Errorf("Test %d: Expected empty body but found %s", i, rec.Body.String()) for i, tc := range tests {
m := new(dns.Msg)
m.SetQuestion("example.com.", dns.TypeA)
m.Question[0].Qclass = dns.ClassINET
if tc.fromOpts != nil {
o := m.IsEdns0()
if o == nil {
m.SetEdns0(4096, true)
o = m.IsEdns0()
} }
if code != s.status { o.Option = append(o.Option, tc.fromOpts...)
t.Errorf("Test %d: Expected status code %d found %d", i, s.status, code) }
rec := dnsrecorder.New(&test.ResponseWriter{})
rw.ServeDNS(ctx, rec, m)
resp := rec.Msg
o := resp.IsEdns0()
if o == nil {
t.Errorf("Test %d: EDNS0 options not set", i)
continue
}
if !optsEqual(o.Option, tc.toOpts) {
t.Errorf("Test %d: Expected %v but got %v", i, tc.toOpts, o)
}
}
}
func optsEqual(a, b []dns.EDNS0) bool {
if len(a) != len(b) {
return false
}
for i := range a {
switch aa := a[i].(type) {
case *dns.EDNS0_LOCAL:
if bb, ok := b[i].(*dns.EDNS0_LOCAL); ok {
if aa.Code != bb.Code {
return false
}
if !bytes.Equal(aa.Data, bb.Data) {
return false
} }
} else { } else {
if code != 0 { return false
t.Errorf("Test %d: Expected no status code found %d", i, code) }
case *dns.EDNS0_NSID:
if bb, ok := b[i].(*dns.EDNS0_NSID); ok {
if aa.Nsid != bb.Nsid {
return false
}
} else {
return false
}
default:
return false
} }
} }
} return true
*/
} }

View file

@ -1,8 +1,6 @@
package rewrite package rewrite
import ( import (
"log"
"github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/middleware" "github.com/coredns/coredns/middleware"
@ -30,93 +28,15 @@ func setup(c *caddy.Controller) error {
} }
func rewriteParse(c *caddy.Controller) ([]Rule, error) { func rewriteParse(c *caddy.Controller) ([]Rule, error) {
var simpleRules []Rule var rules []Rule
var regexpRules []Rule
for c.Next() { for c.Next() {
var rule Rule
/*
var base = "."
var err error
var pattern, to string
var status int
var ifs []If
var ext []string
*/
args := c.RemainingArgs() args := c.RemainingArgs()
rule, err := newRule(args...)
switch len(args) {
case 1:
/*
base = args[0]
fallthrough
*/
case 0:
/*
for c.NextBlock() {
switch c.Val() {
case "r", "regexp":
if !c.NextArg() {
return nil, c.ArgErr()
}
pattern = c.Val()
case "to":
args1 := c.RemainingArgs()
if len(args1) == 0 {
return nil, c.ArgErr()
}
to = strings.Join(args1, " ")
case "ext": // TODO(miek): fix or remove
args1 := c.RemainingArgs()
if len(args1) == 0 {
return nil, c.ArgErr()
}
ext = args1
case "if":
args1 := c.RemainingArgs()
if len(args1) != 3 {
return nil, c.ArgErr()
}
ifCond, err := NewIf(args1[0], args1[1], args1[2])
if err != nil { if err != nil {
return nil, err return nil, err
} }
ifs = append(ifs, ifCond) rules = append(rules, rule)
case "status": // TODO(miek): fix or remove
if !c.NextArg() {
return nil, c.ArgErr()
} }
status, _ = strconv.Atoi(c.Val()) return rules, nil
if status < 200 || (status > 299 && status < 400) || status > 499 {
return nil, c.Err("status must be 2xx or 4xx")
}
default:
return nil, c.ArgErr()
}
}
// ensure to or status is specified
if to == "" && status == 0 {
return nil, c.ArgErr()
}
// TODO(miek): complex rules
if rule, err = NewComplexRule(base, pattern, to, status, ext, ifs); err != nil {
return nil, err
}
regexpRules = append(regexpRules, rule)
*/
// the only unhandled case is 2 and above
default:
if _, ok := Fields[args[0]]; ok {
rule = Fields[args[0]].New(args[1:]...)
simpleRules = append(simpleRules, rule)
} else {
log.Printf("[WARN] %s is not a valid field, ignore %s", args[0], args)
}
}
}
// put simple rules in front to avoid regexp computation for them
return append(simpleRules, regexpRules...), nil
} }

View file

@ -0,0 +1,25 @@
package rewrite
import (
"testing"
"github.com/mholt/caddy"
)
func TestParse(t *testing.T) {
c := caddy.NewTestController("dns", `rewrite`)
_, err := rewriteParse(c)
if err == nil {
t.Errorf("Expected error but found nil for `rewrite`")
}
c = caddy.NewTestController("dns", `rewrite name`)
_, err = rewriteParse(c)
if err == nil {
t.Errorf("Expected error but found nil for `rewrite name`")
}
c = caddy.NewTestController("dns", `rewrite name a.com b.com`)
_, err = rewriteParse(c)
if err != nil {
t.Errorf("Expected success but found %s for `rewrite name a.com b.com`", err)
}
}

View file

@ -2,24 +2,31 @@
package rewrite package rewrite
import ( import (
"fmt"
"strings" "strings"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// TypeRule is a type rewrite rule. // typeRule is a type rewrite rule.
type TypeRule struct { type typeRule struct {
fromType, toType uint16 fromType, toType uint16
} }
// New initializes a rule. func newTypeRule(fromS, toS string) (Rule, error) {
func (rule TypeRule) New(args ...string) Rule { var from, to uint16
from, to := args[0], strings.Join(args[1:], " ") var ok bool
return &TypeRule{dns.StringToType[from], dns.StringToType[to]} if from, ok = dns.StringToType[strings.ToUpper(fromS)]; !ok {
return nil, fmt.Errorf("invalid type %q", strings.ToUpper(fromS))
}
if to, ok = dns.StringToType[strings.ToUpper(toS)]; !ok {
return nil, fmt.Errorf("invalid type %q", strings.ToUpper(toS))
}
return &typeRule{fromType: from, toType: to}, nil
} }
// Rewrite rewrites the the current request. // Rewrite rewrites the the current request.
func (rule TypeRule) Rewrite(r *dns.Msg) Result { func (rule *typeRule) Rewrite(r *dns.Msg) Result {
if rule.fromType > 0 && rule.toType > 0 { if rule.fromType > 0 && rule.toType > 0 {
if r.Question[0].Qtype == rule.fromType { if r.Question[0].Qtype == rule.fromType {
r.Question[0].Qtype = rule.toType r.Question[0].Qtype = rule.toType

View file

@ -23,7 +23,7 @@ func TestLookupBalanceRewriteCacheDnssec(t *testing.T) {
corefile := `example.org:0 { corefile := `example.org:0 {
file ` + name + ` file ` + name + `
rewrite ANY HINFO rewrite type ANY HINFO
dnssec { dnssec {
key file ` + base + ` key file ` + base + `
} }

View file

@ -20,7 +20,7 @@ func benchmarkLookupBalanceRewriteCache(b *testing.B) {
corefile := `example.org:0 { corefile := `example.org:0 {
file ` + name + ` file ` + name + `
rewrite ANY HINFO rewrite type ANY HINFO
loadbalance loadbalance
} }
` `

94
test/rewrite_test.go Normal file
View file

@ -0,0 +1,94 @@
package test
import (
"bytes"
"io/ioutil"
"log"
"testing"
"github.com/miekg/dns"
)
func TestRewrite(t *testing.T) {
t.Parallel()
corefile := `.:0 {
rewrite type MX a
rewrite edns0 local set 0xffee hello-world
erratic . {
drop 0
}
}`
i, err := CoreDNSServer(corefile)
if err != nil {
t.Fatalf("Could not get CoreDNS serving instance: %s", err)
}
udp, _ := CoreDNSServerPorts(i, 0)
if udp == "" {
t.Fatalf("Could not get UDP listening port")
}
defer i.Stop()
log.SetOutput(ioutil.Discard)
testMX(t, udp)
testEdns0(t, udp)
}
func testMX(t *testing.T, server string) {
m := new(dns.Msg)
m.SetQuestion("example.com.", dns.TypeMX)
r, err := dns.Exchange(m, server)
if err != nil {
t.Fatalf("Expected to receive reply, but didn't: %s", err)
}
// expect answer section with A record in it
if len(r.Answer) == 0 {
t.Error("Expected to at least one RR in the answer section, got none")
}
if r.Answer[0].Header().Rrtype != dns.TypeA {
t.Errorf("Expected RR to A, got: %d", r.Answer[0].Header().Rrtype)
}
if r.Answer[0].(*dns.A).A.String() != "192.0.2.53" {
t.Errorf("Expected 192.0.2.53, got: %s", r.Answer[0].(*dns.A).A.String())
}
}
func testEdns0(t *testing.T, server string) {
m := new(dns.Msg)
m.SetQuestion("example.com.", dns.TypeA)
r, err := dns.Exchange(m, server)
if err != nil {
t.Fatalf("Expected to receive reply, but didn't: %s", err)
}
// expect answer section with A record in it
if len(r.Answer) == 0 {
t.Error("Expected to at least one RR in the answer section, got none")
}
if r.Answer[0].Header().Rrtype != dns.TypeA {
t.Errorf("Expected RR to A, got: %d", r.Answer[0].Header().Rrtype)
}
if r.Answer[0].(*dns.A).A.String() != "192.0.2.53" {
t.Errorf("Expected 192.0.2.53, got: %s", r.Answer[0].(*dns.A).A.String())
}
o := r.IsEdns0()
if o == nil || len(o.Option) == 0 {
t.Error("Expected EDNS0 options but got none")
} else {
if e, ok := o.Option[0].(*dns.EDNS0_LOCAL); ok {
if e.Code != 0xffee {
t.Errorf("Expected EDNS_LOCAL code 0xffee but got %x", e.Code)
}
if !bytes.Equal(e.Data, []byte("hello-world")) {
t.Errorf("Expected EDNS_LOCAL data 'hello-world' but got %q", e.Data)
}
} else {
t.Errorf("Expected EDNS0_LOCAL but got %v", o.Option[0])
}
}
}