diff --git a/README.md b/README.md index 3c58a0a..d42ac26 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ This plugin can only be used once per Server Block. ~~~ records [ZONES...] { + $OPTION [OPTION] [INLINE] } ~~~ @@ -42,6 +43,9 @@ records [ZONES...] { * **INLINE** the resource record that are to be served. These must be specified as the text represenation (as specifed in RFC 1035) of the record. See the examples below. Each record must be on a single line. +* **OPTION** is a configuration option for the plugin. The following options are supported: + * `fallthrough`: When no matching record is found, instead of returning NXDOMAIN, the plugin will + call to the next plugin in the chain. If domain name in **INLINE** are not fully qualifed each of the **ZONES** are used as the origin and added to the names. @@ -73,6 +77,22 @@ RFC 1035 zone file and everything after it will be ignored, hence the need for q } ~~~ +Override the record for `example.com`, without overriding anything else. Subdomains, like +`foo.example.com`, will continue to be resolved normally (the `forward` plugin, in this case). + +~~~ +. { + records . { + $OPTION fallthrough + example.com. 300 IN A 127.0.0.1 + } + + forward . 192.168.0.1 { + except example.com + } +} +~~~ + ## Bugs DNSSEC, nor wildcards are implemented. The lookup algorithm is pretty basic. Future enhancements diff --git a/records.go b/records.go index 4a414f7..ca87410 100644 --- a/records.go +++ b/records.go @@ -5,6 +5,7 @@ import ( "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/request" + "github.com/coredns/coredns/plugin/pkg/fall" "github.com/miekg/dns" ) @@ -15,6 +16,7 @@ type Records struct { m map[string][]dns.RR Next plugin.Handler + Fall *fall.F } // ServeDNS implements the plugin.Handle interface. @@ -48,6 +50,9 @@ func (re *Records) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms // handle NXDOMAIN, NODATA and normal response here. if nxdomain { + if re.Fall.Through(qname) { + return plugin.NextOrFailure(re.Name(), re.Next, ctx, w, r) + } m.Rcode = dns.RcodeNameError if soa != nil { m.Ns = []dns.RR{soa} @@ -73,5 +78,6 @@ func (re *Records) Name() string { return "records" } func New() *Records { re := new(Records) re.m = make(map[string][]dns.RR) + re.Fall = &fall.F{} return re } diff --git a/records_test.go b/records_test.go index ef9d11d..c1b9be1 100644 --- a/records_test.go +++ b/records_test.go @@ -2,6 +2,7 @@ package records import ( "context" + "errors" "testing" "github.com/coredns/coredns/plugin/pkg/dnstest" @@ -175,3 +176,72 @@ var testCasesMultipleOrigins = []test.Case{ }, }, } + +func TestLookupFallThrough(t *testing.T) { + const input = ` +records example.org { + $OPTION fallthrough + @ 60 IN A 127.0.0.1 +} +` + + c := caddy.NewTestController("dns", input) + re, err := recordsParse(c) + if err != nil { + t.Fatal(err) + } + +tests: + for i, tc := range testCasesFallThrough { + m := tc.Msg() + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := re.ServeDNS(context.Background(), rec, m) + if tc.Error != nil && err == nil { + t.Errorf("Test %d, expected error %q, no error returned", i, tc.Error.Error()) + return + } + + if tc.Error == nil && err != nil { + t.Errorf("Test %d, expected no error, got %v", i, err) + return + } + + if tc.Error != nil && err != nil { + if tc.Error.Error() != err.Error() { + t.Errorf("Test %d, expected error message %q, got %q", i, tc.Error.Error(), err.Error()) + return + } + continue tests + } + + if rec.Msg == nil { + t.Errorf("Test %d, no message received", i) + return + } + + if rec.Msg.Rcode != tc.Rcode { + t.Errorf("Test %d, expected rcode is %d, but got %d", i, tc.Rcode, rec.Msg.Rcode) + return + } + + if resp := rec.Msg; rec.Msg != nil { + if err := test.SortAndCheck(resp, tc); err != nil { + t.Errorf("Test %d: %v", i, err) + } + } + } +} + +var testCasesFallThrough = []test.Case{ + { + Qname: "example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("example.org. 60 IN A 127.0.0.1"), + }, + }, + { + Qname: "foo.example.net.", Qtype: dns.TypeA, + Error: errors.New("plugin/records: no next plugin found"), + }, +} diff --git a/setup.go b/setup.go index 38c38cd..2497b9f 100644 --- a/setup.go +++ b/setup.go @@ -1,6 +1,7 @@ package records import ( + "fmt" "strings" "github.com/coredns/caddy" @@ -57,9 +58,26 @@ func recordsParse(c *caddy.Controller) (*Records, error) { // c.Val() + c.RemainingArgs() is the record we need to parse (for each zone given; now tracked in re.origins). When parsing // the record we just set the ORIGIN to the correct value and magic will happen. If no origin we set it to "." + parseBlocks: for c.NextBlock() { - s := c.Val() + " " - s += strings.Join(c.RemainingArgs(), " ") + s := c.Val() + if s == "$OPTION" { + if !c.NextArg() { + return nil, fmt.Errorf("parsing block failed: $OPTION missing argument") + } + opt := c.Val() + switch opt { + case "fallthrough": + re.Fall.SetZonesFromArgs(re.origins) + default: + return nil, fmt.Errorf("parsing block failed: unknown option: %q", opt) + } + if len(c.RemainingArgs()) > 0 { + return nil, fmt.Errorf("parsing block failed: extra arguments after option %q", opt) + } + continue parseBlocks + } + s += " " + strings.Join(c.RemainingArgs(), " ") for _, o := range re.origins { rr, err := dns.NewRR("$ORIGIN " + o + "\n" + s + "\n") if err != nil {