summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/pkg/iprange
diff options
context:
space:
mode:
Diffstat (limited to 'src/go/collectors/go.d.plugin/pkg/iprange')
-rw-r--r--src/go/collectors/go.d.plugin/pkg/iprange/README.md37
-rw-r--r--src/go/collectors/go.d.plugin/pkg/iprange/parse.go138
-rw-r--r--src/go/collectors/go.d.plugin/pkg/iprange/parse_test.go258
-rw-r--r--src/go/collectors/go.d.plugin/pkg/iprange/pool.go40
-rw-r--r--src/go/collectors/go.d.plugin/pkg/iprange/pool_test.go104
-rw-r--r--src/go/collectors/go.d.plugin/pkg/iprange/range.go100
-rw-r--r--src/go/collectors/go.d.plugin/pkg/iprange/range_test.go200
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))
+ }
+ })
+ }
+}