diff options
author | 2019-09-04 23:43:45 +0800 | |
---|---|---|
committer | 2019-09-04 08:43:45 -0700 | |
commit | 79f37a1460cc52ce6c63110f4df33316a36af3a5 (patch) | |
tree | 35b3ddcc68ba82eabb9393cb166b5de76a42bb29 /plugin | |
parent | 7894154bfd2f1960c6842318d8ee99c194a04179 (diff) | |
download | coredns-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/OWNERS | 7 | ||||
-rw-r--r-- | plugin/acl/README.md | 68 | ||||
-rw-r--r-- | plugin/acl/acl.go | 115 | ||||
-rw-r--r-- | plugin/acl/acl_test.go | 396 | ||||
-rw-r--r-- | plugin/acl/metrics.go | 24 | ||||
-rw-r--r-- | plugin/acl/setup.go | 166 | ||||
-rw-r--r-- | plugin/acl/setup_test.go | 245 |
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) + } + }) + } +} |