aboutsummaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
authorGravatar An Xiao <hac@zju.edu.cn> 2019-09-04 23:43:45 +0800
committerGravatar Yong Tang <yong.tang.github@outlook.com> 2019-09-04 08:43:45 -0700
commit79f37a1460cc52ce6c63110f4df33316a36af3a5 (patch)
tree35b3ddcc68ba82eabb9393cb166b5de76a42bb29 /plugin
parent7894154bfd2f1960c6842318d8ee99c194a04179 (diff)
downloadcoredns-79f37a1460cc52ce6c63110f4df33316a36af3a5.tar.gz
coredns-79f37a1460cc52ce6c63110f4df33316a36af3a5.tar.zst
coredns-79f37a1460cc52ce6c63110f4df33316a36af3a5.zip
Add plugin ACL for source ip filtering (#3103)
* Add plugin ACL for source ip filtering Signed-off-by: An Xiao <hac@zju.edu.cn> * Allow all arguments to be optional and support multiple qtypes in a single policy Signed-off-by: An Xiao <hac@zju.edu.cn> * Add newline before third party imports Signed-off-by: An Xiao <hac@zju.edu.cn> * Use camel instead of underscore in method name Signed-off-by: An Xiao <hac@zju.edu.cn> * Start with an upper case letter in t.Errorf() Signed-off-by: An Xiao <hac@zju.edu.cn> * Use the qtype parse logic in miekg/dns Signed-off-by: An Xiao <hac@zju.edu.cn> * Use third party trie implementation as the ip filter Signed-off-by: An Xiao <hac@zju.edu.cn> * Update based on rdrozhdzh's comment Signed-off-by: An Xiao <hac@zju.edu.cn> * Change the type of action to int Signed-off-by: An Xiao <hac@zju.edu.cn> * Add IPv6 support Signed-off-by: An Xiao <hac@zju.edu.cn> * Update plugin.cfg Signed-off-by: An Xiao <hac@zju.edu.cn> * Remove file functionality Signed-off-by: An Xiao <hac@zju.edu.cn> * Update Signed-off-by: Xiao An <hac@zju.edu.cn> * Update README Signed-off-by: Xiao An <hac@zju.edu.cn> * remove comments Signed-off-by: Xiao An <hac@zju.edu.cn> * update Signed-off-by: Xiao An <hac@zju.edu.cn> * Update dependency Signed-off-by: Xiao An <hac@zju.edu.cn> * Update Signed-off-by: Xiao An <hac@zju.edu.cn> * Update test Signed-off-by: Xiao An <hac@zju.edu.cn> * Add OWNERS Signed-off-by: Xiao An <hac@zju.edu.cn> * Refactor shouldBlock and skip useless check Signed-off-by: Xiao An <hac@zju.edu.cn> * Introduce ActionNone Signed-off-by: Xiao An <hac@zju.edu.cn> * Update label name Signed-off-by: Xiao An <hac@zju.edu.cn> * Avoid capitalizing private types Signed-off-by: Xiao An <hac@zju.edu.cn>
Diffstat (limited to 'plugin')
-rw-r--r--plugin/acl/OWNERS7
-rw-r--r--plugin/acl/README.md68
-rw-r--r--plugin/acl/acl.go115
-rw-r--r--plugin/acl/acl_test.go396
-rw-r--r--plugin/acl/metrics.go24
-rw-r--r--plugin/acl/setup.go166
-rw-r--r--plugin/acl/setup_test.go245
7 files changed, 1021 insertions, 0 deletions
diff --git a/plugin/acl/OWNERS b/plugin/acl/OWNERS
new file mode 100644
index 000000000..5921fce19
--- /dev/null
+++ b/plugin/acl/OWNERS
@@ -0,0 +1,7 @@
+reviewers:
+ - miekg
+ - ihac
+approvers:
+ - miekg
+ - ihac
+
diff --git a/plugin/acl/README.md b/plugin/acl/README.md
new file mode 100644
index 000000000..49b6895a9
--- /dev/null
+++ b/plugin/acl/README.md
@@ -0,0 +1,68 @@
+# acl
+
+*acl* - enforces access control policies on source ip and prevents unauthorized access to DNS servers.
+
+## Description
+
+With `acl` enabled, users are able to block suspicous DNS queries by configuring IP filter rule sets, i.e. allowing authorized queries to recurse or blocking unauthorized queries.
+
+This plugin can be used multiple times per Server Block.
+
+## Syntax
+
+```
+acl [ZONES...] {
+ ACTION [type QTYPE...] [net SOURCE...]
+}
+```
+
+- **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block are used.
+- **ACTION** (*allow* or *block*) defines the way to deal with DNS queries matched by this rule. The default action is *allow*, which means a DNS query not matched by any rules will be allowed to recurse.
+- **QTYPE** is the query type to match for the requests to be allowed or blocked. Common resource record types are supported. `*` stands for all record types. The default behavior for an omitted `type QTYPE...` is to match all kinds of DNS queries (same as `type *`).
+- **SOURCE** is the source IP address to match for the requests to be allowed or blocked. Typical CIDR notation and single IP address are supported. `*` stands for all possible source IP addresses.
+
+## Examples
+
+To demonstrate the usage of plugin acl, here we provide some typical examples.
+
+Block all DNS queries with record type A from 192.168.0.0/16:
+
+~~~ Corefile
+. {
+ acl {
+ block type A net 192.168.0.0/16
+ }
+}
+~~~
+
+Block all DNS queries from 192.168.0.0/16 except for 192.168.1.0/24:
+
+~~~ Corefile
+. {
+ acl {
+ allow net 192.168.1.0/24
+ block net 192.168.0.0/16
+ }
+}
+```
+
+Allow only DNS queries from 192.168.0.0/24 and 192.168.1.0/24:
+
+~~~ Corefile
+. {
+ acl {
+ allow net 192.168.0.0/16 192.168.1.0/24
+ block
+ }
+}
+~~~
+
+Block all DNS queries from 192.168.1.0/24 towards a.example.org:
+
+~~~ Corefile
+example.org {
+ acl a.example.org {
+ block net 192.168.1.0/24
+ }
+}
+~~~
diff --git a/plugin/acl/acl.go b/plugin/acl/acl.go
new file mode 100644
index 000000000..b25138a30
--- /dev/null
+++ b/plugin/acl/acl.go
@@ -0,0 +1,115 @@
+package acl
+
+import (
+ "context"
+ "net"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/metrics"
+ clog "github.com/coredns/coredns/plugin/pkg/log"
+ "github.com/coredns/coredns/request"
+
+ "github.com/infobloxopen/go-trees/iptree"
+ "github.com/miekg/dns"
+)
+
+var log = clog.NewWithPlugin("acl")
+
+// ACL enforces access control policies on DNS queries.
+type ACL struct {
+ Next plugin.Handler
+
+ Rules []rule
+}
+
+// rule defines a list of Zones and some ACL policies which will be
+// enforced on them.
+type rule struct {
+ zones []string
+ policies []policy
+}
+
+// action defines the action against queries.
+type action int
+
+// policy defines the ACL policy for DNS queries.
+// A policy performs the specified action (block/allow) on all DNS queries
+// matched by source IP or QTYPE.
+type policy struct {
+ action action
+ qtypes map[uint16]struct{}
+ filter *iptree.Tree
+}
+
+const (
+ // actionNone does nothing on the queries.
+ actionNone = iota
+ // actionAllow allows authorized queries to recurse.
+ actionAllow
+ // actionBlock blocks unauthorized queries towards protected DNS zones.
+ actionBlock
+)
+
+// ServeDNS implements the plugin.Handler interface.
+func (a ACL) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ state := request.Request{W: w, Req: r}
+
+RulesCheckLoop:
+ for _, rule := range a.Rules {
+ // check zone.
+ zone := plugin.Zones(rule.zones).Matches(state.Name())
+ if zone == "" {
+ continue
+ }
+
+ action := matchWithPolicies(rule.policies, w, r)
+ switch action {
+ case actionBlock:
+ {
+ m := new(dns.Msg)
+ m.SetRcode(r, dns.RcodeRefused)
+ w.WriteMsg(m)
+ RequestBlockCount.WithLabelValues(metrics.WithServer(ctx), zone).Inc()
+ return dns.RcodeSuccess, nil
+ }
+ case actionAllow:
+ {
+ break RulesCheckLoop
+ }
+ }
+ }
+
+ RequestAllowCount.WithLabelValues(metrics.WithServer(ctx)).Inc()
+ return plugin.NextOrFailure(state.Name(), a.Next, ctx, w, r)
+}
+
+// matchWithPolicies matches the DNS query with a list of ACL polices and returns suitable
+// action agains the query.
+func matchWithPolicies(policies []policy, w dns.ResponseWriter, r *dns.Msg) action {
+ state := request.Request{W: w, Req: r}
+
+ ip := net.ParseIP(state.IP())
+ qtype := state.QType()
+ for _, policy := range policies {
+ // dns.TypeNone matches all query types.
+ _, matchAll := policy.qtypes[dns.TypeNone]
+ _, match := policy.qtypes[qtype]
+ if !matchAll && !match {
+ continue
+ }
+
+ _, contained := policy.filter.GetByIP(ip)
+ if !contained {
+ continue
+ }
+
+ // matched.
+ return policy.action
+ }
+ return actionNone
+}
+
+// Name implements the plugin.Handler interface.
+func (a ACL) Name() string {
+ return "acl"
+}
diff --git a/plugin/acl/acl_test.go b/plugin/acl/acl_test.go
new file mode 100644
index 000000000..9b23edc53
--- /dev/null
+++ b/plugin/acl/acl_test.go
@@ -0,0 +1,396 @@
+package acl
+
+import (
+ "context"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/caddyserver/caddy"
+ "github.com/miekg/dns"
+)
+
+type testResponseWriter struct {
+ test.ResponseWriter
+ Rcode int
+}
+
+func (t *testResponseWriter) setRemoteIP(ip string) {
+ t.RemoteIP = ip
+}
+
+// WriteMsg implement dns.ResponseWriter interface.
+func (t *testResponseWriter) WriteMsg(m *dns.Msg) error {
+ t.Rcode = m.Rcode
+ return nil
+}
+
+func NewTestControllerWithZones(input string, zones []string) *caddy.Controller {
+ ctr := caddy.NewTestController("dns", input)
+ for _, zone := range zones {
+ ctr.ServerBlockKeys = append(ctr.ServerBlockKeys, zone)
+ }
+ return ctr
+}
+
+func TestACLServeDNS(t *testing.T) {
+ type args struct {
+ domain string
+ sourceIP string
+ qtype uint16
+ }
+ tests := []struct {
+ name string
+ config string
+ zones []string
+ args args
+ wantRcode int
+ wantErr bool
+ }{
+ // IPv4 tests.
+ {
+ "Blacklist 1 BLOCKED",
+ `acl example.org {
+ block type A net 192.168.0.0/16
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "192.168.0.2",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Blacklist 1 ALLOWED",
+ `acl example.org {
+ block type A net 192.168.0.0/16
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "192.167.0.2",
+ dns.TypeA,
+ },
+ dns.RcodeSuccess,
+ false,
+ },
+ {
+ "Blacklist 2 BLOCKED",
+ `
+ acl example.org {
+ block type * net 192.168.0.0/16
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "192.168.0.2",
+ dns.TypeAAAA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Blacklist 3 BLOCKED",
+ `acl example.org {
+ block type A
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "10.1.0.2",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Blacklist 3 ALLOWED",
+ `acl example.org {
+ block type A
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "10.1.0.2",
+ dns.TypeAAAA,
+ },
+ dns.RcodeSuccess,
+ false,
+ },
+ {
+ "Blacklist 4 Single IP BLOCKED",
+ `acl example.org {
+ block type A net 192.168.1.2
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "192.168.1.2",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Blacklist 4 Single IP ALLOWED",
+ `acl example.org {
+ block type A net 192.168.1.2
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "192.168.1.3",
+ dns.TypeA,
+ },
+ dns.RcodeSuccess,
+ false,
+ },
+ {
+ "Whitelist 1 ALLOWED",
+ `acl example.org {
+ allow net 192.168.0.0/16
+ block
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "192.168.0.2",
+ dns.TypeA,
+ },
+ dns.RcodeSuccess,
+ false,
+ },
+ {
+ "Whitelist 1 REFUSED",
+ `acl example.org {
+ allow type * net 192.168.0.0/16
+ block
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "10.1.0.2",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Fine-Grained 1 REFUSED",
+ `acl a.example.org {
+ block type * net 192.168.1.0/24
+ }`,
+ []string{"example.org"},
+ args{
+ "a.example.org.",
+ "192.168.1.2",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Fine-Grained 1 ALLOWED",
+ `acl a.example.org {
+ block net 192.168.1.0/24
+ }`,
+ []string{"example.org"},
+ args{
+ "www.example.org.",
+ "192.168.1.2",
+ dns.TypeA,
+ },
+ dns.RcodeSuccess,
+ false,
+ },
+ {
+ "Fine-Grained 2 REFUSED",
+ `acl {
+ block net 192.168.1.0/24
+ }`,
+ []string{"example.org"},
+ args{
+ "a.example.org.",
+ "192.168.1.2",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Fine-Grained 2 ALLOWED",
+ `acl {
+ block net 192.168.1.0/24
+ }`,
+ []string{"example.org"},
+ args{
+ "a.example.com.",
+ "192.168.1.2",
+ dns.TypeA,
+ },
+ dns.RcodeSuccess,
+ false,
+ },
+ {
+ "Fine-Grained 3 REFUSED",
+ `acl a.example.org {
+ block net 192.168.1.0/24
+ }
+ acl b.example.org {
+ block type * net 192.168.2.0/24
+ }`,
+ []string{"example.org"},
+ args{
+ "b.example.org.",
+ "192.168.2.2",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Fine-Grained 3 ALLOWED",
+ `acl a.example.org {
+ block net 192.168.1.0/24
+ }
+ acl b.example.org {
+ block net 192.168.2.0/24
+ }`,
+ []string{"example.org"},
+ args{
+ "b.example.org.",
+ "192.168.1.2",
+ dns.TypeA,
+ },
+ dns.RcodeSuccess,
+ false,
+ },
+ // IPv6 tests.
+ {
+ "Blacklist 1 BLOCKED IPv6",
+ `acl example.org {
+ block type A net 2001:db8:abcd:0012::0/64
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "2001:db8:abcd:0012::1230",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Blacklist 1 ALLOWED IPv6",
+ `acl example.org {
+ block type A net 2001:db8:abcd:0012::0/64
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "2001:db8:abcd:0013::0",
+ dns.TypeA,
+ },
+ dns.RcodeSuccess,
+ false,
+ },
+ {
+ "Blacklist 2 BLOCKED IPv6",
+ `acl example.org {
+ block type A
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Blacklist 3 Single IP BLOCKED IPv6",
+ `acl example.org {
+ block type A net 2001:0db8:85a3:0000:0000:8a2e:0370:7334
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Blacklist 3 Single IP ALLOWED IPv6",
+ `acl example.org {
+ block type A net 2001:0db8:85a3:0000:0000:8a2e:0370:7334
+ }`,
+ []string{},
+ args{
+ "www.example.org.",
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7335",
+ dns.TypeA,
+ },
+ dns.RcodeSuccess,
+ false,
+ },
+ {
+ "Fine-Grained 1 REFUSED IPv6",
+ `acl a.example.org {
+ block type * net 2001:db8:abcd:0012::0/64
+ }`,
+ []string{"example.org"},
+ args{
+ "a.example.org.",
+ "2001:db8:abcd:0012:2019::0",
+ dns.TypeA,
+ },
+ dns.RcodeRefused,
+ false,
+ },
+ {
+ "Fine-Grained 1 ALLOWED IPv6",
+ `acl a.example.org {
+ block net 2001:db8:abcd:0012::0/64
+ }`,
+ []string{"example.org"},
+ args{
+ "www.example.org.",
+ "2001:db8:abcd:0012:2019::0",
+ dns.TypeA,
+ },
+ dns.RcodeSuccess,
+ false,
+ },
+ }
+
+ ctx := context.Background()
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctr := NewTestControllerWithZones(tt.config, tt.zones)
+ a, err := parse(ctr)
+ a.Next = test.NextHandler(dns.RcodeSuccess, nil)
+ if err != nil {
+ t.Errorf("Error: Cannot parse acl from config: %v", err)
+ return
+ }
+
+ w := &testResponseWriter{}
+ m := new(dns.Msg)
+ w.setRemoteIP(tt.args.sourceIP)
+ m.SetQuestion(tt.args.domain, tt.args.qtype)
+ _, err = a.ServeDNS(ctx, w, m)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Error: acl.ServeDNS() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if w.Rcode != tt.wantRcode {
+ t.Errorf("Error: acl.ServeDNS() Rcode = %v, want %v", w.Rcode, tt.wantRcode)
+ }
+ })
+ }
+}
diff --git a/plugin/acl/metrics.go b/plugin/acl/metrics.go
new file mode 100644
index 000000000..442ea2374
--- /dev/null
+++ b/plugin/acl/metrics.go
@@ -0,0 +1,24 @@
+package acl
+
+import (
+ "github.com/coredns/coredns/plugin"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+var (
+ // RequestBlockCount is the number of DNS requests being blocked.
+ RequestBlockCount = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: "dns",
+ Name: "request_block_count_total",
+ Help: "Counter of DNS requests being blocked.",
+ }, []string{"server", "zone"})
+ // RequestAllowCount is the number of DNS requests being Allowed.
+ RequestAllowCount = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: plugin.Namespace,
+ Subsystem: "dns",
+ Name: "request_allow_count_total",
+ Help: "Counter of DNS requests being allowed.",
+ }, []string{"server"})
+)
diff --git a/plugin/acl/setup.go b/plugin/acl/setup.go
new file mode 100644
index 000000000..1179175dd
--- /dev/null
+++ b/plugin/acl/setup.go
@@ -0,0 +1,166 @@
+package acl
+
+import (
+ "net"
+ "strings"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/metrics"
+
+ "github.com/caddyserver/caddy"
+ "github.com/infobloxopen/go-trees/iptree"
+ "github.com/miekg/dns"
+)
+
+func init() {
+ caddy.RegisterPlugin("acl", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func newDefaultFilter() *iptree.Tree {
+ defaultFilter := iptree.NewTree()
+ _, IPv4All, _ := net.ParseCIDR("0.0.0.0/0")
+ _, IPv6All, _ := net.ParseCIDR("::/0")
+ defaultFilter.InplaceInsertNet(IPv4All, struct{}{})
+ defaultFilter.InplaceInsertNet(IPv6All, struct{}{})
+ return defaultFilter
+}
+
+func setup(c *caddy.Controller) error {
+ a, err := parse(c)
+ if err != nil {
+ return plugin.Error("acl", err)
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ a.Next = next
+ return a
+ })
+
+ // Register all metrics.
+ c.OnStartup(func() error {
+ metrics.MustRegister(c, RequestBlockCount, RequestAllowCount)
+ return nil
+ })
+ return nil
+}
+
+func parse(c *caddy.Controller) (ACL, error) {
+ a := ACL{}
+ for c.Next() {
+ r := rule{}
+ r.zones = c.RemainingArgs()
+ if len(r.zones) == 0 {
+ // if empty, the zones from the configuration block are used.
+ r.zones = make([]string, len(c.ServerBlockKeys))
+ copy(r.zones, c.ServerBlockKeys)
+ }
+ for i := range r.zones {
+ r.zones[i] = plugin.Host(r.zones[i]).Normalize()
+ }
+
+ for c.NextBlock() {
+ p := policy{}
+
+ action := strings.ToLower(c.Val())
+ if action == "allow" {
+ p.action = actionAllow
+ } else if action == "block" {
+ p.action = actionBlock
+ } else {
+ return a, c.Errf("unexpected token %q; expect 'allow' or 'block'", c.Val())
+ }
+
+ p.qtypes = make(map[uint16]struct{})
+ p.filter = iptree.NewTree()
+
+ hasTypeSection := false
+ hasNetSection := false
+
+ remainingTokens := c.RemainingArgs()
+ for len(remainingTokens) > 0 {
+ if !isPreservedIdentifier(remainingTokens[0]) {
+ return a, c.Errf("unexpected token %q; expect 'type | net'", remainingTokens[0])
+ }
+ section := strings.ToLower(remainingTokens[0])
+
+ i := 1
+ var tokens []string
+ for ; i < len(remainingTokens) && !isPreservedIdentifier(remainingTokens[i]); i++ {
+ tokens = append(tokens, remainingTokens[i])
+ }
+ remainingTokens = remainingTokens[i:]
+
+ if len(tokens) == 0 {
+ return a, c.Errf("no token specified in %q section", section)
+ }
+
+ switch section {
+ case "type":
+ hasTypeSection = true
+ for _, token := range tokens {
+ if token == "*" {
+ p.qtypes[dns.TypeNone] = struct{}{}
+ break
+ }
+ qtype, ok := dns.StringToType[token]
+ if !ok {
+ return a, c.Errf("unexpected token %q; expect legal QTYPE", token)
+ }
+ p.qtypes[qtype] = struct{}{}
+ }
+ case "net":
+ hasNetSection = true
+ for _, token := range tokens {
+ if token == "*" {
+ p.filter = newDefaultFilter()
+ break
+ }
+ token = normalize(token)
+ _, source, err := net.ParseCIDR(token)
+ if err != nil {
+ return a, c.Errf("illegal CIDR notation %q", token)
+ }
+ p.filter.InplaceInsertNet(source, struct{}{})
+ }
+ default:
+ return a, c.Errf("unexpected token %q; expect 'type | net'", section)
+ }
+ }
+
+ // optional `type` section means all record types.
+ if !hasTypeSection {
+ p.qtypes[dns.TypeNone] = struct{}{}
+ }
+
+ // optional `net` means all ip addresses.
+ if !hasNetSection {
+ p.filter = newDefaultFilter()
+ }
+
+ r.policies = append(r.policies, p)
+ }
+ a.Rules = append(a.Rules, r)
+ }
+ return a, nil
+}
+
+func isPreservedIdentifier(token string) bool {
+ identifier := strings.ToLower(token)
+ return identifier == "type" || identifier == "net"
+}
+
+// normalize appends '/32' for any single IPv4 address and '/128' for IPv6.
+func normalize(rawNet string) string {
+ if idx := strings.IndexAny(rawNet, "/"); idx >= 0 {
+ return rawNet
+ }
+
+ if idx := strings.IndexAny(rawNet, ":"); idx >= 0 {
+ return rawNet + "/128"
+ }
+ return rawNet + "/32"
+}
diff --git a/plugin/acl/setup_test.go b/plugin/acl/setup_test.go
new file mode 100644
index 000000000..f48da3f24
--- /dev/null
+++ b/plugin/acl/setup_test.go
@@ -0,0 +1,245 @@
+package acl
+
+import (
+ "testing"
+
+ "github.com/caddyserver/caddy"
+)
+
+func TestSetup(t *testing.T) {
+ tests := []struct {
+ name string
+ config string
+ wantErr bool
+ }{
+ // IPv4 tests.
+ {
+ "Blacklist 1",
+ `acl {
+ block type A net 192.168.0.0/16
+ }`,
+ false,
+ },
+ {
+ "Blacklist 2",
+ `acl {
+ block type * net 192.168.0.0/16
+ }`,
+ false,
+ },
+ {
+ "Blacklist 3",
+ `acl {
+ block type A net *
+ }`,
+ false,
+ },
+ {
+ "Blacklist 4",
+ `acl {
+ allow type * net 192.168.1.0/24
+ block type * net 192.168.0.0/16
+ }`,
+ false,
+ },
+ {
+ "Whitelist 1",
+ `acl {
+ allow type * net 192.168.0.0/16
+ block type * net *
+ }`,
+ false,
+ },
+ {
+ "fine-grained 1",
+ `acl a.example.org {
+ block type * net 192.168.1.0/24
+ }`,
+ false,
+ },
+ {
+ "fine-grained 2",
+ `acl a.example.org {
+ block type * net 192.168.1.0/24
+ }
+ acl b.example.org {
+ block type * net 192.168.2.0/24
+ }`,
+ false,
+ },
+ {
+ "Multiple Networks 1",
+ `acl example.org {
+ block type * net 192.168.1.0/24 192.168.3.0/24
+ }`,
+ false,
+ },
+ {
+ "Multiple Qtypes 1",
+ `acl example.org {
+ block type TXT ANY CNAME net 192.168.3.0/24
+ }`,
+ false,
+ },
+ {
+ "Missing argument 1",
+ `acl {
+ block A net 192.168.0.0/16
+ }`,
+ true,
+ },
+ {
+ "Missing argument 2",
+ `acl {
+ block type net 192.168.0.0/16
+ }`,
+ true,
+ },
+ {
+ "Illegal argument 1",
+ `acl {
+ block type ABC net 192.168.0.0/16
+ }`,
+ true,
+ },
+ {
+ "Illegal argument 2",
+ `acl {
+ blck type A net 192.168.0.0/16
+ }`,
+ true,
+ },
+ {
+ "Illegal argument 3",
+ `acl {
+ block type A net 192.168.0/16
+ }`,
+ true,
+ },
+ {
+ "Illegal argument 4",
+ `acl {
+ block type A net 192.168.0.0/33
+ }`,
+ true,
+ },
+ // IPv6 tests.
+ {
+ "Blacklist 1 IPv6",
+ `acl {
+ block type A net 2001:0db8:85a3:0000:0000:8a2e:0370:7334
+ }`,
+ false,
+ },
+ {
+ "Blacklist 2 IPv6",
+ `acl {
+ block type * net 2001:db8:85a3::8a2e:370:7334
+ }`,
+ false,
+ },
+ {
+ "Blacklist 3 IPv6",
+ `acl {
+ block type A
+ }`,
+ false,
+ },
+ {
+ "Blacklist 4 IPv6",
+ `acl {
+ allow net 2001:db8:abcd:0012::0/64
+ block net 2001:db8:abcd:0012::0/48
+ }`,
+ false,
+ },
+ {
+ "Whitelist 1 IPv6",
+ `acl {
+ allow net 2001:db8:abcd:0012::0/64
+ block
+ }`,
+ false,
+ },
+ {
+ "fine-grained 1 IPv6",
+ `acl a.example.org {
+ block net 2001:db8:abcd:0012::0/64
+ }`,
+ false,
+ },
+ {
+ "fine-grained 2 IPv6",
+ `acl a.example.org {
+ block net 2001:db8:abcd:0012::0/64
+ }
+ acl b.example.org {
+ block net 2001:db8:abcd:0013::0/64
+ }`,
+ false,
+ },
+ {
+ "Multiple Networks 1 IPv6",
+ `acl example.org {
+ block net 2001:db8:abcd:0012::0/64 2001:db8:85a3::8a2e:370:7334/64
+ }`,
+ false,
+ },
+ {
+ "Illegal argument 1 IPv6",
+ `acl {
+ block type A net 2001::85a3::8a2e:370:7334
+ }`,
+ true,
+ },
+ {
+ "Illegal argument 2 IPv6",
+ `acl {
+ block type A net 2001:db8:85a3:::8a2e:370:7334
+ }`,
+ true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctr := caddy.NewTestController("dns", tt.config)
+ if err := setup(ctr); (err != nil) != tt.wantErr {
+ t.Errorf("Error: setup() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestNormalize(t *testing.T) {
+ type args struct {
+ rawNet string
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ }{
+ {
+ "Network range 1",
+ args{"10.218.10.8/24"},
+ "10.218.10.8/24",
+ },
+ {
+ "IP address 1",
+ args{"10.218.10.8"},
+ "10.218.10.8/32",
+ },
+ {
+ "IPv6 address 1",
+ args{"2001:0db8:85a3:0000:0000:8a2e:0370:7334"},
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334/128",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := normalize(tt.args.rawNet); got != tt.want {
+ t.Errorf("Error: normalize() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}