Skip to content

Code Examples 2: Subnet Containment, Matching, Comparing

Sean C Foley edited this page Aug 14, 2024 · 23 revisions

Check if Subnet contains Address or Subnet

Containment and equality checks work the same regardless of whether something is a subnet or address. Generally, you can use the Contains method in any of the Address types for containment checks:

var containing bool = ipaddr.NewIPAddressString("1::3-4:5-6").GetAddress().
	Contains(ipaddr.NewIPAddressString("1::4:5").GetAddress()) //true

But there are other options and other considerations.

Containment within a CIDR prefix block

The most typical scenario is checking for containment within a CIDR prefix block like 10.10.20.0/30. The address 10.10.20.0/30 is parsed as a CIDR prefix block subnet because the host, the last two bits, are zero. Because such addresses are parsed directly to CIDR block subnets, IPAddressString can be used to immediately check for containment, which is a bit quicker and avoids IPAddress instance creation.

contains("10.10.20.0/30", "10.10.20.3")
contains("10.10.20.0/30", "10.10.20.5")
contains("10.10.20.0/30", "10.10.20.0/31")
contains("1::/64", "1::1")
contains("1::/64", "2::1")
contains("1::/64", "1::/32")
contains("1::/64", "1::/112")
contains("1::3-4:5-6", "1::4:5")
contains("1-2::/64", "2::")
contains("bla", "foo")

func contains(network, address string) {
	one, two := ipaddr.NewIPAddressString(network), 
		ipaddr.NewIPAddressString(address)
	fmt.Printf("%v contains %v %v\n", one, two, one.Contains(two))
}

Output:

10.10.20.0/30 contains 10.10.20.3 true
10.10.20.0/30 contains 10.10.20.5 false
10.10.20.0/30 contains 10.10.20.0/31 true
1::/64 contains 1::1 true
1::/64 contains 2::1 false
1::/64 contains 1::/32 false
1::/64 contains 1::/112 true
1::3-4:5-6 contains 1::4:5 true
1-2::/64 contains 2:: true
bla contains foo false

Containment within the enclosing CIDR prefix block

Suppose you had the address 10.10.20.1/30 instead. This is not parsed as a subnet, because the host, the last two bits, are not zero. This is parsed as the individual address 10.10.20.1.

If you wish to check the associated prefix block (the block of addresses sharing the same prefix) for containment of another subnet or address, you must get the associated prefix block, and then check that block for containment. Use toPrefixBlock() to convert any address or subnet to the associated prefix block subnet first. Of course, this also works for subnets that are already CIDR prefix blocks. If you want to get an exact look at the network and host bits, use ToBinaryString().

prefixedAddrStr, addrStr := "10.10.20.1/30", "10.10.20.3"
contains(prefixedAddrStr, addrStr)
enclosingBlockContains(prefixedAddrStr, addrStr)

prefixedAddrStr, addrStr = "1::f/64", "1::1"
contains(prefixedAddrStr, addrStr)
enclosingBlockContains(prefixedAddrStr, addrStr)

func enclosingBlockContains(network, address string) {
	one, two := ipaddr.NewIPAddressString(network),
		ipaddr.NewIPAddressString(address)
	oneAddr := one.GetAddress().ToPrefixBlock()
	twoAddr := two.GetAddress()
	fmt.Printf("%v block %v contains %v %v\n",
		one, oneAddr, twoAddr, oneAddr.Contains(twoAddr))
}

Output:

10.10.20.1/30 contains 10.10.20.3 false
10.10.20.1/30 block 10.10.20.0/30 contains 10.10.20.3 true
1::f/64 contains 1::1 false
1::f/64 block 1::/64 contains 1::1 true

Comparing just the prefix

Alternatively, you can simply check just the CIDR prefixes for containment using prefixContains or equality using prefixEquals of IPAddressString. The latter is what many libraries do in all cases, since those other libraries do not support the additional formats supported by this library. These two options ignore the CIDR host entirely.

prefixedAddrStr, addrStr := "10.10.20.1/30", "10.10.20.3"
contains(prefixedAddrStr, addrStr)
prefixContains(prefixedAddrStr, addrStr)

prefixedAddrStr, addrStr = "1::f/64", "1::1"
contains(prefixedAddrStr, addrStr)
prefixContains(prefixedAddrStr, addrStr)

func prefixContains(network, address string) {
	one, two := ipaddr.NewIPAddressString(network),
		ipaddr.NewIPAddressString(address)
	fmt.Printf("%v prefix contains %v %v\n",
		one, two, one.PrefixContains(two))
}

Output:

10.10.20.1/30 contains 10.10.20.3 false
10.10.20.1/30 prefix contains 10.10.20.3 true
1::f/64 contains 1::1 false
1::f/64 prefix contains 1::1 true

Get Position of Address in Subnet

To find the position of an address relative to a subnet, use the Enumerate method. The method is the inverse of the Increment method, as shown with this pseudo-code:

subnet.Increment(subnet.Enumerate(address)) -> address
subnet.Enumerate(subnet.Increment(value)) -> value

Applying the Enumerate method returns the position as a big.Int. The argument to Enumerate must be an individual address and not multi-valued, otherwise nil is returned.

func findPosition(subnetStr, addressStr string) *positionResult {
	subnet := ipaddr.NewIPAddressString(subnetStr).GetAddress()
	address := ipaddr.NewIPAddressString(addressStr).GetAddress()
	position := subnet.Enumerate(address)
	return &positionResult{
		subnet:   subnet,
		address:  address,
		position: position,
	}
}

The helper type positionResult illustrates how to interpret the values returned from Enumerate. There are four possibilities. The first three possibilities are: below the subnet, above the subnet, or within the subnet. The last possibility is neither above, nor below, nor within the subnet.

type positionResult struct {
	subnet, address *ipaddr.IPAddress
	position        *big.Int
}

func (p *positionResult) String() string {
	var tmp big.Int
	subnet, address, position := p.subnet, p.address, p.position
	if subnet.IsMultiple() {
		if position == nil {
			return fmt.Sprintf(
				"%v is within the bounds but not contained by %v",
				address, subnet)
		} else if position.Sign() < 0 {
			return fmt.Sprintf(
				"%v is %v below the smallest element %v of %v",
				address, tmp.Neg(position), subnet.GetLower(), subnet)
		}
		count := subnet.GetCount()
		if position.CmpAbs(count) < 0 {
			return fmt.Sprintf("%v is at index %d in %v",
				address, position, subnet)
		}
		return fmt.Sprintf(
			"%v is %v above the largest element %v of %v",
			address, tmp.Sub(position, count), subnet.GetUpper(), subnet)
	}
	if position.Sign() > 0 {
		return fmt.Sprintf("%v is %v above %v", address, position, subnet)
	} else if position.Sign() < 0 {
		return fmt.Sprintf("%v is %v below %v", address, tmp.Neg(position), subnet)
	}
	return fmt.Sprintf("%v matches %v", address, subnet)
}

Here are two cases where the address lies above the subnet bounds, followed by two cases where the address is within the subnet, followed by a case where the address is neither above, nor below, nor within the subnet.

printPosition("1000::/125", "1000::15ff")
printPosition("1000-2000::/125", "2000::15ff")

printPosition("1000::/125", "1000::7")
printPosition("1000-2000::/125", "2000::7")

printPosition("1000-2000::/125", "1a00::8")

func printPosition(subnetStr, addressStr string) {
	fmt.Println(findPosition(subnetStr, addressStr))
}

Output:

1000::15ff is 5623 above the largest element 1000::7/125 of 1000::/125
2000::15ff is 5623 above the largest element 2000::7/125 of 1000-2000::/125
1000::7 is at index 7 in 1000::/125
2000::7 is at index 32775 in 1000-2000::/125
1a00::8 is within the bounds but not contained by 1000-2000::/125

Switching to IPv4, a few example results:

printPosition("0.0.0.0/16", "0.0.2.2")
printPosition("0.0.1-3.1-3", "0.0.2.2")
printPosition("0.0.2.0/24", "0.0.2.2")

Output:

0.0.2.2 is at index 514 in 0.0.0.0/16
0.0.2.2 is at index 4 in 0.0.1-3.1-3
0.0.2.2 is at index 2 in 0.0.2.0/24

The Enumerate method provides one more way of checking containment, although not the most efficient. You can check if the index of an address in a subnet is non-negative and less than the subnet element count.

printContains("10.10.20.0/30", "10.10.20.3")
printContains("10.10.20.0/30", "10.10.20.5")

func printContains(subnetStr, addressStr string) {
	result := findPosition(subnetStr, addressStr)
	position := result.position
	contains := position != nil && position.Sign() >= 0 && 
		position.CmpAbs(result.subnet.GetCount()) < 0
	fmt.Printf("%s contains %v %t\n", result.subnet, result.address, contains)
}

Output:

10.10.20.0/30 contains 10.10.20.3 true
10.10.20.0/30 contains 10.10.20.5 false

Get Distance between Two Addresses

An address can be considered a subnet with a single element. In this library, the same type is used for both.

When the Enumerate method is called on a subnet that is a single element, then all the possibilities, whether above, below, or within the subnet, translate to the same thing. The result is the distance from the argument address to the one-element subnet, or put more simply, the distance between the two addresses.

Reusing the printPosition function from the previous example:

printPosition("1a00::ffff", "1a00::ffff")
printPosition("1a00::ffff", "1a00::1:0")
printPosition("1a00::8", "1a00::ffff")
printPosition("1a00::ffff", "1a00::8")

Output:

1a00::ffff matches 1a00::ffff
1a00::1:0 is 1 above 1a00::ffff
1a00::ffff is 65527 above 1a00::8
1a00::8 is 65527 below 1a00::ffff

Here is the full IPv4 address space size, and the same for IPv6:

printPosition("0.0.0.0", "255.255.255.255");
printPosition("::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");

Output:

255.255.255.255 is 4294967295 above 0.0.0.0
ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff is 340282366920938463463374607431768211455 above ::

We can instead use int64 for IPv4, since the size of the IPv4 address space can fit into an int64. The EnumerateIPv4 method of IPv4Address returns an int64.

subnet := ipaddr.NewIPAddressString("0.0.0.0").GetAddress().ToIPv4()
address := ipaddr.NewIPAddressString("255.255.255.255").GetAddress().ToIPv4()
var position int64
position, _ = subnet.EnumerateIPv4(address)
fmt.Printf("The distance of 255.255.255.255 from 0.0.0.0 is %v", position)

Output:

The distance of 255.255.255.255 from 0.0.0.0 is 4294967295

Check if Address or Subnet Falls within Arbitrary Address Range

func rng(lowerStr, upperStr, str string) {
	lower := ipaddr.NewIPAddressString(lowerStr).GetAddress()
	upper := ipaddr.NewIPAddressString(upperStr).GetAddress()
	addr := ipaddr.NewIPAddressString(str).GetAddress()
	rng := lower.SpanWithRange(upper)
	fmt.Printf("%v contains %v %v\n",
		rng, addr, rng.Contains(addr))
}

rng("192.200.0.0", "192.255.0.0", "192.200.3.0")
rng("2001:0db8:85a3::8a2e:0370:7334", "2001:0db8:85a3::8a00:ff:ffff",
	"2001:0db8:85a3::8a03:a:b")
rng("192.200.0.0", "192.255.0.0", "191.200.3.0")
rng("2001:0db8:85a3::8a2e:0370:7334", "2001:0db8:85a3::8a00:ff:ffff",
	"2002:0db8:85a3::8a03:a:b")

Output:

192.200.0.0 -> 192.255.0.0 contains 192.200.3.0 true
2001:db8:85a3::8a00:ff:ffff -> 2001:db8:85a3::8a2e:370:7334 contains 2001:db8:85a3::8a03:a:b true
192.200.0.0 -> 192.255.0.0 contains 191.200.3.0 false
2001:db8:85a3::8a00:ff:ffff -> 2001:db8:85a3::8a2e:370:7334 contains 2002:db8:85a3::8a03:a:b false

Select CIDR Prefix Block Subnets Containing an Address

Suppose we had some CIDR prefix blocks, and we wanted to know which ones contained a specific address. The address trie data structure allows for containment or equality checks with many prefix block subnets or addresses at once in constant time.

Starting with an array of address strings, we convert to a list of address objects.

var ipv4AddrStrs = []string{
	"62.109.3.240", "91.87.64.89", "209.97.191.87", "195.231.4.132",
	"212.253.90.111", "192.250.200.250", "26.154.36.255", "82.200.127.52",
	"103.192.63.244", "212.253.90.11", "109.197.13.33", "141.101.231.203",
	"212.74.202.30", "88.250.248.235", "188.18.253.203", "122.114.118.63",
	"186.169.171.32", "46.56.236.163", "195.231.4.217", "109.197.13.33",
	"109.197.13.149", "212.253.90.11", "194.59.250.237", "37.99.32.144",
	"185.148.82.122", "141.101.231.182", "37.99.32.76", "62.192.232.40",
	"62.109.11.226"}
addresses := make([]*ipaddr.IPv4Address, 0, len(ipv4AddrStrs))
for _, str := range ipv4AddrStrs {
	addresses = append(addresses, 
		ipaddr.NewIPAddressString(str).GetAddress().ToIPv4())
}

For this example, the CIDR prefix blocks will be /24 block of every address. We construct a trie, adding those blocks to it:

prefixBlockLen := 24
blocksTrie := ipaddr.NewIPv4AddressTrie()
for _, addr := range addresses {
	blocksTrie.Add(addr.ToPrefixBlockLen(prefixBlockLen))
}
fmt.Printf("From the list of %v addresses,\n"+
	"there are %d unique address blocks of prefix length %d\n%v\n",
	len(ipv4AddrStrs), blocksTrie.Size(), prefixBlockLen, blocksTrie)

Output:

From the list of 29 addresses,
there are 22 unique address blocks of prefix length 24

○ 0.0.0.0/0 (22)
├─○ 0.0.0.0/1 (12)
│ ├─○ 0.0.0.0/2 (6)
│ │ ├─● 26.154.36.0/24 (1)
│ │ └─○ 32.0.0.0/3 (5)
│ │   ├─○ 32.0.0.0/4 (2)
│ │   │ ├─● 37.99.32.0/24 (1)
│ │   │ └─● 46.56.236.0/24 (1)
│ │   └─○ 62.0.0.0/8 (3)
│ │     ├─○ 62.109.0.0/20 (2)
│ │     │ ├─● 62.109.3.0/24 (1)
│ │     │ └─● 62.109.11.0/24 (1)
│ │     └─● 62.192.232.0/24 (1)
│ └─○ 64.0.0.0/2 (6)
│   ├─○ 80.0.0.0/4 (3)
│   │ ├─● 82.200.127.0/24 (1)
│   │ └─○ 88.0.0.0/6 (2)
│   │   ├─● 88.250.248.0/24 (1)
│   │   └─● 91.87.64.0/24 (1)
│   └─○ 96.0.0.0/3 (3)
│     ├─○ 96.0.0.0/4 (2)
│     │ ├─● 103.192.63.0/24 (1)
│     │ └─● 109.197.13.0/24 (1)
│     └─● 122.114.118.0/24 (1)
└─○ 128.0.0.0/1 (10)
  ├─○ 128.0.0.0/2 (4)
  │ ├─● 141.101.231.0/24 (1)
  │ └─○ 184.0.0.0/5 (3)
  │   ├─○ 184.0.0.0/6 (2)
  │   │ ├─● 185.148.82.0/24 (1)
  │   │ └─● 186.169.171.0/24 (1)
  │   └─● 188.18.253.0/24 (1)
  └─○ 192.0.0.0/3 (6)
    ├─○ 192.0.0.0/6 (3)
    │ ├─● 192.250.200.0/24 (1)
    │ └─○ 194.0.0.0/7 (2)
    │   ├─● 194.59.250.0/24 (1)
    │   └─● 195.231.4.0/24 (1)
    └─○ 208.0.0.0/5 (3)
      ├─● 209.97.191.0/24 (1)
      └─○ 212.0.0.0/8 (2)
        ├─● 212.74.202.0/24 (1)
        └─● 212.253.90.0/24 (1)

With a single operation we determine whether a block contains address 188.18.253.203:

toFindAddrStr := ipaddr.NewIPAddressString("188.18.253.203")
toFindBlockFor := toFindAddrStr.GetAddress().ToIPv4()
containingPath := blocksTrie.ElementsContaining(toFindBlockFor)
containingNode := containingPath.ShortestPrefixMatch()
if containingNode != nil {
	fmt.Printf("For address %v containing block is %v\n",
		toFindBlockFor, containingNode.GetKey())
}

Output:

For address 188.18.253.203 containing block is 188.18.253.0/24

Select Addresses Contained by a CIDR Prefix Block Subnet

Using the addresses from the previous example, we construct a trie from those addresses:

addressTrie := ipaddr.NewIPv4AddressTrie()
for _, addr := range addresses {
	addressTrie.Add(addr)
}
fmt.Printf("There are %d unique addresses from the list of %d:%v\n",
	addressTrie.Size(), len(ipv4AddrStrs), addressTrie)

Output:

There are 27 unique addresses from the list of 29:
○ 0.0.0.0/0 (27)
├─○ 0.0.0.0/1 (14)
│ ├─○ 0.0.0.0/2 (7)
│ │ ├─● 26.154.36.255 (1)
│ │ └─○ 32.0.0.0/3 (6)
│ │   ├─○ 32.0.0.0/4 (3)
│ │   │ ├─○ 37.99.32.0/24 (2)
│ │   │ │ ├─● 37.99.32.76 (1)
│ │   │ │ └─● 37.99.32.144 (1)
│ │   │ └─● 46.56.236.163 (1)
│ │   └─○ 62.0.0.0/8 (3)
│ │     ├─○ 62.109.0.0/20 (2)
│ │     │ ├─● 62.109.3.240 (1)
│ │     │ └─● 62.109.11.226 (1)
│ │     └─● 62.192.232.40 (1)
│ └─○ 64.0.0.0/2 (7)
│   ├─○ 80.0.0.0/4 (3)
│   │ ├─● 82.200.127.52 (1)
│   │ └─○ 88.0.0.0/6 (2)
│   │   ├─● 88.250.248.235 (1)
│   │   └─● 91.87.64.89 (1)
│   └─○ 96.0.0.0/3 (4)
│     ├─○ 96.0.0.0/4 (3)
│     │ ├─● 103.192.63.244 (1)
│     │ └─○ 109.197.13.0/24 (2)
│     │   ├─● 109.197.13.33 (1)
│     │   └─● 109.197.13.149 (1)
│     └─● 122.114.118.63 (1)
└─○ 128.0.0.0/1 (13)
  ├─○ 128.0.0.0/2 (5)
  │ ├─○ 141.101.231.128/25 (2)
  │ │ ├─● 141.101.231.182 (1)
  │ │ └─● 141.101.231.203 (1)
  │ └─○ 184.0.0.0/5 (3)
  │   ├─○ 184.0.0.0/6 (2)
  │   │ ├─● 185.148.82.122 (1)
  │   │ └─● 186.169.171.32 (1)
  │   └─● 188.18.253.203 (1)
  └─○ 192.0.0.0/3 (8)
    ├─○ 192.0.0.0/6 (4)
    │ ├─● 192.250.200.250 (1)
    │ └─○ 194.0.0.0/7 (3)
    │   ├─● 194.59.250.237 (1)
    │   └─○ 195.231.4.128/25 (2)
    │     ├─● 195.231.4.132 (1)
    │     └─● 195.231.4.217 (1)
    └─○ 208.0.0.0/5 (4)
      ├─● 209.97.191.87 (1)
      └─○ 212.0.0.0/8 (3)
        ├─● 212.74.202.30 (1)
        └─○ 212.253.90.0/25 (2)
          ├─● 212.253.90.11 (1)
          └─● 212.253.90.111 (1)

With a single operation we determine what addresses are contained in 62.109.0.0/16:

toFindAddrsInStr := ipaddr.NewIPAddressString("62.109.0.0/16")
toFindAddrsIn := toFindAddrsInStr.GetAddress().ToIPv4()
containedNode := addressTrie.ElementsContainedBy(toFindAddrsIn)
if containedNode != nil {
	fmt.Printf("For block %v contained addresses are:\n%s\n",
		toFindAddrsIn, containedNode.TreeString(true, true))
}

Output:

For block 62.109.0.0/16 contained addresses are:

○ 62.109.0.0/20 (2)
├─● 62.109.3.240 (1)
└─● 62.109.11.226 (1)

Trying the larger /8 block and printing the results in sorted order:

largerBlock := toFindAddrsIn.AdjustPrefixLen(-8).ToPrefixBlock()
containedNode = addressTrie.ElementsContainedBy(largerBlock)
if containedNode != nil {
	fmt.Printf("For block %v contained addresses are:\n%s\n",
		largerBlock, containedNode.TreeString(true, true))
	fmt.Println("In order they are:")
	iterator := containedNode.Iterator()
	for addr := iterator.Next(); addr != nil; addr = iterator.Next() {
		fmt.Println(addr)
	}
}

Output:

For block 62.0.0.0/8 contained addresses are:

○ 62.0.0.0/8 (3)
├─○ 62.109.0.0/20 (2)
│ ├─● 62.109.3.240 (1)
│ └─● 62.109.11.226 (1)
└─● 62.192.232.40 (1)

In order they are:
62.109.3.240
62.109.11.226
62.192.232.40

​ ​

Get Prefix Blocks Common to List of Addresses

This is precisely how the address trie works, it organizes by common prefix. Using the address trie from the previous example, we iterate by block size to go from largest blocks (smallest prefix) to smallest blocks, and then skip the addresses.

fmt.Println("\nThe common prefixes, in order from largest block size to smallest:")
nodeIterator := addressTrie.BlockSizeAllNodeIterator(true)
for nodeIterator.HasNext() {
	next := nodeIterator.Next()
	nextAddr := next.GetKey()
	if nextAddr.IsPrefixed() {
		fmt.Println(nextAddr)
	} else {
		break
	}
}

Output:

The common prefixes, in order from largest block size to smallest:
0.0.0.0/0
0.0.0.0/1
128.0.0.0/1
0.0.0.0/2
64.0.0.0/2
128.0.0.0/2
32.0.0.0/3
96.0.0.0/3
192.0.0.0/3
32.0.0.0/4
80.0.0.0/4
96.0.0.0/4
184.0.0.0/5
208.0.0.0/5
88.0.0.0/6
184.0.0.0/6
192.0.0.0/6
194.0.0.0/7
62.0.0.0/8
212.0.0.0/8
62.109.0.0/20
37.99.32.0/24
109.197.13.0/24
141.101.231.128/25
195.231.4.128/25
212.253.90.0/25

Longest Prefix Match

We find the subnet with the longest matching prefix, which is the the smallest subnet containing an address. First we construct a trie from the list of subnets to be matched against:

func add(trie *ipaddr.IPv4AddressTrie, addrStr string) {
	trie.Add(ipaddr.NewIPAddressString(addrStr).GetAddress().ToIPv4())
}

trie := ipaddr.NewIPv4AddressTrie()
trie.GetRoot().SetAdded() // makes 0.0.0.0/0 an added node
add(trie, "127.0.0.1")
add(trie, "10.0.2.15")
add(trie, "8.9.8.12/31")
add(trie, "8.9.8.12/30")
add(trie, "8.9.8.9")
add(trie, "8.9.8.10")
add(trie, "8.9.8.8/29")
add(trie, "8.9.8.0/29")
add(trie, "8.9.8.0/24")

fmt.Println(trie)

Here is the trie:

● 0.0.0.0/0 (10)
└─○ 0.0.0.0/1 (9)
  ├─○ 8.0.0.0/6 (8)
  │ ├─● 8.9.8.0/24 (7)
  │ │ └─○ 8.9.8.0/28 (6)
  │ │   ├─● 8.9.8.0/29 (1)
  │ │   └─● 8.9.8.8/29 (5)
  │ │     ├─○ 8.9.8.8/30 (2)
  │ │     │ ├─● 8.9.8.9 (1)
  │ │     │ └─● 8.9.8.10 (1)
  │ │     └─● 8.9.8.12/30 (2)
  │ │       └─● 8.9.8.12/31 (1)
  │ └─● 10.0.2.15 (1)
  └─● 127.0.0.1 (1)

We can use the LongestPrefixMatch method to get the desired matching element. We can also use the ElementsContaining method to get a list of all containing subnets (all matching prefixes, not just the longest).

longPrefMatch(trie, "8.9.8.10")
longPrefMatch(trie, "8.9.8.11")
longPrefMatch(trie, "8.9.8.12")
longPrefMatch(trie, "8.9.8.13")
longPrefMatch(trie, "8.9.8.14")

func longPrefMatch(trie *ipaddr.IPv4AddressTrie, addrStr string) {
	addr := ipaddr.NewIPAddressString(addrStr).GetAddress().ToIPv4()
	result := trie.LongestPrefixMatch(addr)
	fmt.Println("Longest prefix match for " + addr.String() +
		" is " + result.String())

	path := trie.ElementsContaining(addr)
	fmt.Println("All matching prefixes: " + path.String())
}

Output:

Longest prefix match for 8.9.8.10 is 8.9.8.10
All matching prefixes: 
● 0.0.0.0/0 (4)
└─● 8.9.8.0/24 (3)
  └─● 8.9.8.8/29 (2)
    └─● 8.9.8.10 (1)

Longest prefix match for 8.9.8.11 is 8.9.8.8/29
All matching prefixes: 
● 0.0.0.0/0 (3)
└─● 8.9.8.0/24 (2)
  └─● 8.9.8.8/29 (1)

Longest prefix match for 8.9.8.12 is 8.9.8.12/31
All matching prefixes: 
● 0.0.0.0/0 (5)
└─● 8.9.8.0/24 (4)
  └─● 8.9.8.8/29 (3)
    └─● 8.9.8.12/30 (2)
      └─● 8.9.8.12/31 (1)

Longest prefix match for 8.9.8.13 is 8.9.8.12/31
All matching prefixes: 
● 0.0.0.0/0 (5)
└─● 8.9.8.0/24 (4)
  └─● 8.9.8.8/29 (3)
    └─● 8.9.8.12/30 (2)
      └─● 8.9.8.12/31 (1)

Longest prefix match for 8.9.8.14 is 8.9.8.12/30
All matching prefixes: 
● 0.0.0.0/0 (4)
└─● 8.9.8.0/24 (3)
  └─● 8.9.8.8/29 (2)
    └─● 8.9.8.12/30 (1)

Select Addresses and Subnets within Arbitrary Address Range

Starting with a list of address strings, we convert to addresses. They will be the addresses used for the example.

var ipv6AddrStrs = []string{
	"2001:4860:4860::8888", "2001:4860:4860::8844", "2620:fe::fe",
	"2620:fe::9", "2620:119:35::35", "2620:119:53::53",
	"2606:4700:4700::1111", "2606:4700:4700::1001", "2a0d:2a00:1::2",
	"2a0d:2a00:2::2", "2620:74:1b::1:1", "2620:74:1c::2:2",
	"2001:4800:780e:510:a8cf:392e:ff04:8982",
	"2001:4801:7825:103:be76:4eff:fe10:2e49",
	"2a00:5a60::ad1:0ff", "2a00:5a60::ad2:0ff",
}
addresses := make([]*ipaddr.IPv6Address, 0, len(ipv6AddrStrs))
for _, str := range ipv6AddrStrs {
	addresses = append(addresses,
		ipaddr.NewIPAddressString(str).GetAddress().ToIPv6())
}

The arbitrary address range we choose for the example is 2610:: to 2a08::, so we will now show how to find the addresses within that range.

low := ipaddr.NewIPAddressString("2610::").GetAddress().ToIPv6()
high := ipaddr.NewIPAddressString("2a08::").GetAddress().ToIPv6()

The simplest way is to simply sort in place in the slice. Once sorted, we can iterate through the slice first comparing with the lower bound of our range, and then the upper:

sort.Slice(addresses, func(i, j int) bool {
	return addresses[i].Compare(addresses[j]) < 0
})
fmt.Println("The addresses are:")
for i, addr := range addresses {
	if addr.Compare(low) >= 0 {
		for _, addr := range addresses[i:] {
			if addr.Compare(high) > 0 {
				break
			}
			fmt.Println(addr)
		}
		break
	}
}

Output:

The addresses are:
2620:74:1b::1:1
2620:74:1c::2:2
2620:fe::9
2620:fe::fe
2620:119:35::35
2620:119:53::53
2a00:5a60::ad1:ff
2a00:5a60::ad2:ff

Another way is to use an address trie. Create an address trie and add the addresses to it.

addressTrie := ipaddr.IPv6AddressTrie{}
for _, addr := range addresses {
	addressTrie.Add(addr)
}
fmt.Printf("\nThere are %d unique addresses in the trie:\n%v\n",
	addressTrie.Size(), addressTrie)	

Output:

There are 16 unique addresses in the trie:

○ ::/0 (16)
└─○ 2000::/4 (16)
  ├─○ 2000::/5 (12)
  │ ├─○ 2001:4800::/25 (4)
  │ │ ├─○ 2001:4800::/31 (2)
  │ │ │ ├─● 2001:4800:780e:510:a8cf:392e:ff04:8982 (1)
  │ │ │ └─● 2001:4801:7825:103:be76:4eff:fe10:2e49 (1)
  │ │ └─○ 2001:4860:4860::8800/120 (2)
  │ │   ├─● 2001:4860:4860::8844 (1)
  │ │   └─● 2001:4860:4860::8888 (1)
  │ └─○ 2600::/10 (8)
  │   ├─○ 2606:4700:4700::1000/119 (2)
  │   │ ├─● 2606:4700:4700::1001 (1)
  │   │ └─● 2606:4700:4700::1111 (1)
  │   └─○ 2620::/23 (6)
  │     ├─○ 2620::/24 (4)
  │     │ ├─○ 2620:74:18::/45 (2)
  │     │ │ ├─● 2620:74:1b::1:1 (1)
  │     │ │ └─● 2620:74:1c::2:2 (1)
  │     │ └─○ 2620:fe::/120 (2)
  │     │   ├─● 2620:fe::9 (1)
  │     │   └─● 2620:fe::fe (1)
  │     └─○ 2620:119::/41 (2)
  │       ├─● 2620:119:35::35 (1)
  │       └─● 2620:119:53::53 (1)
  └─○ 2a00::/12 (4)
    ├─○ 2a00:5a60::ad0:0/110 (2)
    │ ├─● 2a00:5a60::ad1:ff (1)
    │ └─● 2a00:5a60::ad2:ff (1)
    └─○ 2a0d:2a00::/46 (2)
      ├─● 2a0d:2a00:1::2 (1)
      └─● 2a0d:2a00:2::2 (1)

We find the lowest and highest in the range using the trie, then iterate through the whole trie, looking for the lowest and then highest. To display the results, we create a new trie.

lowestNode := addressTrie.CeilingAddedNode(low)
if lowestNode != nil {
	highestNode := addressTrie.FloorAddedNode(high)
	iterator := addressTrie.NodeIterator(true)
	reducedTrie := ipaddr.IPv6AddressTrie{}
	next := iterator.Next()
	for ; next != lowestNode; next = iterator.Next() {
	}
	for ; next != highestNode; next = iterator.Next() {
		reducedTrie.Add(next.GetKey())
	}
	reducedTrie.Add(next.GetKey())

	fmt.Println("\nThe trimmed trie is:", reducedTrie)
	fmt.Println("The compact trie view is:", reducedTrie.AddedNodesTreeString())
}

Output:

The trimmed trie is: 
○ ::/0 (8)
└─○ 2000::/4 (8)
  ├─○ 2620::/23 (6)
  │ ├─○ 2620::/24 (4)
  │ │ ├─○ 2620:74:18::/45 (2)
  │ │ │ ├─● 2620:74:1b::1:1 (1)
  │ │ │ └─● 2620:74:1c::2:2 (1)
  │ │ └─○ 2620:fe::/120 (2)
  │ │   ├─● 2620:fe::9 (1)
  │ │   └─● 2620:fe::fe (1)
  │ └─○ 2620:119::/41 (2)
  │   ├─● 2620:119:35::35 (1)
  │   └─● 2620:119:53::53 (1)
  └─○ 2a00:5a60::ad0:0/110 (2)
    ├─● 2a00:5a60::ad1:ff (1)
    └─● 2a00:5a60::ad2:ff (1)

The compact trie view is: 
○ ::/0
├─● 2620:74:1b::1:1
├─● 2620:74:1c::2:2
├─● 2620:fe::9
├─● 2620:fe::fe
├─● 2620:119:35::35
├─● 2620:119:53::53
├─● 2a00:5a60::ad1:ff
└─● 2a00:5a60::ad2:ff

Select Address Ranges Containing Arbitrary Address or Subnet

We start with 28 random address ranges.

ipRangeStrs := [][2]string{
    {"26.154.36.255", "82.200.127.52"}, {"26.154.36.255", "192.250.200.250"},
    {"37.99.32.76", "62.192.232.40"}, {"37.99.32.76", "141.101.231.182"},
    {"37.99.32.144", "185.148.82.122"}, {"37.99.32.144", "194.59.250.237"},
    {"46.56.236.163", "186.169.171.32"}, {"46.56.236.163", "195.231.4.217"},
    {"62.109.3.240", "91.87.64.89"}, {"62.109.11.226", "62.192.232.40"},
    {"82.200.127.52", "103.192.63.244"}, {"88.250.248.235", "188.18.253.203"},
    {"88.250.248.235", "212.74.202.30"}, {"91.87.64.89", "209.97.191.87"},
    {"103.192.63.244", "212.253.90.11"}, {"109.197.13.33", "109.197.13.149"},
    {"109.197.13.33", "141.101.231.203"}, {"109.197.13.33", "195.231.4.217"},
    {"109.197.13.33", "212.253.90.11"}, {"109.197.13.149", "212.253.90.11"},
    {"122.114.118.63", "186.169.171.32"}, {"122.114.118.63", "188.18.253.203"},
    {"141.101.231.182", "185.148.82.122"}, {"141.101.231.203", "212.74.202.30"},
    {"192.250.200.250", "212.253.90.111"}, {"194.59.250.237", "212.253.90.11"},
    {"195.231.4.132", "209.97.191.87"}, {"195.231.4.132", "212.253.90.111"},
}

We will select those ranges which contain the address 88.255.1.1.

One way is to use sequential ranges, iterating through each. This is O(n), where n is the number of ranges.

var ipRanges []*ipaddr.IPAddressSeqRange
for _, strs := range ipRangeStrs {
    addr1, addr2 := ipaddr.NewIPAddressString(strs[0]).GetAddress(),
        ipaddr.NewIPAddressString(strs[1]).GetAddress()
    rng := addr1.SpanWithRange(addr2)
    ipRanges = append(ipRanges, rng)
}

addrToCheck := ipaddr.NewIPAddressString("88.255.1.1").GetAddress()
counter := 0
for _, rng := range ipRanges {
    if rng.Contains(addrToCheck) {
        counter++
        fmt.Printf("range %d %v contains %v\n", counter, rng, addrToCheck)
    }
}

Output:

range 1 26.154.36.255 -> 192.250.200.250 contains 88.255.1.1
range 2 37.99.32.76 -> 141.101.231.182 contains 88.255.1.1
range 3 37.99.32.144 -> 185.148.82.122 contains 88.255.1.1
range 4 37.99.32.144 -> 194.59.250.237 contains 88.255.1.1
range 5 46.56.236.163 -> 186.169.171.32 contains 88.255.1.1
range 6 46.56.236.163 -> 195.231.4.217 contains 88.255.1.1
range 7 62.109.3.240 -> 91.87.64.89 contains 88.255.1.1
range 8 82.200.127.52 -> 103.192.63.244 contains 88.255.1.1
range 9 88.250.248.235 -> 188.18.253.203 contains 88.255.1.1
range 10 88.250.248.235 -> 212.74.202.30 contains 88.255.1.1

The following is an alternative algorithm using a trie. Each search is a constant time operation O(1). Building the trie is O(n). So this is best for cases where you may be doing multiple searches to find the containing ranges, and the list of ranges is fixed, or is not changing much.

We convert each range to spanning blocks that can be added to an associative address trie, and map each block to its originating range.

ipv4Trie := ipaddr.AssociativeTrie[*ipaddr.IPv4Address, []*ipaddr.IPAddressSeqRange]{}
for _, rng := range ipRanges {
	blocks := rng.SpanWithPrefixBlocks()
	for _, block := range blocks {
		ipv4Trie.Remap(block.ToIPv4(), 
			func(existing []*ipaddr.IPAddressSeqRange, found bool) (
				mapped []*ipaddr.IPAddressSeqRange, mapIt bool) {

				if !found {
					return []*ipaddr.IPAddressSeqRange{rng}, true
				}
				return append(existing, rng), true
			})
	}
}

fmt.Printf("The trie has %d blocks\n", ipv4Trie.Size())
//print the trie with: fmt.Println(ipv4Trie)

We look up the trie elements containing the address 88.255.1.1. This is a constant time operation that requires at most 32 comparisons because IPv4 address bit-size is 32. On average, it will do log(n) comparisons where n is the number of items in the trie. So in this example, the average is 9 comparisons (trie size is 421), less than the 28 comparisons required with the previous code.

From each containing block trie element, we get the associated ranges:

triePath := ipv4Trie.ElementsContaining(addrToCheck.ToIPv4())
var result []*ipaddr.IPAddressSeqRange
triePathNode := triePath.ShortestPrefixMatch()
for triePathNode != nil {
	ranges := triePathNode.GetValue()
	result = append(result, ranges...)
	triePathNode = triePathNode.Next()
}
fmt.Printf("The %d containing ranges are: %v", len(result), result)

Output:

The trie has 421 blocks
The 10 containing ranges are: [26.154.36.255 -> 192.250.200.250 37.99.32.76 -> 141.101.231.182 37.99.32.144 -> 185.148.82.122 37.99.32.144 -> 194.59.250.237 46.56.236.163 -> 186.169.171.32 46.56.236.163 -> 195.231.4.217 82.200.127.52 -> 103.192.63.244 62.109.3.240 -> 91.87.64.89 88.250.248.235 -> 188.18.253.203 88.250.248.235 -> 212.74.202.30]

Select CIDR Prefix Block Subnets Intersecting with Arbitrary Block

Given a list of CIDR blocks and addresses, we find which ones intersect with a given subnet. With CIDR blocks, intersection means either containment or equality. In other words, if two blocks intersect, then they are either the same, or one is contained in the other.

We start with some blocks and addresses:

ipv6BlockStrs := []string{
	"2001:4860:4860::8888",
	"2001:4860:4860::/64",
	"2001:4860:4860::/96",
	"2001:4860:4860:0:1::/96",
	"2001:4860:4860::8844",

	"2001:4801:7825:103:be76:4eff::/96",
	"2001:4801:7825:103:be76:4efe::/96",
	"2001:4801:7825:103:be76::/80",
	"2001:4801:7825:103:be75::/80",
	"2001:4801:7825:103:be77::/80",
	"2001:4801:7825:103:be76:4eff:fe10:2e49",

	"2001:4800:780e:510:a8cf:392e:ff04:8982",
	"2001:4800:7825:103:be76:4eff:fe10:2e49",
	"2001:4800:7825:103:be76:4eff:fe10:2e48",
	"2001:4800:7800::/40",
	"2001:4800:7825::/40",
	"2001:4800:7825:103::/64",
	"2001:4800:780e:510::/64",
	"2001:4800:780e:510:a8cf::/80",
	"2001:4800:780e:510:a8ff::/80",
	"2001:4800:7825:103:be76:4eff::/96",
	"2001:4800:7825:103:be76:4efe::/96",
	"2001:4800:7825:103:be76::/80",
	"2001:4800:7825:103:ce76::/80",

	"2620:fe::fe",
	"2620:fe::9",
	"2620:119:35::/64",
	"2620:119:35::35",
	"2620:119:35::37",
	"2620:119:53::53",
}
		
blocks := make([]*ipaddr.IPAddress, 0, len(ipv6BlockStrs))
for _, str := range ipv6BlockStrs {
	blocks = append(blocks, ipaddr.NewIPAddressString(str).GetAddress())
}

Our example block to check for intersection is 2001:4800:7825:103:be76:4000::/84

candidate := ipaddr.NewIPAddressString("2001:4800:7825:103:be76:4000::/84").GetAddress()	

One way is to simply iterate through the ranges:

containing := make([]*ipaddr.IPAddress, 0, len(ipv6BlockStrs))
contained := make([]*ipaddr.IPAddress, 0, len(ipv6BlockStrs))

for _, block := range blocks {
	if block.Contains(candidate) {
		containing = append(containing, block)
	} else if candidate.Contains(block) {
		contained = append(contained, block)
	}
}
fmt.Printf("contained by %v\ncontains %v\n", containing, contained)

Output:

contained by [2001:4800:7800::/40 2001:4800:7825:103::/64 2001:4800:7825:103:be76::/80]
contains [2001:4800:7825:103:be76:4eff:fe10:2e49 2001:4800:7825:103:be76:4eff:fe10:2e48 2001:4800:7825:103:be76:4eff::/96 2001:4800:7825:103:be76:4efe::/96]

Suppose you wanted to repeat this operation many times against the same set of blocks. Then comparing every block in the list, every time, is expensive, especially if the list is large or the check is repeated many times. In such cases a more efficient option is to use a trie. Create a trie with all the addresses, and use that to check for containment.

trie := ipaddr.AddressTrie{}

for _, block := range blocks {
	trie.Add(block.ToAddressBase())
}
containingBlocks := trie.ElementsContaining(candidate.ToAddressBase())
containedBlocks := trie.ElementsContainedBy(candidate.ToAddressBase())

// convert the linked list and the trie to slices, respectively
containing, contained = containing[:0], contained[:0]
for block := containingBlocks.ShortestPrefixMatch(); block != nil; block = block.Next() {
	containing = append(containing, block.GetKey().ToIP())
}
iter := containedBlocks.Iterator()
for block := iter.Next(); block != nil; block = iter.Next() {
	contained = append(contained, block.ToIP())
}
fmt.Printf("contained by %v\ncontains %v\n", containing, contained)

Output:

contained by [2001:4800:7800::/40 2001:4800:7825:103::/64 2001:4800:7825:103:be76::/80]
contains [2001:4800:7825:103:be76:4efe::/96 2001:4800:7825:103:be76:4eff::/96 2001:4800:7825:103:be76:4eff:fe10:2e48 2001:4800:7825:103:be76:4eff:fe10:2e49]

Select Address Ranges Intersecting with Arbitrary Range

Given a list of sequential address ranges, we find which ones intersect with a given range. This is similar to the previous example, although range intersection is not always containment, as is the case with CIDR blocks.

Much like the previous example for CIDR blocks, one way is to simply iterate through the ranges.

We reuse the array of 28 ranges in the variable ipRanges from the second-previous example. The range from 37.97.0.0 to 88.0.0.0 is this example's arbitrary range to check for intersection.

candidateRange := ipaddr.NewIPAddressString("37.97.0.0").GetAddress().
	SpanWithRange(ipaddr.NewIPAddressString("88.0.0.0").GetAddress())

intersectingRanges := IPAddressSeqRangeSet{make(map[ipaddr.IPAddressSeqRangeKey]struct{})}
for _, rng := range ipRanges {
	if rng.Intersect(candidateRange) != nil {
		intersectingRanges.Add(rng)
	}
}

fmt.Println("intersects: ", intersectingRanges.Elements())

Output:

intersects:  [26.154.36.255 -> 192.250.200.250 37.99.32.76 -> 62.192.232.40 37.99.32.76 -> 141.101.231.182 46.56.236.163 -> 195.231.4.217 26.154.36.255 -> 82.200.127.52 37.99.32.144 -> 185.148.82.122 37.99.32.144 -> 194.59.250.237 46.56.236.163 -> 186.169.171.32 62.109.3.240 -> 91.87.64.89 62.109.11.226 -> 62.192.232.40 82.200.127.52 -> 103.192.63.244]

Much like the previous example, when you need to repeat this operation many times against the same list of ranges, and that list of ranges may be large, then comparing with each and every range in the list is expensive. Once again, a more efficient option is to use a trie.

This time, in order to use a trie, we must first convert each range to its associated spanning CIDR blocks. We can recover the original range from each by using an associative trie that maps each trie element to its originating ranges.

We created such a trie in the second-previous example. It was populated from our example ranges using the trie's remap operation and stored in the variable ipv4Trie. We can reuse that same trie here.

To use the trie we must also split the candidate range into spanning blocks:

blocks := candidateRange.SpanWithPrefixBlocks()

For each such block we check for intersecting blocks in the trie. We use a set type IPAddressSeqRangeSet which will be defined in a further example.

intersectingRanges = IPAddressSeqRangeSet{make(map[ipaddr.IPAddressSeqRangeKey]struct{})}
for _, block := range blocks {
	containingPath := ipv4Trie.ElementsContaining(block.ToIPv4())
	containedBlocks := ipv4Trie.ElementsContainedBy(block.ToIPv4())

	// map the intersecting blocks back to their original ranges
	containingNode := containingPath.ShortestPrefixMatch()
	for ; containingNode != nil; containingNode = containingNode.Next() {
		ranges := containingNode.GetValue()
		for _, rng := range ranges {
			intersectingRanges.Add(rng)
		}
	}
	if containedBlocks != nil {
		nodeIterator := containedBlocks.ContainingFirstIterator(true)
		for nodeIterator.HasNext() {
			ranges := nodeIterator.Next().GetValue()
			for _, rng := range ranges {
				intersectingRanges.Add(rng)
			}
		}
	}
}
fmt.Println("intersects: ", intersectingRanges.Elements())

Output:

intersects:  [37.99.32.144 -> 185.148.82.122 46.56.236.163 -> 186.169.171.32 62.109.3.240 -> 91.87.64.89 62.109.11.226 -> 62.192.232.40 26.154.36.255 -> 192.250.200.250 37.99.32.76 -> 141.101.231.182 37.99.32.144 -> 194.59.250.237 46.56.236.163 -> 195.231.4.217 82.200.127.52 -> 103.192.63.244 26.154.36.255 -> 82.200.127.52 37.99.32.76 -> 62.192.232.40]

Because the IPAddressSeqRangeSet type is backed by a Go map, which is a hash map, the ordering of the above output, based on the map ordering, may change.

Select Address Closest to Arbitrary Address

We reuse the IPv4 addresses in the variable ipv4AddrStrs in a previous example. We will show different ways to find the address in the list closest to the 190.0.0.0 address.

ipAddresses := make([]*ipaddr.IPAddress, 0, len(ipv4AddrStrs))
for _, str := range ipv4AddrStrs {
	ipAddresses = append(ipAddresses,
		ipaddr.NewIPAddressString(str).GetAddress())
}

candidate := ipaddr.NewIPAddressString("190.0.0.0").GetAddress()

One way is to check the distance to each and every address, to find the closest.

fmt.Println(eachNear(ipAddresses, candidate))

func eachNear(addresses []*ipaddr.IPAddress, candidate *ipaddr.IPAddress) *ipaddr.IPAddress {
	currentClosest := addresses[0]
	distance := candidate.Enumerate(currentClosest)
	currentDistance := distance.Abs(distance)
	addresses = addresses[1:]
	for _, addr := range addresses {
		distance = candidate.Enumerate(addr)
		distance.Abs(distance)
		if distance.CmpAbs(currentDistance) < 0 {
			currentClosest = addr
			currentDistance = distance
		}
	}
	return currentClosest
}

Output:

188.18.253.203

If you were to repeat this operation many times with different candidates, you might want to do it more efficiently. The following options use binary search to avoid comparisons with each and every address in the list.

Here is a binary list search.

fmt.Println(listNear(ipAddresses, candidate))

func listNear(addresses []*ipaddr.IPAddress, candidate *ipaddr.IPAddress) *ipaddr.IPAddress {
	// don't alter original slice
	addresses = append(make([]*ipaddr.IPAddress, 0, len(addresses)), addresses...) 
	compFunc := func(a, b *ipaddr.IPAddress) int {
		return a.Compare(b)
	}
	slices.SortFunc(addresses, compFunc)
	index, found := slices.BinarySearchFunc(addresses, candidate, compFunc)
	if found {
		// exact match
		return addresses[index]
	}
	size := len(addresses)
	if index == 0 {
		// lowest address
		return addresses[0]
	} else if index == size {
		// highest address
		return addresses[size-1]
	}

	// We get the address closest from below, and the address closest from above
	lower, higher := addresses[index-1], addresses[index]

	// Find the closest of the two
	return closest(candidate, lower, higher)
}

func closest[E interface {
	ipaddr.AddressType
	Enumerate(ipaddr.AddressType) *big.Int
}](candidate, lower, higher E) E {
	// We use the Enumerate method to find which one is closest of the two candidates
	lowerDistance := lower.Enumerate(candidate)
	higherDistance := candidate.Enumerate(higher)
	comp := lowerDistance.CmpAbs(higherDistance)
	if comp <= 0 {
		return lower
	}
	return higher
}

Output:

188.18.253.203

Another way is to do a binary trie search.

trie := ipaddr.AddressTrie{}
for _, addr := range ipAddresses {
	trie.Add(addr.ToAddressBase())
}

fmt.Println(trieNear(trie, candidate.ToAddressBase()))

func trieNear(trie ipaddr.AddressTrie, candidate *ipaddr.Address) *ipaddr.Address {
	// We find the address closest from below, and the address closest from above
	lower, higher := trie.Lower(candidate), trie.Higher(candidate)
	if lower == nil {
		return higher
	} else if higher == nil {
		return lower
	}
	// Find the closest of the two
	return closest(candidate, lower, higher)
}

Output:

188.18.253.203

Look up Associated IP Address Data by Containing Subnet or Matching Address

Suppose we had a number of regional buildings, departments, and employees in an organization. The subnets are allocated according to these subdivisions, as shown.

var mappings = map[string]string{
	"region R1": "1.0.0.0/8",

	"building A": "1.1.0.0/16",
	"building B": "1.2.0.0/16",
	"building C": "1.3.0.0/16",

	"department A1": "1.1.1.0/24",
	"department A2": "1.1.2.0/24",
	"department A3": "1.1.3.0/24",

	"employee 1": "1.1.1.1", // department A1

	"employee 2": "1.1.2.1", // department A2
	"employee 3": "1.1.2.2",
	"employee 4": "1.1.2.3",

	"employee 5": "1.1.3.1", // department A3
	"employee 6": "1.1.3.2",
}

We start with a trie of the associated subnets.

trie := ipaddr.NewIPv4AddressAssociativeTrie()
for key, addr := range mappings {
	addToTrie(trie, key, addr)
}
fmt.Println("The trie is:")
fmt.Println(trie)

func addToTrie(trie *ipaddr.IPv4AddressAssociativeTrie, entry, addr string) {
	trie.Put(getAddr(addr), Info{entry})
}

func getAddr(addrStr string) *ipaddr.IPv4Address {
	return ipaddr.NewIPAddressString(addrStr).GetAddress().ToIPv4()
}

type Info struct {
	str string
}

func (i Info) String() string {
	return i.str
}

Output:

The trie is:

○ 0.0.0.0/0 (13)
└─● 1.0.0.0/8 = region R1 (13)
  └─○ 1.0.0.0/14 (12)
    ├─● 1.1.0.0/16 = building A (10)
    │ └─○ 1.1.0.0/22 (9)
    │   ├─● 1.1.1.0/24 = department A1 (2)
    │   │ └─● 1.1.1.1 = employee 1 (1)
    │   └─○ 1.1.2.0/23 (7)
    │     ├─● 1.1.2.0/24 = department A2 (4)
    │     │ └─○ 1.1.2.0/30 (3)
    │     │   ├─● 1.1.2.1 = employee 2 (1)
    │     │   └─○ 1.1.2.2/31 (2)
    │     │     ├─● 1.1.2.2 = employee 3 (1)
    │     │     └─● 1.1.2.3 = employee 4 (1)
    │     └─● 1.1.3.0/24 = department A3 (3)
    │       └─○ 1.1.3.0/30 (2)
    │         ├─● 1.1.3.1 = employee 5 (1)
    │         └─● 1.1.3.2 = employee 6 (1)
    └─○ 1.2.0.0/15 (2)
      ├─● 1.2.0.0/16 = building B (1)
      └─● 1.3.0.0/16 = building C (1)

Given an address in the network, whether an employee or from some other origin, we can use the trie to identify the details for that address.

printDetails(trie, "1.1.2.2") // known employee
fmt.Println()
printDetails(trie, "1.1.3.11") // unknown IP

func printDetails(trie *ipaddr.IPv4AddressAssociativeTrie, addrStr string) {
	addr := getAddr(addrStr)
	subnetNode := trie.LongestPrefixMatchNode(addr)
	info := subnetNode.GetValue().(Info)
	fmt.Printf("details for %v:\n%s", addr, info.str)
	subnetNode = subnetNode.GetParent()
	for subnetNode != nil {
		if subnetNode.IsAdded() {
			info = subnetNode.GetValue().(Info)
			fmt.Print(", " + info.str)
		}
		subnetNode = subnetNode.GetParent()
	}
	fmt.Println()
}

Output:

details for 1.1.2.2:
employee 3, department A2, building A, region R1

details for 1.1.3.11:
department A3, building A, region R1

Sort Addresses, Blocks and Ranges Collectively using Various Orderings

In addition to showing options for sorting, this example and the next also shows how the address framework can be useful.

Firstly, we create some addresses using address strings from previous examples:

ipv6Addrs := make([]*ipaddr.IPv6Address, 0, len(ipv6AddrStrs))
for _, str := range ipv6AddrStrs {
	ipv6Addrs = append(ipv6Addrs,
		ipaddr.NewIPAddressString(str).GetAddress().ToIPv6())
}
ipv4Addrs := make([]*ipaddr.IPv4Address, 0, len(ipv4AddrStrs))
for _, str := range ipv4AddrStrs {
	ipv4Addrs = append(ipv4Addrs,
		ipaddr.NewIPAddressString(str).GetAddress().ToIPv4())
}

We create some sequential ranges from those addresses:

ipv6Ranges := make([]*ipaddr.IPv6AddressSeqRange, 0, len(ipv6Addrs)-1)
for i := 1; i < len(ipv6Addrs); i += 2 {
	ipv6Ranges = append(ipv6Ranges, ipv6Addrs[i-1].SpanWithRange(ipv6Addrs[i]))
}
ipv4Ranges := make([]*ipaddr.IPv4AddressSeqRange, 0, len(ipv4Addrs)-1)
for i := 1; i < len(ipv4Addrs); i++ {
	ipv4Ranges = append(ipv4Ranges, ipv4Addrs[i-1].SpanWithRange(ipv4Addrs[i]))
}

We create some CIDR prefix blocks from a few of those ranges:

var ipv6Blocks []*ipaddr.IPv6Address
for _, rng := range ipv6Ranges[:(len(ipv6Ranges)+11)/10] {
	ipv6Blocks = append(ipv6Blocks, rng.SpanWithPrefixBlocks()...)
}
var ipv4Blocks []*ipaddr.IPv4Address
for _, rng := range ipv4Ranges[:(len(ipv6Ranges)+11)/10] {
	ipv4Blocks = append(ipv4Blocks, rng.SpanWithPrefixBlocks()...)
}
fmt.Printf("We have %d IPv4 addresses, %d IPv6 addresses, "+
	"%d IPv4 ranges, %d IPv6 ranges, "+
	"%d IPv4 blocks, %d IPv6 blocks.\n", len(ipv4Addrs), len(ipv6Addrs),
	len(ipv4Ranges), len(ipv6Ranges), len(ipv4Blocks), len(ipv6Blocks))

Output:

We have 29 IPv4 addresses, 16 IPv6 addresses, 28 IPv4 ranges, 8 IPv6 ranges, 25 IPv4 blocks, 6 IPv6 blocks.

We add them all to the same slice:

ipItems := make([]ipaddr.AddressItem, 0, len(ipv6Addrs)+len(ipv4Addrs)+
	len(ipv6Ranges)+len(ipv4Ranges)+len(ipv6Blocks)+len(ipv4Blocks))
for _, addr := range ipv6Addrs {
	ipItems = append(ipItems, addr)
}
for _, addr := range ipv4Addrs {
	ipItems = append(ipItems, addr)
}
for _, rng := range ipv6Ranges {
	ipItems = append(ipItems, rng)
}
for _, rng := range ipv4Ranges {
	ipItems = append(ipItems, rng)
}
for _, block := range ipv6Blocks {
	ipItems = append(ipItems, block)
}
for _, block := range ipv4Blocks {
	ipItems = append(ipItems, block)
}

We start with a comparison that goes by size first and values in descending order second, with addresses/blocks first and ranges second. We sort them all with the same comparison method, since the method can compare any address item with any other.

sort.Slice(ipItems, func(i, j int) bool {
	return ipItems[i].Compare(ipItems[j]) > 0
})
fmt.Println("The sorted IP addresses, ranges and blocks are:\n", ipItems)

Output:

The sorted IP addresses, ranges and blocks are:
 [2001:4860:4860::8860/123 2001:4860:4860::8850/124 2001:4860:4860::8880/125 2001:4860:4860::8848/125 2001:4860:4860::8844/126 2a0d:2a00:2::2 2a0d:2a00:1::2 2a00:5a60::ad2:ff 2a00:5a60::ad1:ff 2620:119:53::53 2620:119:35::35 2620:fe::fe 2620:fe::9 2620:74:1c::2:2 2620:74:1b::1:1 2606:4700:4700::1111 2606:4700:4700::1001 2001:4860:4860::8888/128 2001:4860:4860::8888 2001:4860:4860::8844 2001:4801:7825:103:be76:4eff:fe10:2e49 2001:4800:780e:510:a8cf:392e:ff04:8982 64.0.0.0/4 80.0.0.0/5 88.0.0.0/7 90.0.0.0/8 63.0.0.0/8 62.128.0.0/9 91.0.0.0/10 91.64.0.0/12 62.112.0.0/12 91.80.0.0/14 91.84.0.0/15 62.110.0.0/15 91.86.0.0/16 62.109.128.0/17 91.87.0.0/18 62.109.64.0/18 62.109.32.0/19 62.109.16.0/20 62.109.8.0/21 62.109.4.0/22 91.87.64.0/26 91.87.64.64/28 62.109.3.240/28 91.87.64.80/29 91.87.64.88/31 212.253.90.111 212.253.90.11 212.253.90.11 212.74.202.30 209.97.191.87 195.231.4.217 195.231.4.132 194.59.250.237 192.250.200.250 188.18.253.203 186.169.171.32 185.148.82.122 141.101.231.203 141.101.231.182 122.114.118.63 109.197.13.149 109.197.13.33 109.197.13.33 103.192.63.244 91.87.64.89 88.250.248.235 82.200.127.52 62.192.232.40 62.109.11.226 62.109.3.240 46.56.236.163 37.99.32.144 37.99.32.76 26.154.36.255 2001:4800:780e:510:a8cf:392e:ff04:8982 -> 2001:4801:7825:103:be76:4eff:fe10:2e49 2620:119:35::35 -> 2620:119:53::53 2620:74:1b::1:1 -> 2620:74:1c::2:2 2a0d:2a00:1::2 -> 2a0d:2a00:2::2 2a00:5a60::ad1:ff -> 2a00:5a60::ad2:ff 2606:4700:4700::1001 -> 2606:4700:4700::1111 2620:fe::9 -> 2620:fe::fe 2001:4860:4860::8844 -> 2001:4860:4860::8888 26.154.36.255 -> 192.250.200.250 37.99.32.144 -> 194.59.250.237 46.56.236.163 -> 195.231.4.217 37.99.32.144 -> 185.148.82.122 46.56.236.163 -> 186.169.171.32 88.250.248.235 -> 212.74.202.30 91.87.64.89 -> 209.97.191.87 103.192.63.244 -> 212.253.90.11 37.99.32.76 -> 141.101.231.182 109.197.13.33 -> 212.253.90.11 109.197.13.149 -> 212.253.90.11 88.250.248.235 -> 188.18.253.203 109.197.13.33 -> 195.231.4.217 141.101.231.203 -> 212.74.202.30 122.114.118.63 -> 188.18.253.203 122.114.118.63 -> 186.169.171.32 26.154.36.255 -> 82.200.127.52 141.101.231.182 -> 185.148.82.122 109.197.13.33 -> 141.101.231.203 62.109.3.240 -> 91.87.64.89 37.99.32.76 -> 62.192.232.40 82.200.127.52 -> 103.192.63.244 192.250.200.250 -> 212.253.90.111 194.59.250.237 -> 212.253.90.11 195.231.4.132 -> 212.253.90.111 195.231.4.132 -> 209.97.191.87 62.109.11.226 -> 62.192.232.40 109.197.13.33 -> 109.197.13.149]

Now we use a comparison that goes by low values in descending order, then size second, but starts with addresses/blocks first, ranges second. Once again we can sort them all with a single comparison method.

sort.Slice(ipItems, func(i, j int) bool {
	return ipaddr.LowValueComparator.Compare(ipItems[i], ipItems[j]) < 0
})
fmt.Println("The sorted IP addresses, ranges and blocks are:\n", ipItems)

Output:

The sorted IP addresses, ranges and blocks are:
 [26.154.36.255 -> 82.200.127.52 26.154.36.255 -> 192.250.200.250 37.99.32.76 -> 62.192.232.40 37.99.32.76 -> 141.101.231.182 37.99.32.144 -> 185.148.82.122 37.99.32.144 -> 194.59.250.237 46.56.236.163 -> 186.169.171.32 46.56.236.163 -> 195.231.4.217 62.109.3.240 -> 91.87.64.89 62.109.11.226 -> 62.192.232.40 82.200.127.52 -> 103.192.63.244 88.250.248.235 -> 188.18.253.203 88.250.248.235 -> 212.74.202.30 91.87.64.89 -> 209.97.191.87 103.192.63.244 -> 212.253.90.11 109.197.13.33 -> 109.197.13.149 109.197.13.33 -> 141.101.231.203 109.197.13.33 -> 195.231.4.217 109.197.13.33 -> 212.253.90.11 109.197.13.149 -> 212.253.90.11 122.114.118.63 -> 186.169.171.32 122.114.118.63 -> 188.18.253.203 141.101.231.182 -> 185.148.82.122 141.101.231.203 -> 212.74.202.30 192.250.200.250 -> 212.253.90.111 194.59.250.237 -> 212.253.90.11 195.231.4.132 -> 209.97.191.87 195.231.4.132 -> 212.253.90.111 2001:4800:780e:510:a8cf:392e:ff04:8982 -> 2001:4801:7825:103:be76:4eff:fe10:2e49 2001:4860:4860::8844 -> 2001:4860:4860::8888 2606:4700:4700::1001 -> 2606:4700:4700::1111 2620:74:1b::1:1 -> 2620:74:1c::2:2 2620:fe::9 -> 2620:fe::fe 2620:119:35::35 -> 2620:119:53::53 2a00:5a60::ad1:ff -> 2a00:5a60::ad2:ff 2a0d:2a00:1::2 -> 2a0d:2a00:2::2 26.154.36.255 37.99.32.76 37.99.32.144 46.56.236.163 62.109.3.240 62.109.3.240/28 62.109.4.0/22 62.109.8.0/21 62.109.11.226 62.109.16.0/20 62.109.32.0/19 62.109.64.0/18 62.109.128.0/17 62.110.0.0/15 62.112.0.0/12 62.128.0.0/9 62.192.232.40 63.0.0.0/8 64.0.0.0/4 80.0.0.0/5 82.200.127.52 88.0.0.0/7 88.250.248.235 90.0.0.0/8 91.0.0.0/10 91.64.0.0/12 91.80.0.0/14 91.84.0.0/15 91.86.0.0/16 91.87.0.0/18 91.87.64.0/26 91.87.64.64/28 91.87.64.80/29 91.87.64.88/31 91.87.64.89 103.192.63.244 109.197.13.33 109.197.13.33 109.197.13.149 122.114.118.63 141.101.231.182 141.101.231.203 185.148.82.122 186.169.171.32 188.18.253.203 192.250.200.250 194.59.250.237 195.231.4.132 195.231.4.217 209.97.191.87 212.74.202.30 212.253.90.11 212.253.90.11 212.253.90.111 2001:4800:780e:510:a8cf:392e:ff04:8982 2001:4801:7825:103:be76:4eff:fe10:2e49 2001:4860:4860::8844 2001:4860:4860::8844/126 2001:4860:4860::8848/125 2001:4860:4860::8850/124 2001:4860:4860::8860/123 2001:4860:4860::8880/125 2001:4860:4860::8888/128 2001:4860:4860::8888 2606:4700:4700::1001 2606:4700:4700::1111 2620:74:1b::1:1 2620:74:1c::2:2 2620:fe::9 2620:fe::fe 2620:119:35::35 2620:119:53::53 2a00:5a60::ad1:ff 2a00:5a60::ad2:ff 2a0d:2a00:1::2 2a0d:2a00:2::2]

Finally, we use a comparison that orders addresses and blocks by trie order, excluding the ranges which have been sorted to the front of the slice.

ipAddrs := ipItems[len(ipv6Ranges)+len(ipv4Ranges):]
	
sort.Slice(ipAddrs, func(i, j int) bool {
	firstAddr := ipAddrs[i].(ipaddr.IPAddressType).ToIP()
	secondAddr := ipAddrs[j].(ipaddr.IPAddressType).ToIP()
	if firstAddr.IsIPv4() {
		if secondAddr.IsIPv4() {
			return firstAddr.ToIPv4().TrieCompare(secondAddr.ToIPv4()) < 0
		}
		return true
	} else if secondAddr.IsIPv6() {
		return firstAddr.ToIPv6().TrieCompare(secondAddr.ToIPv6()) < 0
	}
	return false
})

fmt.Println("The sorted IP addresses and blocks are:\n", ipAddrs)

Output:

The sorted IP addresses and blocks are:
 [26.154.36.255 37.99.32.76 37.99.32.144 46.56.236.163 62.109.3.240 62.109.3.240/28 62.109.4.0/22 62.109.11.226 62.109.8.0/21 62.109.16.0/20 62.109.32.0/19 62.109.64.0/18 62.109.128.0/17 62.110.0.0/15 62.112.0.0/12 62.128.0.0/9 62.192.232.40 63.0.0.0/8 64.0.0.0/4 82.200.127.52 80.0.0.0/5 88.250.248.235 88.0.0.0/7 90.0.0.0/8 91.0.0.0/10 91.64.0.0/12 91.80.0.0/14 91.84.0.0/15 91.86.0.0/16 91.87.0.0/18 91.87.64.0/26 91.87.64.64/28 91.87.64.80/29 91.87.64.88/31 91.87.64.89 103.192.63.244 109.197.13.33 109.197.13.33 109.197.13.149 122.114.118.63 141.101.231.182 141.101.231.203 185.148.82.122 186.169.171.32 188.18.253.203 192.250.200.250 194.59.250.237 195.231.4.132 195.231.4.217 209.97.191.87 212.74.202.30 212.253.90.11 212.253.90.11 212.253.90.111 2001:4800:780e:510:a8cf:392e:ff04:8982 2001:4801:7825:103:be76:4eff:fe10:2e49 2001:4860:4860::8844 2001:4860:4860::8844/126 2001:4860:4860::8848/125 2001:4860:4860::8850/124 2001:4860:4860::8860/123 2001:4860:4860::8880/125 2001:4860:4860::8888 2001:4860:4860::8888/128 2606:4700:4700::1001 2606:4700:4700::1111 2620:74:1b::1:1 2620:74:1c::2:2 2620:fe::9 2620:fe::fe 2620:119:35::35 2620:119:53::53 2a00:5a60::ad1:ff 2a00:5a60::ad2:ff 2a0d:2a00:1::2 2a0d:2a00:2::2]

Use Addresses or Address Ranges as Keys for Go built-in maps

Previous examples showed that you can use tries for sets, and associative address tries for maps, with the elements or keys being addresses or CIDR prefix blocks, using a bitwise comparison between any two prefix blocks or individual addresses. The preceding example showed some additional comparisons available for address items in general.

However, such address item types are not comparable with Go language operators. In fact, Go's standard SDK types in the net package net.IP, net.IPAddr, net.IPMask, net.IPNet, net.TCPAddr, and net.UDPAddr, are also not comparable with Go language operators. This means none of these types can be used as keys for Go's built-in map type.

Instead, you can use the key types associated with the address and range types in this library.

Here we show how to use keys to create maps (and thus sets). The built-in maps are hash maps, so here we create a hash set of them using their associated keys. We create an IPv6Address set type as follows:

type IPv6Set struct {
	elements map[ipaddr.IPv6AddressKey]struct{}
}

func (set IPv6Set) Contains(addr *ipaddr.IPv6Address) (contains bool) {
	_, contains = set.elements[addr.ToKey()] // <-- converts address to its associated key
	return
}

func (set IPv6Set) Add(addr *ipaddr.IPv6Address) {
	set.elements[addr.ToKey()] = struct{}{}
}

func (set IPv6Set) Elements() (elements []ipaddr.IPv6Address) {
	elements = make([]ipaddr.IPv6Address, 0, len(set.elements))
	for key, _ := range set.elements {
		elements = append(elements, *key.ToAddress())
	}
	return
}

func (set IPv6Set) String() string {
	return fmt.Sprint(set.Elements())
}

Starting with the list of IPv6 addresses ipv6Addrs from the previous example, we create an example IPv6 address set.

ipv6Set := IPv6Set{make(map[ipaddr.IPv6AddressKey]struct{})}
for _, addr := range ipv6Addrs {
	ipv6Set.Add(addr)
}
fmt.Println("set is:", ipv6Set)

Output:

set is: [2001:4860:4860::8888 2620:fe::fe 2620:fe::9 2606:4700:4700::1111 2a0d:2a00:1::2 2a0d:2a00:2::2 2620:74:1b::1:1 2620:74:1c::2:2 2001:4801:7825:103:be76:4eff:fe10:2e49 2a00:5a60::ad2:ff 2001:4860:4860::8844 2620:119:35::35 2620:119:53::53 2606:4700:4700::1001 2001:4800:780e:510:a8cf:392e:ff04:8982 2a00:5a60::ad1:ff]

Trying it out:

ipv6Addr1 := ipaddr.NewIPAddressString(
	"ffff::aaaa").GetAddress().ToIPv6()
ipv6Addr2 := ipaddr.NewIPAddressString(
	"2001:4801:7825:103:be76:4eff:fe10:2e49").GetAddress().ToIPv6()
fmt.Println("set contains", ipv6Addr1, ipv6Set.Contains(ipv6Addr1))
fmt.Println("set contains", ipv6Addr2, ipv6Set.Contains(ipv6Addr2))

Output:

set contains ffff::aaaa false
set contains 2001:4801:7825:103:be76:4eff:fe10:2e49 true

Any address in the library can be converted to an address of the base type Address, and that type provides AddressKey map keys, so that is the type to use for a map using any address as keys. For a map using any IP address as keys, you can choose IPAddressKey. We create an IP-version agnostic set type as follows:

type IPSet struct {
	elements map[ipaddr.IPAddressKey]*ipaddr.IPAddress
}

func (set IPSet) Contains(addr *ipaddr.IPAddress) (contains bool) {
	_, contains = set.elements[addr.ToKey()]
	return
}

func (set IPSet) Add(addr *ipaddr.IPAddress) {
	set.elements[addr.ToKey()] = addr
}

func (set IPSet) Elements() (elements []ipaddr.IPAddress) {
	elements = make([]ipaddr.IPAddress, 0, len(set.elements))
	for _, value := range set.elements {
		elements = append(elements, *value)
	}
	return
}

func (set IPSet) String() string {
	return fmt.Sprint(set.Elements())
}

Here we convert all addresses and blocks from the ipAddrs slice in the previous example to the IPAddress type, in order to populate a larger version-agnostic set.

ipSet := IPSet{make(map[ipaddr.IPAddressKey]*ipaddr.IPAddress)}
for _, addr := range ipAddrs {
	ipSet.Add(addr.(ipaddr.IPAddressType).ToIP())
}
fmt.Println("set is:", ipSet)

Output:

set is: [62.109.3.240/28 62.109.8.0/21 62.109.128.0/17 109.197.13.33 122.114.118.63 185.148.82.122 2606:4700:4700::1111 2a0d:2a00:2::2 62.192.232.40 91.84.0.0/15 91.86.0.0/16 91.87.0.0/18 195.231.4.132 2620:fe::fe 2a0d:2a00:1::2 64.0.0.0/4 91.64.0.0/12 103.192.63.244 141.101.231.182 62.109.64.0/18 63.0.0.0/8 82.200.127.52 88.250.248.235 188.18.253.203 62.109.3.240 62.109.4.0/22 91.0.0.0/10 2001:4800:780e:510:a8cf:392e:ff04:8982 2001:4860:4860::8888 2620:119:35::35 62.110.0.0/15 90.0.0.0/8 2001:4860:4860::8850/124 80.0.0.0/5 192.250.200.250 195.231.4.217 2620:74:1c::2:2 2620:119:53::53 37.99.32.144 62.112.0.0/12 141.101.231.203 2001:4860:4860::8844/126 2001:4860:4860::8888/128 2a00:5a60::ad1:ff 2a00:5a60::ad2:ff 91.80.0.0/14 91.87.64.88/31 62.109.16.0/20 91.87.64.0/26 91.87.64.64/28 212.74.202.30 2620:74:1b::1:1 37.99.32.76 62.109.11.226 186.169.171.32 2001:4860:4860::8848/125 91.87.64.80/29 91.87.64.89 109.197.13.149 212.253.90.11 2001:4860:4860::8860/123 26.154.36.255 46.56.236.163 212.253.90.111 2620:fe::9 62.109.32.0/19 62.128.0.0/9 88.0.0.0/7 194.59.250.237 209.97.191.87 2001:4801:7825:103:be76:4eff:fe10:2e49 2001:4860:4860::8844 2001:4860:4860::8880/125 2606:4700:4700::1001] 

Trying it out with the same two IPv6 addresses as before:

fmt.Println("set contains", ipv6Addr1, ipSet.Contains(ipv6Addr1.ToIP()))
fmt.Println("set contains", ipv6Addr2, ipSet.Contains(ipv6Addr2.ToIP()))

Output:

set contains ffff::aaaa false
set contains 2001:4801:7825:103:be76:4eff:fe10:2e49 true

Similarly, we can create sets of IP address ranges using address range keys. Here we define a set that can hold both IPv4 and IPv6 address ranges.

type IPAddressSeqRangeSet struct {
	elements map[ipaddr.IPAddressSeqRangeKey]struct{}
}

func (set IPAddressSeqRangeSet) Add(rng *ipaddr.IPAddressSeqRange) {
	set.elements[rng.ToKey()] = struct{}{}
}

func (set IPAddressSeqRangeSet) Elements() (elements []ipaddr.IPAddressSeqRange) {
	elements = make([]ipaddr.IPAddressSeqRange, 0, len(set.elements))
	for key, _ := range set.elements {
		elements = append(elements, *key.ToSeqRange())
	}
	return
}

func (set IPAddressSeqRangeSet) Contains(rng *ipaddr.IPAddressSeqRange) bool {
	_, contains := set.elements[rng.ToKey()]
	return contains
}