diff options
Diffstat (limited to 'src/go/collectors/go.d.plugin/pkg/iprange')
-rw-r--r-- | src/go/collectors/go.d.plugin/pkg/iprange/README.md | 37 | ||||
-rw-r--r-- | src/go/collectors/go.d.plugin/pkg/iprange/parse.go | 138 | ||||
-rw-r--r-- | src/go/collectors/go.d.plugin/pkg/iprange/parse_test.go | 258 | ||||
-rw-r--r-- | src/go/collectors/go.d.plugin/pkg/iprange/pool.go | 40 | ||||
-rw-r--r-- | src/go/collectors/go.d.plugin/pkg/iprange/pool_test.go | 104 | ||||
-rw-r--r-- | src/go/collectors/go.d.plugin/pkg/iprange/range.go | 100 | ||||
-rw-r--r-- | src/go/collectors/go.d.plugin/pkg/iprange/range_test.go | 200 |
7 files changed, 877 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/pkg/iprange/README.md b/src/go/collectors/go.d.plugin/pkg/iprange/README.md new file mode 100644 index 000000000..4020bba02 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/iprange/README.md @@ -0,0 +1,37 @@ +<!-- +title: "iprange" +custom_edit_url: "https://github.com/netdata/netdata/blob/master/src/go/collectors/go.d.plugin/pkg/iprange/README.md" +sidebar_label: "iprange" +learn_status: "Published" +learn_rel_path: "Developers/External plugins/go.d.plugin/Helper Packages" +--> + +# iprange + +This package helps you to work with IP ranges. + +IP range is a set of IP addresses. Both IPv4 and IPv6 are supported. + +IP range interface: + +``` +type Range interface { + Family() Family + Contains(ip net.IP) bool + Size() *big.Int + fmt.Stringer +} +``` + +## Supported formats + +- `IPv4 address` (192.0.2.1) +- `IPv4 range` (192.0.2.0-192.0.2.10) +- `IPv4 CIDR` (192.0.2.0/24) +- `IPv4 subnet mask` (192.0.2.0/255.255.255.0) +- `IPv6 address` (2001:db8::1) +- `IPv6 range` (2001:db8::-2001:db8::10) +- `IPv6 CIDR` (2001:db8::/64) + +IP range doesn't contain network and broadcast IP addresses if the format is `IPv4 CIDR`, `IPv4 subnet mask` +or `IPv6 CIDR`. diff --git a/src/go/collectors/go.d.plugin/pkg/iprange/parse.go b/src/go/collectors/go.d.plugin/pkg/iprange/parse.go new file mode 100644 index 000000000..3471702a1 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/iprange/parse.go @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package iprange + +import ( + "bytes" + "fmt" + "net" + "regexp" + "strings" + + "github.com/apparentlymart/go-cidr/cidr" +) + +// ParseRanges parses s as a space separated list of IP Ranges, returning the result and an error if any. +// IP Range can be in IPv4 address ("192.0.2.1"), IPv4 range ("192.0.2.0-192.0.2.10") +// IPv4 CIDR ("192.0.2.0/24"), IPv4 subnet mask ("192.0.2.0/255.255.255.0"), +// IPv6 address ("2001:db8::1"), IPv6 range ("2001:db8::-2001:db8::10"), +// or IPv6 CIDR ("2001:db8::/64") form. +// IPv4 CIDR, IPv4 subnet mask and IPv6 CIDR ranges don't include network and broadcast addresses. +func ParseRanges(s string) ([]Range, error) { + parts := strings.Fields(s) + if len(parts) == 0 { + return nil, nil + } + + var ranges []Range + for _, v := range parts { + r, err := ParseRange(v) + if err != nil { + return nil, err + } + + if r != nil { + ranges = append(ranges, r) + } + } + return ranges, nil +} + +var ( + reRange = regexp.MustCompile("^[0-9a-f.:-]+$") // addr | addr-addr + reCIDR = regexp.MustCompile("^[0-9a-f.:]+/[0-9]{1,3}$") // addr/prefix_length + reSubnetMask = regexp.MustCompile("^[0-9.]+/[0-9.]{7,}$") // v4_addr/mask +) + +// ParseRange parses s as an IP Range, returning the result and an error if any. +// The string s can be in IPv4 address ("192.0.2.1"), IPv4 range ("192.0.2.0-192.0.2.10") +// IPv4 CIDR ("192.0.2.0/24"), IPv4 subnet mask ("192.0.2.0/255.255.255.0"), +// IPv6 address ("2001:db8::1"), IPv6 range ("2001:db8::-2001:db8::10"), +// or IPv6 CIDR ("2001:db8::/64") form. +// IPv4 CIDR, IPv4 subnet mask and IPv6 CIDR ranges don't include network and broadcast addresses. +func ParseRange(s string) (Range, error) { + s = strings.ToLower(s) + if s == "" { + return nil, nil + } + + var r Range + switch { + case reRange.MatchString(s): + r = parseRange(s) + case reCIDR.MatchString(s): + r = parseCIDR(s) + case reSubnetMask.MatchString(s): + r = parseSubnetMask(s) + } + + if r == nil { + return nil, fmt.Errorf("ip range (%s) invalid syntax", s) + } + return r, nil +} + +func parseRange(s string) Range { + var start, end net.IP + if idx := strings.IndexByte(s, '-'); idx != -1 { + start, end = net.ParseIP(s[:idx]), net.ParseIP(s[idx+1:]) + } else { + start, end = net.ParseIP(s), net.ParseIP(s) + } + + return New(start, end) +} + +func parseCIDR(s string) Range { + ip, network, err := net.ParseCIDR(s) + if err != nil { + return nil + } + + start, end := cidr.AddressRange(network) + prefixLen, _ := network.Mask.Size() + + if isV4IP(ip) && prefixLen < 31 || isV6IP(ip) && prefixLen < 127 { + start = cidr.Inc(start) + end = cidr.Dec(end) + } + + return parseRange(fmt.Sprintf("%s-%s", start, end)) +} + +func parseSubnetMask(s string) Range { + idx := strings.LastIndexByte(s, '/') + if idx == -1 { + return nil + } + + address, mask := s[:idx], s[idx+1:] + + ip := net.ParseIP(mask).To4() + if ip == nil { + return nil + } + + prefixLen, bits := net.IPv4Mask(ip[0], ip[1], ip[2], ip[3]).Size() + if prefixLen+bits == 0 { + return nil + } + + return parseCIDR(fmt.Sprintf("%s/%d", address, prefixLen)) +} + +func isV4RangeValid(start, end net.IP) bool { + return isV4IP(start) && isV4IP(end) && bytes.Compare(end, start) >= 0 +} + +func isV6RangeValid(start, end net.IP) bool { + return isV6IP(start) && isV6IP(end) && bytes.Compare(end, start) >= 0 +} + +func isV4IP(ip net.IP) bool { + return ip.To4() != nil +} + +func isV6IP(ip net.IP) bool { + return !isV4IP(ip) && ip.To16() != nil +} diff --git a/src/go/collectors/go.d.plugin/pkg/iprange/parse_test.go b/src/go/collectors/go.d.plugin/pkg/iprange/parse_test.go new file mode 100644 index 000000000..8b4ab96b3 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/iprange/parse_test.go @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package iprange + +import ( + "fmt" + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseRanges(t *testing.T) { + tests := map[string]struct { + input string + wantRanges []Range + wantErr bool + }{ + "single range": { + input: "192.0.2.0-192.0.2.10", + wantRanges: []Range{ + prepareRange("192.0.2.0", "192.0.2.10"), + }, + }, + "multiple ranges": { + input: "2001:db8::0 192.0.2.0-192.0.2.10 2001:db8::0/126 192.0.2.0/255.255.255.0", + wantRanges: []Range{ + prepareRange("2001:db8::0", "2001:db8::0"), + prepareRange("192.0.2.0", "192.0.2.10"), + prepareRange("2001:db8::1", "2001:db8::2"), + prepareRange("192.0.2.1", "192.0.2.254"), + }, + }, + "single invalid syntax": { + input: "192.0.2.0-192.0.2.", + wantErr: true, + }, + "multiple invalid syntax": { + input: "2001:db8::0 192.0.2.0-192.0.2.10 2001:db8::0/999 192.0.2.0/255.255.255.0", + wantErr: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + rs, err := ParseRanges(test.input) + + if test.wantErr { + assert.Error(t, err) + assert.Nilf(t, rs, "want: nil, got: %s", rs) + } else { + assert.NoError(t, err) + assert.Equalf(t, test.wantRanges, rs, "want: %s, got: %s", test.wantRanges, rs) + } + }) + } +} + +func TestParseRange(t *testing.T) { + tests := map[string]struct { + input string + wantRange Range + wantErr bool + }{ + "v4 IP": { + input: "192.0.2.0", + wantRange: prepareRange("192.0.2.0", "192.0.2.0"), + }, + "v4 IP: invalid address": { + input: "192.0.2.", + wantErr: true, + }, + "v4 Range": { + input: "192.0.2.0-192.0.2.10", + wantRange: prepareRange("192.0.2.0", "192.0.2.10"), + }, + "v4 Range: start == end": { + input: "192.0.2.0-192.0.2.0", + wantRange: prepareRange("192.0.2.0", "192.0.2.0"), + }, + "v4 Range: start > end": { + input: "192.0.2.10-192.0.2.0", + wantErr: true, + }, + "v4 Range: invalid start": { + input: "192.0.2.-192.0.2.10", + wantErr: true, + }, + "v4 Range: invalid end": { + input: "192.0.2.0-192.0.2.", + wantErr: true, + }, + "v4 Range: v6 start": { + input: "2001:db8::0-192.0.2.10", + wantErr: true, + }, + "v4 Range: v6 end": { + input: "192.0.2.0-2001:db8::0", + wantErr: true, + }, + "v4 CIDR: /0": { + input: "192.0.2.0/0", + wantRange: prepareRange("0.0.0.1", "255.255.255.254"), + }, + "v4 CIDR: /24": { + input: "192.0.2.0/24", + wantRange: prepareRange("192.0.2.1", "192.0.2.254"), + }, + "v4 CIDR: /30": { + input: "192.0.2.0/30", + wantRange: prepareRange("192.0.2.1", "192.0.2.2"), + }, + "v4 CIDR: /31": { + input: "192.0.2.0/31", + wantRange: prepareRange("192.0.2.0", "192.0.2.1"), + }, + "v4 CIDR: /32": { + input: "192.0.2.0/32", + wantRange: prepareRange("192.0.2.0", "192.0.2.0"), + }, + "v4 CIDR: ip instead of host address": { + input: "192.0.2.10/24", + wantRange: prepareRange("192.0.2.1", "192.0.2.254"), + }, + "v4 CIDR: missing prefix length": { + input: "192.0.2.0/", + wantErr: true, + }, + "v4 CIDR: invalid prefix length": { + input: "192.0.2.0/99", + wantErr: true, + }, + "v4 Mask: /0": { + input: "192.0.2.0/0.0.0.0", + wantRange: prepareRange("0.0.0.1", "255.255.255.254"), + }, + "v4 Mask: /24": { + input: "192.0.2.0/255.255.255.0", + wantRange: prepareRange("192.0.2.1", "192.0.2.254"), + }, + "v4 Mask: /30": { + input: "192.0.2.0/255.255.255.252", + wantRange: prepareRange("192.0.2.1", "192.0.2.2"), + }, + "v4 Mask: /31": { + input: "192.0.2.0/255.255.255.254", + wantRange: prepareRange("192.0.2.0", "192.0.2.1"), + }, + "v4 Mask: /32": { + input: "192.0.2.0/255.255.255.255", + wantRange: prepareRange("192.0.2.0", "192.0.2.0"), + }, + "v4 Mask: missing prefix mask": { + input: "192.0.2.0/", + wantErr: true, + }, + "v4 Mask: invalid mask": { + input: "192.0.2.0/mask", + wantErr: true, + }, + "v4 Mask: not canonical form mask": { + input: "192.0.2.0/255.255.0.254", + wantErr: true, + }, + "v4 Mask: v6 address": { + input: "2001:db8::/255.255.255.0", + wantErr: true, + }, + + "v6 IP": { + input: "2001:db8::0", + wantRange: prepareRange("2001:db8::0", "2001:db8::0"), + }, + "v6 IP: invalid address": { + input: "2001:db8", + wantErr: true, + }, + "v6 Range": { + input: "2001:db8::-2001:db8::10", + wantRange: prepareRange("2001:db8::", "2001:db8::10"), + }, + "v6 Range: start == end": { + input: "2001:db8::-2001:db8::", + wantRange: prepareRange("2001:db8::", "2001:db8::"), + }, + "v6 Range: start > end": { + input: "2001:db8::10-2001:db8::", + wantErr: true, + }, + "v6 Range: invalid start": { + input: "2001:db8-2001:db8::10", + wantErr: true, + }, + "v6 Range: invalid end": { + input: "2001:db8::-2001:db8", + wantErr: true, + }, + "v6 Range: v4 start": { + input: "192.0.2.0-2001:db8::10", + wantErr: true, + }, + "v6 Range: v4 end": { + input: "2001:db8::-192.0.2.10", + wantErr: true, + }, + "v6 CIDR: /0": { + input: "2001:db8::/0", + wantRange: prepareRange("::1", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe"), + }, + "v6 CIDR: /64": { + input: "2001:db8::/64", + wantRange: prepareRange("2001:db8::1", "2001:db8::ffff:ffff:ffff:fffe"), + }, + "v6 CIDR: /126": { + input: "2001:db8::/126", + wantRange: prepareRange("2001:db8::1", "2001:db8::2"), + }, + "v6 CIDR: /127": { + input: "2001:db8::/127", + wantRange: prepareRange("2001:db8::", "2001:db8::1"), + }, + "v6 CIDR: /128": { + input: "2001:db8::/128", + wantRange: prepareRange("2001:db8::", "2001:db8::"), + }, + "v6 CIDR: ip instead of host address": { + input: "2001:db8::10/64", + wantRange: prepareRange("2001:db8::1", "2001:db8::ffff:ffff:ffff:fffe"), + }, + "v6 CIDR: missing prefix length": { + input: "2001:db8::/", + wantErr: true, + }, + "v6 CIDR: invalid prefix length": { + input: "2001:db8::/999", + wantErr: true, + }, + } + + for name, test := range tests { + name = fmt.Sprintf("%s (%s)", name, test.input) + t.Run(name, func(t *testing.T) { + r, err := ParseRange(test.input) + + if test.wantErr { + assert.Error(t, err) + assert.Nilf(t, r, "want: nil, got: %s", r) + } else { + assert.NoError(t, err) + assert.Equalf(t, test.wantRange, r, "want: %s, got: %s", test.wantRange, r) + } + }) + } +} + +func prepareRange(start, end string) Range { + return New(net.ParseIP(start), net.ParseIP(end)) +} diff --git a/src/go/collectors/go.d.plugin/pkg/iprange/pool.go b/src/go/collectors/go.d.plugin/pkg/iprange/pool.go new file mode 100644 index 000000000..48ba5689b --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/iprange/pool.go @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package iprange + +import ( + "math/big" + "net" + "strings" +) + +// Pool is a collection of IP Ranges. +type Pool []Range + +// String returns the string form of the pool. +func (p Pool) String() string { + var b strings.Builder + for _, r := range p { + b.WriteString(r.String() + " ") + } + return strings.TrimSpace(b.String()) +} + +// Size reports the number of IP addresses in the pool. +func (p Pool) Size() *big.Int { + size := big.NewInt(0) + for _, r := range p { + size.Add(size, r.Size()) + } + return size +} + +// Contains reports whether the pool includes IP. +func (p Pool) Contains(ip net.IP) bool { + for _, r := range p { + if r.Contains(ip) { + return true + } + } + return false +} diff --git a/src/go/collectors/go.d.plugin/pkg/iprange/pool_test.go b/src/go/collectors/go.d.plugin/pkg/iprange/pool_test.go new file mode 100644 index 000000000..2864b6711 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/iprange/pool_test.go @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package iprange + +import ( + "fmt" + "math/big" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPool_String(t *testing.T) { + tests := map[string]struct { + input string + wantString string + }{ + "singe": { + input: "192.0.2.0-192.0.2.10", + wantString: "192.0.2.0-192.0.2.10", + }, + "multiple": { + input: "192.0.2.0-192.0.2.10 2001:db8::-2001:db8::10", + wantString: "192.0.2.0-192.0.2.10 2001:db8::-2001:db8::10", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + rs, err := ParseRanges(test.input) + require.NoError(t, err) + p := Pool(rs) + + assert.Equal(t, test.wantString, p.String()) + }) + } +} + +func TestPool_Size(t *testing.T) { + tests := map[string]struct { + input string + wantSize *big.Int + }{ + "singe": { + input: "192.0.2.0-192.0.2.10", + wantSize: big.NewInt(11), + }, + "multiple": { + input: "192.0.2.0-192.0.2.10 2001:db8::-2001:db8::10", + wantSize: big.NewInt(11 + 17), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + rs, err := ParseRanges(test.input) + require.NoError(t, err) + p := Pool(rs) + + assert.Equal(t, test.wantSize, p.Size()) + }) + } +} + +func TestPool_Contains(t *testing.T) { + tests := map[string]struct { + input string + ip string + wantFail bool + }{ + "inside first": { + input: "192.0.2.0-192.0.2.10 192.0.2.20-192.0.2.30 2001:db8::-2001:db8::10", + ip: "192.0.2.5", + }, + "inside last": { + input: "192.0.2.0-192.0.2.10 192.0.2.20-192.0.2.30 2001:db8::-2001:db8::10", + ip: "2001:db8::5", + }, + "outside": { + input: "192.0.2.0-192.0.2.10 192.0.2.20-192.0.2.30 2001:db8::-2001:db8::10", + ip: "192.0.2.100", + wantFail: true, + }, + } + + for name, test := range tests { + name = fmt.Sprintf("%s (range: %s, ip: %s)", name, test.input, test.ip) + t.Run(name, func(t *testing.T) { + rs, err := ParseRanges(test.input) + require.NoError(t, err) + ip := net.ParseIP(test.ip) + require.NotNil(t, ip) + p := Pool(rs) + + if test.wantFail { + assert.False(t, p.Contains(ip)) + } else { + assert.True(t, p.Contains(ip)) + } + }) + } +} diff --git a/src/go/collectors/go.d.plugin/pkg/iprange/range.go b/src/go/collectors/go.d.plugin/pkg/iprange/range.go new file mode 100644 index 000000000..1fe02eace --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/iprange/range.go @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package iprange + +import ( + "bytes" + "fmt" + "math/big" + "net" +) + +// Family represents IP Range address-family. +type Family uint8 + +const ( + // V4Family is IPv4 address-family. + V4Family Family = iota + // V6Family is IPv6 address-family. + V6Family +) + +// Range represents an IP range. +type Range interface { + Family() Family + Contains(ip net.IP) bool + Size() *big.Int + fmt.Stringer +} + +// New returns new IP Range. +// If it is not a valid range (start and end IPs have different address-families, or start > end), +// New returns nil. +func New(start, end net.IP) Range { + if isV4RangeValid(start, end) { + return v4Range{start: start, end: end} + } + if isV6RangeValid(start, end) { + return v6Range{start: start, end: end} + } + return nil +} + +type v4Range struct { + start net.IP + end net.IP +} + +// String returns the string form of the range. +func (r v4Range) String() string { + return fmt.Sprintf("%s-%s", r.start, r.end) +} + +// Family returns the range address family. +func (r v4Range) Family() Family { + return V4Family +} + +// Contains reports whether the range includes IP. +func (r v4Range) Contains(ip net.IP) bool { + return bytes.Compare(ip, r.start) >= 0 && bytes.Compare(ip, r.end) <= 0 +} + +// Size reports the number of IP addresses in the range. +func (r v4Range) Size() *big.Int { + return big.NewInt(v4ToInt(r.end) - v4ToInt(r.start) + 1) +} + +type v6Range struct { + start net.IP + end net.IP +} + +// String returns the string form of the range. +func (r v6Range) String() string { + return fmt.Sprintf("%s-%s", r.start, r.end) +} + +// Family returns the range address family. +func (r v6Range) Family() Family { + return V6Family +} + +// Contains reports whether the range includes IP. +func (r v6Range) Contains(ip net.IP) bool { + return bytes.Compare(ip, r.start) >= 0 && bytes.Compare(ip, r.end) <= 0 +} + +// Size reports the number of IP addresses in the range. +func (r v6Range) Size() *big.Int { + size := big.NewInt(0) + size.Add(size, big.NewInt(0).SetBytes(r.end)) + size.Sub(size, big.NewInt(0).SetBytes(r.start)) + size.Add(size, big.NewInt(1)) + return size +} + +func v4ToInt(ip net.IP) int64 { + ip = ip.To4() + return int64(ip[0])<<24 | int64(ip[1])<<16 | int64(ip[2])<<8 | int64(ip[3]) +} diff --git a/src/go/collectors/go.d.plugin/pkg/iprange/range_test.go b/src/go/collectors/go.d.plugin/pkg/iprange/range_test.go new file mode 100644 index 000000000..631d012e0 --- /dev/null +++ b/src/go/collectors/go.d.plugin/pkg/iprange/range_test.go @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package iprange + +import ( + "fmt" + "math/big" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestV4Range_String(t *testing.T) { + tests := map[string]struct { + input string + wantString string + }{ + "IP": {input: "192.0.2.0", wantString: "192.0.2.0-192.0.2.0"}, + "Range": {input: "192.0.2.0-192.0.2.10", wantString: "192.0.2.0-192.0.2.10"}, + "CIDR": {input: "192.0.2.0/24", wantString: "192.0.2.1-192.0.2.254"}, + "Mask": {input: "192.0.2.0/255.255.255.0", wantString: "192.0.2.1-192.0.2.254"}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + r, err := ParseRange(test.input) + require.NoError(t, err) + + assert.Equal(t, test.wantString, r.String()) + }) + } +} + +func TestV4Range_Family(t *testing.T) { + tests := map[string]struct { + input string + }{ + "IP": {input: "192.0.2.0"}, + "Range": {input: "192.0.2.0-192.0.2.10"}, + "CIDR": {input: "192.0.2.0/24"}, + "Mask": {input: "192.0.2.0/255.255.255.0"}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + r, err := ParseRange(test.input) + require.NoError(t, err) + + assert.Equal(t, V4Family, r.Family()) + }) + } +} + +func TestV4Range_Size(t *testing.T) { + tests := map[string]struct { + input string + wantSize *big.Int + }{ + "IP": {input: "192.0.2.0", wantSize: big.NewInt(1)}, + "Range": {input: "192.0.2.0-192.0.2.10", wantSize: big.NewInt(11)}, + "CIDR": {input: "192.0.2.0/24", wantSize: big.NewInt(254)}, + "CIDR 31": {input: "192.0.2.0/31", wantSize: big.NewInt(2)}, + "CIDR 32": {input: "192.0.2.0/32", wantSize: big.NewInt(1)}, + "Mask": {input: "192.0.2.0/255.255.255.0", wantSize: big.NewInt(254)}, + "Mask 31": {input: "192.0.2.0/255.255.255.254", wantSize: big.NewInt(2)}, + "Mask 32": {input: "192.0.2.0/255.255.255.255", wantSize: big.NewInt(1)}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + r, err := ParseRange(test.input) + require.NoError(t, err) + + assert.Equal(t, test.wantSize, r.Size()) + }) + } +} + +func TestV4Range_Contains(t *testing.T) { + tests := map[string]struct { + input string + ip string + wantFail bool + }{ + "inside": {input: "192.0.2.0-192.0.2.10", ip: "192.0.2.5"}, + "outside": {input: "192.0.2.0-192.0.2.10", ip: "192.0.2.55", wantFail: true}, + "eq start": {input: "192.0.2.0-192.0.2.10", ip: "192.0.2.0"}, + "eq end": {input: "192.0.2.0-192.0.2.10", ip: "192.0.2.10"}, + "v6": {input: "192.0.2.0-192.0.2.10", ip: "2001:db8::", wantFail: true}, + } + + for name, test := range tests { + name = fmt.Sprintf("%s (range: %s, ip: %s)", name, test.input, test.ip) + t.Run(name, func(t *testing.T) { + r, err := ParseRange(test.input) + require.NoError(t, err) + ip := net.ParseIP(test.ip) + require.NotNil(t, ip) + + if test.wantFail { + assert.False(t, r.Contains(ip)) + } else { + assert.True(t, r.Contains(ip)) + } + }) + } +} + +func TestV6Range_String(t *testing.T) { + tests := map[string]struct { + input string + wantString string + }{ + "IP": {input: "2001:db8::", wantString: "2001:db8::-2001:db8::"}, + "Range": {input: "2001:db8::-2001:db8::10", wantString: "2001:db8::-2001:db8::10"}, + "CIDR": {input: "2001:db8::/126", wantString: "2001:db8::1-2001:db8::2"}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + r, err := ParseRange(test.input) + require.NoError(t, err) + + assert.Equal(t, test.wantString, r.String()) + }) + } +} + +func TestV6Range_Family(t *testing.T) { + tests := map[string]struct { + input string + }{ + "IP": {input: "2001:db8::"}, + "Range": {input: "2001:db8::-2001:db8::10"}, + "CIDR": {input: "2001:db8::/126"}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + r, err := ParseRange(test.input) + require.NoError(t, err) + + assert.Equal(t, V6Family, r.Family()) + }) + } +} + +func TestV6Range_Size(t *testing.T) { + tests := map[string]struct { + input string + wantSize *big.Int + }{ + "IP": {input: "2001:db8::", wantSize: big.NewInt(1)}, + "Range": {input: "2001:db8::-2001:db8::10", wantSize: big.NewInt(17)}, + "CIDR": {input: "2001:db8::/120", wantSize: big.NewInt(254)}, + "CIDR 127": {input: "2001:db8::/127", wantSize: big.NewInt(2)}, + "CIDR 128": {input: "2001:db8::/128", wantSize: big.NewInt(1)}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + r, err := ParseRange(test.input) + require.NoError(t, err) + + assert.Equal(t, test.wantSize, r.Size()) + }) + } +} + +func TestV6Range_Contains(t *testing.T) { + tests := map[string]struct { + input string + ip string + wantFail bool + }{ + "inside": {input: "2001:db8::-2001:db8::10", ip: "2001:db8::5"}, + "outside": {input: "2001:db8::-2001:db8::10", ip: "2001:db8::ff", wantFail: true}, + "eq start": {input: "2001:db8::-2001:db8::10", ip: "2001:db8::"}, + "eq end": {input: "2001:db8::-2001:db8::10", ip: "2001:db8::10"}, + "v4": {input: "2001:db8::-2001:db8::10", ip: "192.0.2.0", wantFail: true}, + } + + for name, test := range tests { + name = fmt.Sprintf("%s (range: %s, ip: %s)", name, test.input, test.ip) + t.Run(name, func(t *testing.T) { + r, err := ParseRange(test.input) + require.NoError(t, err) + ip := net.ParseIP(test.ip) + require.NotNil(t, ip) + + if test.wantFail { + assert.False(t, r.Contains(ip)) + } else { + assert.True(t, r.Contains(ip)) + } + }) + } +} |