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:
An Xiao
2019-09-04 23:43:45 +08:00
committed by Yong Tang
parent 7894154bfd
commit 79f37a1460
12 changed files with 1031 additions and 26 deletions

7
plugin/acl/OWNERS Normal file
View File

@@ -0,0 +1,7 @@
reviewers:
- miekg
- ihac
approvers:
- miekg
- ihac

68
plugin/acl/README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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)
}
})
}
}