feat(plugin/file): fallthrough (#7327)

* feat(plugin/file): fallthrough

implement and test fallthrough for the file plugin

Signed-off-by: vdbe <vdbewout@gmail.com>

* docs(plugin/file): fallthrough

Signed-off-by: vdbe <vdbewout@gmail.com>

* docs(plugin/file): regenerate man page

`make -f Makefile.doc man/coredns-file.7`

Signed-off-by: vdbe <vdbewout@gmail.com>

---------

Signed-off-by: vdbe <vdbewout@gmail.com>
This commit is contained in:
vdbe
2025-05-29 02:34:16 +02:00
committed by GitHub
parent bebb7bce43
commit b3acbe5046
6 changed files with 149 additions and 16 deletions

View File

@@ -1,5 +1,5 @@
.\" Generated by Mmark Markdown Processer - mmark.miek.nl
.TH "COREDNS-FILE" 7 "March 2021" "CoreDNS" "CoreDNS Plugins"
.TH "COREDNS-FILE" 7 "May 2025" "CoreDNS" "CoreDNS Plugins"
.SH "NAME"
.PP
@@ -39,6 +39,7 @@ If you want to round-robin A and AAAA responses look at the \fIloadbalance\fP pl
.nf
file DBFILE [ZONES... ] {
reload DURATION
fallthrough [ZONES...]
}
.fi
@@ -48,6 +49,11 @@ file DBFILE [ZONES... ] {
\fB\fCreload\fR interval to perform a reload of the zone if the SOA version changes. Default is one minute.
Value of \fB\fC0\fR means to not scan for changes and reload. For example, \fB\fC30s\fR checks the zonefile every 30 seconds
and reloads the zone when serial changes.
.IP \(bu 4
\fB\fCfallthrough\fR If zone matches and no record can be generated, pass request to the next plugin.
If \fB[ZONES...]\fP is omitted, then fallthrough happens for all zones for which the plugin
is authoritative. If specific zones are listed (for example \fB\fCin-addr.arpa\fR and \fB\fCip6.arpa\fR), then only
queries for those zones will be subject to fallthrough.
.PP

View File

@@ -27,12 +27,17 @@ If you want to round-robin A and AAAA responses look at the *loadbalance* plugin
~~~
file DBFILE [ZONES... ] {
reload DURATION
fallthrough [ZONES...]
}
~~~
* `reload` interval to perform a reload of the zone if the SOA version changes. Default is one minute.
Value of `0` means to not scan for changes and reload. For example, `30s` checks the zonefile every 30 seconds
and reloads the zone when serial changes.
* `fallthrough` If zone matches and no record can be generated, pass request to the next plugin.
If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin
is authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only
queries for those zones will be subject to fallthrough.
If you need outgoing zone transfers, take a look at the *transfer* plugin.

View File

@@ -7,6 +7,7 @@ import (
"io"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/fall"
clog "github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/plugin/transfer"
"github.com/coredns/coredns/request"
@@ -22,6 +23,8 @@ type (
Next plugin.Handler
Zones
transfer *transfer.Transfer
Fall fall.F
}
// Zones maps zone names to a *Zone.
@@ -86,6 +89,13 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i
answer, ns, extra, result := z.Lookup(ctx, state, qname)
// Only on NXDOMAIN we will fallthrough.
// `z.Lookup` can also return NOERROR for NXDOMAIN see comment see comment "Hacky way to get around empty-non-terminals" inside `Zone.Lookup`.
// It's safe to fallthrough with `result` Sucess (NOERROR) since all other return points in Lookup with Success have answer(s).
if len(answer) == 0 && (result == NameError || result == Success) && f.Fall.Through(qname) {
return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
}
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true

View File

@@ -6,6 +6,7 @@ import (
"testing"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/fall"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
@@ -216,6 +217,85 @@ func TestLookUpNoDataResult(t *testing.T) {
}
}
func TestLookupFallthrough(t *testing.T) {
zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0)
if err != nil {
t.Fatalf("Expected no error when reading zone, got %q", err)
}
type FallWithTestCases struct {
Fall fall.F
Cases []test.Case
}
var fallsWithTestCases = []FallWithTestCases{
{
Fall: fall.Root,
Cases: []test.Case{
{
Qname: "doesnotexist.miek.nl.", Qtype: dns.TypeA,
Rcode: dns.RcodeServerFailure,
},
{
Qname: "x.miek.nl.", Qtype: dns.TypeA,
Rcode: dns.RcodeServerFailure,
},
},
},
{
Fall: fall.F{Zones: []string{"a.miek.nl."}},
Cases: []test.Case{
{
Qname: "a.miek.nl.", Qtype: dns.TypeA,
Rcode: dns.RcodeSuccess,
},
{
Qname: "doesnotexist.miek.nl.", Qtype: dns.TypeA,
Rcode: dns.RcodeNameError,
},
{
Qname: "passthrough.a.miek.nl.", Qtype: dns.TypeA,
Rcode: dns.RcodeServerFailure,
Answer: []dns.RR{},
},
},
},
{
Fall: fall.F{Zones: []string{"x.miek.nl."}},
Cases: []test.Case{
{
Qname: "x.miek.nl.", Qtype: dns.TypeA,
Rcode: dns.RcodeServerFailure,
},
{
Qname: "wildcard.x.miek.nl.", Qtype: dns.TypeA,
Rcode: dns.RcodeSuccess,
},
},
},
}
for _, fallWithTestCases := range fallsWithTestCases {
fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}, Fall: fallWithTestCases.Fall}
ctx := context.TODO()
for _, tc := range fallWithTestCases.Cases {
m := tc.Msg()
rec := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := fm.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("Expected no error, got %v", err)
return
}
if rec.Msg.Rcode != tc.Rcode {
t.Errorf("rcode is %q, expected %q", dns.RcodeToString[rec.Msg.Rcode], dns.RcodeToString[tc.Rcode])
return
}
}
}
}
func BenchmarkFileLookup(b *testing.B) {
zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0)
if err != nil {

View File

@@ -9,6 +9,7 @@ import (
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/fall"
"github.com/coredns/coredns/plugin/pkg/upstream"
"github.com/coredns/coredns/plugin/transfer"
)
@@ -16,12 +17,12 @@ import (
func init() { plugin.Register("file", setup) }
func setup(c *caddy.Controller) error {
zones, err := fileParse(c)
zones, fall, err := fileParse(c)
if err != nil {
return plugin.Error("file", err)
}
f := File{Zones: zones}
f := File{Zones: zones, Fall: fall}
// get the transfer plugin, so we can send notifies and send notifies on startup as well.
c.OnStartup(func() error {
t := dnsserver.GetConfig(c).Handler("transfer")
@@ -67,9 +68,10 @@ func setup(c *caddy.Controller) error {
return nil
}
func fileParse(c *caddy.Controller) (Zones, error) {
func fileParse(c *caddy.Controller) (Zones, fall.F, error) {
z := make(map[string]*Zone)
names := []string{}
fall := fall.F{}
config := dnsserver.GetConfig(c)
@@ -79,7 +81,7 @@ func fileParse(c *caddy.Controller) (Zones, error) {
for c.Next() {
// file db.file [zones...]
if !c.NextArg() {
return Zones{}, c.ArgErr()
return Zones{}, fall, c.ArgErr()
}
fileName := c.Val()
@@ -112,19 +114,21 @@ func fileParse(c *caddy.Controller) (Zones, error) {
}()
if err != nil {
return Zones{}, err
return Zones{}, fall, err
}
for c.NextBlock() {
switch c.Val() {
case "fallthrough":
fall.SetZonesFromArgs(c.RemainingArgs())
case "reload":
t := c.RemainingArgs()
if len(t) < 1 {
return Zones{}, errors.New("reload duration value is expected")
return Zones{}, fall, errors.New("reload duration value is expected")
}
d, err := time.ParseDuration(t[0])
if err != nil {
return Zones{}, plugin.Error("file", err)
return Zones{}, fall, plugin.Error("file", err)
}
reload = d
case "upstream":
@@ -132,7 +136,7 @@ func fileParse(c *caddy.Controller) (Zones, error) {
c.RemainingArgs()
default:
return Zones{}, c.Errf("unknown property '%s'", c.Val())
return Zones{}, fall, c.Errf("unknown property '%s'", c.Val())
}
}
@@ -145,9 +149,9 @@ func fileParse(c *caddy.Controller) (Zones, error) {
if openErr != nil {
if reload == 0 {
// reload hasn't been set make this a fatal error
return Zones{}, plugin.Error("file", openErr)
return Zones{}, fall, plugin.Error("file", openErr)
}
log.Warningf("Failed to open %q: trying again in %s", openErr, reload)
}
return Zones{Z: z, Names: names}, nil
return Zones{Z: z, Names: names}, fall, nil
}

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/coredns/caddy"
"github.com/coredns/coredns/plugin/pkg/fall"
"github.com/coredns/coredns/plugin/test"
)
@@ -22,24 +23,44 @@ func TestFileParse(t *testing.T) {
defer rm()
tests := []struct {
inputFileRules string
shouldErr bool
expectedZones Zones
inputFileRules string
shouldErr bool
expectedZones Zones
expectedFallthrough fall.F
}{
{
`file ` + zoneFileName1 + ` miek.nl.`,
false,
Zones{Names: []string{"miek.nl."}},
fall.Zero,
},
{
`file ` + zoneFileName2 + ` dnssex.nl.`,
false,
Zones{Names: []string{"dnssex.nl."}},
fall.Zero,
},
{
`file ` + zoneFileName2 + ` 10.0.0.0/8`,
false,
Zones{Names: []string{"10.in-addr.arpa."}},
fall.Zero,
},
{
`file ` + zoneFileName2 + ` example.org. {
fallthrough
}`,
false,
Zones{Names: []string{"example.org."}},
fall.Root,
},
{
`file ` + zoneFileName2 + ` example.org. {
fallthrough www.example.org
}`,
false,
Zones{Names: []string{"example.org."}},
fall.F{Zones: []string{"www.example.org."}},
},
// errors.
{
@@ -48,11 +69,13 @@ func TestFileParse(t *testing.T) {
}`,
true,
Zones{},
fall.Zero,
},
{
`file`,
true,
Zones{},
fall.Zero,
},
{
`file ` + zoneFileName1 + ` example.net. {
@@ -60,6 +83,7 @@ func TestFileParse(t *testing.T) {
}`,
true,
Zones{},
fall.Zero,
},
{
`file ` + zoneFileName1 + ` example.net. {
@@ -67,12 +91,13 @@ func TestFileParse(t *testing.T) {
}`,
true,
Zones{},
fall.Zero,
},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.inputFileRules)
actualZones, err := fileParse(c)
actualZones, actualFallthrough, err := fileParse(c)
if err == nil && test.shouldErr {
t.Fatalf("Test %d expected errors, but got no error", i)
@@ -87,6 +112,9 @@ func TestFileParse(t *testing.T) {
t.Fatalf("Test %d expected %v for %d th zone, got %v", i, name, j, actualZones.Names[j])
}
}
if !actualFallthrough.Equal(test.expectedFallthrough) {
t.Errorf("Test %d expected fallthrough of %v, got %v", i, test.expectedFallthrough, actualFallthrough)
}
}
}
}
@@ -116,7 +144,7 @@ func TestParseReload(t *testing.T) {
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
z, _ := fileParse(c)
z, _, _ := fileParse(c)
if x := z.Z["example.org."].ReloadInterval; x != test.reload {
t.Errorf("Test %d expected reload to be %s, but got %s", i, test.reload, x)
}