The IPAddress Go library can do this a couple of different ways with code that is polymorphic, working with both IPv4 and IPv6 addresses. Repository here. Disclaimer: I am the project manager.
I'll show both ways to do it, using sample IPv4 addresses. Either way is not much code.
Here's 28 random sample IPv4 ranges and a single address to check what ranges it lies within:
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"},
}
// candidate to check
candidate := "88.255.1.1"
One way is to use sequential ranges, iterating through each. This is O(n).
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(candidate).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
Now for the other way.
The following algorithm is a constant time operation O(1) for each search candidate, no matter how many ranges are used. 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 not changing much.
Convert each range to spanning CIDR prefix blocks that can be added to an address trie.
Map each trie entry to the original range.
Look up the containing blocks in the trie. This is a constant time operation. It requires at most 32 comparisons no matter how many ranges are used, 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 required with the previous code.
I'll also note that this code is polymorphic, it also works with IPv6 addresses.
The ipRanges slice and the addrToCheck address carry over from the previous example.
trie := ipaddr.AssociativeAddressTrie{}
for _, rng := range ipRanges {
blocks := rng.SpanWithPrefixBlocks()
for _, block := range blocks {
trie.Remap(block.ToAddressBase(),
func(existing ipaddr.NodeValue) ipaddr.NodeValue {
// make the node point to the original range(s)
if existing == nil {
return []*ipaddr.IPAddressSeqRange{rng}
} else {
return append(existing.([]*ipaddr.IPAddressSeqRange), rng)
}
})
}
}
fmt.Printf("The trie has %d blocks\n", trie.Size())
//print the trie with: fmt.Println(trie)
trieNode := trie.ElementsContaining(addrToCheck.ToAddressBase())
// ElementsContaining returns a linked list in trie form of containing blocks.
// The mapped values of each in the list are the containing ranges.
var result []*ipaddr.IPAddressSeqRange
for trieNode != nil {
ranges := trieNode.GetValue().([]*ipaddr.IPAddressSeqRange)
result = append(result, ranges...)
if trieNode.GetUpperSubNode() != nil {
trieNode = trieNode.GetUpperSubNode()
} else {
trieNode = trieNode.GetLowerSubNode()
}
}
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]