mirror of
https://github.com/coredns/coredns.git
synced 2025-11-02 02:03:13 -05:00
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>
This commit is contained in:
7
plugin/acl/OWNERS
Normal file
7
plugin/acl/OWNERS
Normal file
@@ -0,0 +1,7 @@
|
||||
reviewers:
|
||||
- miekg
|
||||
- ihac
|
||||
approvers:
|
||||
- miekg
|
||||
- ihac
|
||||
|
||||
68
plugin/acl/README.md
Normal file
68
plugin/acl/README.md
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
~~~
|
||||
115
plugin/acl/acl.go
Normal file
115
plugin/acl/acl.go
Normal file
@@ -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"
|
||||
}
|
||||
396
plugin/acl/acl_test.go
Normal file
396
plugin/acl/acl_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
24
plugin/acl/metrics.go
Normal file
24
plugin/acl/metrics.go
Normal file
@@ -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"})
|
||||
)
|
||||
166
plugin/acl/setup.go
Normal file
166
plugin/acl/setup.go
Normal file
@@ -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"
|
||||
}
|
||||
245
plugin/acl/setup_test.go
Normal file
245
plugin/acl/setup_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user