From bd8474dea3775c887d622ff0d0166a9ef091aea6 Mon Sep 17 00:00:00 2001 From: Dan Fuhry Date: Thu, 9 Mar 2023 16:35:36 -0500 Subject: [PATCH] Add `$OPTION` directive and "fallthrough" option Add the `$OPTION` directive to allow configuring the plugin. Implement an option, "fallthrough" which causes the plugin to continue to the next plugin in the chain when there is no match, instead of always returning NXDOMAIN. Signed-Off-By: Dan Fuhry --- README.md | 20 ++++++++++++++ records.go | 6 +++++ records_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ setup.go | 22 ++++++++++++++-- 4 files changed, 116 insertions(+), 2 deletions(-) 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 {