Skip to content

Code Examples 3: Subnetting and Other Subnet Operations

Sean C Foley edited this page Nov 20, 2024 · 64 revisions

Iterate through Subnet

Use with caution, subnets can get quite large, an IPv6 /64 subnet has 18,446,744,073,709,551,616 addresses.

The basic structure is this:

subnet := ipaddr.NewIPAddressString("192.168.1.0/24").GetAddress().WithoutPrefixLen()
iterator := subnet.Iterator()
for next := iterator.Next(); next != nil; next = iterator.Next() {
	fmt.Println(next)
}

The call to WithoutPrefixLen is optional, it removes the prefix length from the subnet and all iterated addresses. The first 3 of the 256 lines of output:

192.168.1.0
192.168.1.1
192.168.1.2

Here we iterate through all, but print only beginning and end.

show("192.168.5.0/24")

func show(subnetStr string) {
	iterateAll(ipaddr.NewIPAddressString(subnetStr).GetAddress(), 3)
}

func iterateAll(subnet *ipaddr.IPAddress, edgeCount int) {
	count := subnet.GetCount()
	bigEdge := new(big.Int).SetInt64(int64(edgeCount))
	currCount := new(big.Int).Set(count)
	i := 0
	iterator := subnet.Iterator()
	for iterator.HasNext() {
		addr := iterator.Next()
		currCount.Sub(currCount, big.NewInt(1))
		if i < edgeCount { // initial values
			i++
			fmt.Printf("%d: %v\n", i, addr)
		} else if currCount.Cmp(bigEdge) < 0 { // end values
			var countDup big.Int
			fmt.Printf("%d: %v\n", countDup.Sub(count, currCount), addr)
		} else if i == edgeCount {
			fmt.Printf("skipping %v addresses\n",
				new(big.Int).Sub(count, big.NewInt(2*int64(edgeCount))))
			i++
		}
	}
}

Output:

1: 192.168.5.0/24
2: 192.168.5.1/24
3: 192.168.5.2/24
skipping 250 addresses
254: 192.168.5.253/24
255: 192.168.5.254/24
256: 192.168.5.255/24

You can use ToPrefixBlock to get the containing block, if not a zero-host subnet.

subnet := ipaddr.NewIPAddressString("1.186.0.1/15").GetAddress().
	ToPrefixBlock().WithoutPrefixLen()
iterator := subnet.Iterator()
listNum := 4 
for i := 0; i < listNum && iterator.HasNext(); i++ {
	fmt.Println(iterator.Next())
}
count := subnet.GetCount() // count of all addresses in the subnet
fmt.Println(count.Sub(count, big.NewInt(int64(listNum))), "more addresses...")

Output:

1.186.0.0
1.186.0.1
1.186.0.2
1.186.0.3
131068 more addresses...

The addresses are iterated in order starting from the zero host, so this variation omits the zero host and max host. In IPv4, those are the network and broadcast addresses, respectively.

subnet := ipaddr.NewIPAddressString("192.168.1.0/29").GetAddress()
if iterator := subnet.Iterator(); iterator.HasNext() {
	iterator.Next()
	for addr := iterator.Next(); iterator.HasNext(); addr = iterator.Next() {
		fmt.Println(addr)
	}
}

Output:

192.168.1.1/29
192.168.1.2/29
192.168.1.3/29
192.168.1.4/29
192.168.1.5/29
192.168.1.6/29

Iterate at Subnet Boundaries

List addresses at the beginning and end of a subnet's address range. Unlike the previous example, no caution is required with large subnets.

showEdges("2001:db8:abcd:0012::/64")

func showEdges(subnetStr string) {
	iterateEdges(ipaddr.NewIPAddressString(subnetStr).GetAddress(), 3)
}

func iterateEdges(subnet *ipaddr.IPAddress, edgeCount int) {
	if subnet.GetCount().Cmp(big.NewInt(2*(int64(edgeCount)+1))) < 0 {
		// the subnet is small, just print it
		i := 1
		iterator := subnet.Iterator()
		for ; iterator.HasNext(); i++ {
			fmt.Printf("%d: %v\n", i, iterator.Next())
		}
	} else {
		// print low boundary addresses
		lower := subnet.GetLower()
		for increment := 0; increment < edgeCount; increment++ {
			fmt.Printf("%d: %v\n",
				increment+1, lower.Increment(int64(increment)))
		}
		count := subnet.GetCount()
		fmt.Printf("skipping %v addresses\n",
			new(big.Int).Sub(count, big.NewInt(2*int64(edgeCount))))
		// print high boundary addresses
		upper := subnet.GetUpper()
		for decrement := 1 - edgeCount; decrement <= 0; decrement++ {
			dec64 := int64(decrement)
			dec := big.NewInt(dec64)
			fmt.Printf("%d: %v\n", dec.Add(count, dec), upper.Increment(dec64))
		}
	}
}

Output:

1: 2001:db8:abcd:12::/64
2: 2001:db8:abcd:12::1/64
3: 2001:db8:abcd:12::2/64
skipping 18446744073709551610 addresses
18446744073709551614: 2001:db8:abcd:12:ffff:ffff:ffff:fffd/64
18446744073709551615: 2001:db8:abcd:12:ffff:ffff:ffff:fffe/64
18446744073709551616: 2001:db8:abcd:12:ffff:ffff:ffff:ffff/64

From Start and End Address, Get Minimal List of CIDR Blocks Spanning the Range

toPrefixBlocks("2001:db8:abcd:0012:1::", "2001:db8:abcd:0012:3::")
toPrefixBlocks("1.1.1.111", "1.1.1.120")
	
func toPrefixBlocks(str1, str2 string) {
	string1, string2 := ipaddr.NewIPAddressString(str1), ipaddr.NewIPAddressString(str2)
	addr1, addr2 := string1.GetAddress(), string2.GetAddress()
	rng := addr1.SpanWithRange(addr2)
	blocks := rng.SpanWithPrefixBlocks()
	fmt.Printf("Starting with range %v,\nCIDR prefix blocks are %v\n\n", rng, blocks)
}

Output:

Starting with range 2001:db8:abcd:12:1:: -> 2001:db8:abcd:12:3::,
CIDR prefix blocks are [2001:db8:abcd:12:1::/80 2001:db8:abcd:12:2::/80 2001:db8:abcd:12:3::/128]

Starting with range 1.1.1.111 -> 1.1.1.120,
CIDR prefix blocks are [1.1.1.111/32 1.1.1.112/29 1.1.1.120/32]

From Start and End Address, Get Single CIDR Block Covering Both

In the example above, the prefix blocks span the range exactly. If you are looking for just a single prefix block including both addresses:

toPrefixBlock("2001:db8:abcd:0012:1::", "2001:db8:abcd:0012:3::")
toPrefixBlock("1.1.1.111", "1.1.1.120")

func toPrefixBlock(str1, str2 string) {
	string1, string2 := ipaddr.NewIPAddressString(str1), ipaddr.NewIPAddressString(str2)
	addr1, addr2 := string1.GetAddress(), string2.GetAddress()
	fmt.Printf("Starting with range %v,\nCIDR prefix blocks are %v\n\n",
		addr1.SpanWithRange(addr2), addr1.CoverWithPrefixBlockTo(addr2))
}

Output:

Starting with range 2001:db8:abcd:12:1:: -> 2001:db8:abcd:12:3::,
CIDR prefix block is 2001:db8:abcd:12::/78

Starting with range 1.1.1.111 -> 1.1.1.120,
CIDR prefix block is 1.1.1.96/27

Merge List of Addresses or Subnets into Minimal List of CIDR Blocks

print(merge("209.152.214.112/30", "209.152.214.116/31", "209.152.214.118/31"))
print(merge("209.152.214.112/30", "209.152.214.116/32", "209.152.214.118/31"))
print(merge("1:2:3:4:8000::/65", "1:2:3:4::/66", "1:2:3:4:4000::/66",
	"1:2:3:5:4000::/66", "1:2:3:5::/66", "1:2:3:5:8000::/65"))
	
func print(addresses []*ipaddr.IPAddress) {
	fmt.Printf("blocks are %v\n", addresses)
}

func merge(strs ...string) []*ipaddr.IPAddress {
	first := ipaddr.NewIPAddressString(strs[0]).GetAddress()
	remaining := make([]*ipaddr.IPAddress, 0, len(strs)-1)
	for _, str := range strs[1:] {
		remaining = append(remaining, ipaddr.NewIPAddressString(str).GetAddress())
	}
	return first.MergeToPrefixBlocks(remaining...)
}

Output:

blocks are [209.152.214.112/29]
blocks are [209.152.214.112/30 209.152.214.116/32 209.152.214.118/31]
blocks are [1:2:3:4::/63]

Break CIDR Prefix Block into Direct Component Blocks

Starting with a prefix block subnet, you can increase the prefix length of the subnet, while not changing the address values themselves. Then the set of addresses have multiple different prefixes. Then you can iterate through those prefix blocks.

adjustBlock("192.168.0.0/28", 2)
	
func adjustBlock(original string, bitShift int) {
	subnet := ipaddr.NewIPAddressString(original).GetAddress()
	newSubnets := subnet.SetPrefixLen(subnet.GetPrefixLen().Len() + bitShift)
	subnetSet := make([]*ipaddr.IPAddress, 0, newSubnets.GetPrefixCount().Uint64())
	iterator := newSubnets.PrefixBlockIterator()
	for next := iterator.Next(); next != nil; next = iterator.Next() {
		subnetSet = append(subnetSet, next)
	}
	fmt.Printf("original block: %v\nprefix length increased by %d: %v\nsubnets: %v\n\n",
		subnet, bitShift, newSubnets, subnetSet)
}

Output:

original block: 192.168.0.0/28
prefix length increased by 2: 192.168.0.0-12/30
subnets: [192.168.0.0/30 192.168.0.4/30 192.168.0.8/30 192.168.0.12/30]

Iteratively Break CIDR Prefix Block into Component Blocks

You can iterate through prefixes repeatedly, in different orders.

Iterate, ordered by block size (breadth-first traversal):

subnet := ipaddr.NewIPAddressString("192.168.0.0/29").GetAddress()
listByDecreasingSize(subnet, 1)

func listByDecreasingSize(subnet *ipaddr.IPAddress, bitShift int) {
	subnetPrefixLength := subnet.GetPrefixLen()
	fmt.Printf("%v size %v\n", subnet, subnet.GetCount())
	nextIndent := ""

	subPrefLen := subnetPrefixLength.Len()
	for subnetPrefixBits := subPrefLen; subnetPrefixBits+bitShift <= 
		subnet.GetBitCount(); {

		// adjust original subnet prefix length
		subnetPrefixBits += bitShift
		blocks := subnet.SetPrefixLen(subnetPrefixBits)

		// iterate through all prefixes
		blocksIterator := blocks.PrefixBlockIterator()
		for {
			next := blocksIterator.Next()
			node := fmt.Sprintf("%v size %d", next, next.GetCount())
			fmt.Print(nextIndent)
			if blocksIterator.HasNext() {
				fmt.Print(leftElbow)
				fmt.Println(node)
			} else {
				fmt.Print(rightElbow)
				fmt.Println(node)
				break
			}
		}
		nextIndent += belowElbows
	}
}

const (
	leftElbow     = "\u251C\u2500" // |-
	betweenElbows = "\u2502 "      // |
	rightElbow    = "\u2514\u2500" // '-
	belowElbows   = "  "
)

Output:

192.168.0.0/29 size 8
├─192.168.0.0/30 size 4
└─192.168.0.4/30 size 4
  ├─192.168.0.0/31 size 2
  ├─192.168.0.2/31 size 2
  ├─192.168.0.4/31 size 2
  └─192.168.0.6/31 size 2
    ├─192.168.0.0/32 size 1
    ├─192.168.0.1/32 size 1
    ├─192.168.0.2/32 size 1
    ├─192.168.0.3/32 size 1
    ├─192.168.0.4/32 size 1
    ├─192.168.0.5/32 size 1
    ├─192.168.0.6/32 size 1
    └─192.168.0.7/32 size 1

Iterate, ordered by block containment (depth-first traversal):

subnet := ipaddr.NewIPAddressString("192.168.0.0/29").GetAddress()
listByContainment(subnet, 1)

func listByContainment(subnet *ipaddr.IPAddress, bitShift int) {
	recurseBlocks(subnet, "", "", bitShift)
}

func recurseBlocks(subnet *ipaddr.IPAddress, indent, secondIndent string, bitShift int) {
	fmt.Printf("%s%v size %d\n", indent, subnet, subnet.GetCount())
	prefLength := subnet.GetPrefixLen()
	if prefLength.Len() >= subnet.GetBitCount() {
		return
	}
	subnetPrefixBits := prefLength.Len() + bitShift
	blocks := subnet.SetPrefixLen(subnetPrefixBits)

	// iterate through those blocks
	blocksIterator := blocks.PrefixBlockIterator()
	for {
		next := blocksIterator.Next()
		if blocksIterator.HasNext() {
			recurseBlocks(next, secondIndent+leftElbow,
				secondIndent+betweenElbows, bitShift)
		} else {
			recurseBlocks(next, secondIndent+rightElbow,
				secondIndent+belowElbows, bitShift)
			if subnetPrefixBits >= subnet.GetBitCount() {
				fmt.Println(secondIndent)
			}
			break
		}
	}
}

Output:

192.168.0.0/29 size 8
├─192.168.0.0/30 size 4
│ ├─192.168.0.0/31 size 2
│ │ ├─192.168.0.0/32 size 1
│ │ └─192.168.0.1/32 size 1
│ │ 
│ └─192.168.0.2/31 size 2
│   ├─192.168.0.2/32 size 1
│   └─192.168.0.3/32 size 1
│   
└─192.168.0.4/30 size 4
  ├─192.168.0.4/31 size 2
  │ ├─192.168.0.4/32 size 1
  │ └─192.168.0.5/32 size 1
  │ 
  └─192.168.0.6/31 size 2
    ├─192.168.0.6/32 size 1
    └─192.168.0.7/32 size 1

Derive New Subnet from Existing CIDR Subnet

The examples above iterate through prefixes, but if you want to derive a specific subnet rather than iterate through the list:

adjustSubnet("192.168.0.0/28", 2, "0.0.0.12")
adjustSubnet("2001:0db8:85a3::/64", 3, "0:0:0:0:e000::")

func adjustSubnet(original string, bitShift int, additionalPrefStr string) {
	subnet := ipaddr.NewIPAddressString(original).GetAddress()

	fmt.Printf("original block: %v with count %d\n",
		subnet, subnet.GetCount())

	additionalPrefix := ipaddr.NewIPAddressString(additionalPrefStr).
		GetAddress()
	newSubnet, _ := subnet.AdjustPrefixLenZeroed(bitShift)
	specifiedSubnet, _ := newSubnet.BitwiseOr(additionalPrefix)

	fmt.Printf("prefix length increased by %d: %v with count %d\n",
		bitShift, newSubnet, newSubnet.GetCount())
	fmt.Printf("after inserting additional prefix: %v with count %d\n",
		specifiedSubnet, specifiedSubnet.GetCount())

	originalSubnet := specifiedSubnet.AdjustPrefixLen(-bitShift).ToPrefixBlock()

	fmt.Printf("back to original %v with count %d\n\n",
		originalSubnet, originalSubnet.GetCount())
}

Output:

original block: 192.168.0.0/28 with count 16
prefix length increased by 2: 192.168.0.0/30 with count 4
after inserting additional prefix: 192.168.0.12/30 with count 4
back to original 192.168.0.0/28 with count 16

original block: 2001:db8:85a3::/64 with count 18446744073709551616
prefix length increased by 3: 2001:db8:85a3::/67 with count 2305843009213693952
after inserting additional prefix: 2001:db8:85a3:0:e000::/67 with count 2305843009213693952
back to original 2001:db8:85a3::/64 with count 18446744073709551616

Remove Address or Subnet from Subnet

subnet := "192.0.10.0/28"
exclude(subnet, "192.0.10.1")
exclude(subnet, "192.0.10.2/31")

func exclude(addrStr, sub string) {
	one, two := ipaddr.NewIPAddressString(addrStr).GetAddress(),
		ipaddr.NewIPAddressString(sub).GetAddress()
	result := one.Subtract(two)
	fmt.Printf("Removing %v from %v results in: %v\n", two, one, result)
	var blockList []*ipaddr.IPAddress
	for _, addr := range result {
		blockList = append(blockList, addr.SpanWithPrefixBlocks()...)
	}
	fmt.Printf("converted to prefix blocks: %v\n\n", blockList)
}

Output:

Removing 192.0.10.1 from 192.0.10.0/28 results in: [192.0.10.0 192.0.10.2-14/31]
converted to prefix blocks: [192.0.10.0/32 192.0.10.2/31 192.0.10.4/30 192.0.10.8/29]

Removing 192.0.10.2/31 from 192.0.10.0/28 results in: [192.0.10.0/31 192.0.10.4-12/30]
converted to prefix blocks: [192.0.10.0/31 192.0.10.4/30 192.0.10.8/29]

Variable Length Subnetting

In this example is code that can produce the subnetting shown in some online examples of subnetting, here and the calculator shown here.

For the quick and easy solution see the next example. This example shows a longer solution that can be customized.

We have a number of groups of hosts, each group to be allocated a subnet. We will represent each host group with the hostgroup type, and each resulting subnet allocation with the allocation type:

type hostGroup struct {
	name  string
	count uint
}

func (group hostGroup) String() string {
	return fmt.Sprint(group.name, " of size ", group.count)
}

type allocation struct {
	hostGroup
	subnet *ipaddr.IPAddress
}

func (alloc allocation) String() string {
	return fmt.Sprint(alloc.hostGroup, " from ", alloc.subnet)
}

We start with one or more CIDR blocks of adequate size for the subnetting, the availableBlocks list. Throughout, the availableBlocks list will maintain the list of available CIDR prefix block subnets, sorted by block size from smallest to largest. We want the smallest in the list first because it is more efficient to try to allocate from smaller blocks first, using the smallest block that can fit the number of required hosts.

After we are done, each allocation will have an assigned CIDR block subnet for its host group, and the availableBlocks list will have the unused CIDR blocks that can be used for additional host groups at a future time.

The host groups list is also sorted by size, but instead by largest groups first. We will allocate a subnet for each host group in turn, starting with the largest.

func allocateHosts(availableBlocks []string, toAllocate []hostGroup, 
	reservedPerBlock uint) {

	availableBlockAddrs := make([]*ipaddr.IPAddress, 0, len(availableBlocks))
	for _, availableBlock := range availableBlocks {
		block := ipaddr.NewIPAddressString(availableBlock).GetAddress()
		if block == nil || !block.IsSinglePrefixBlock() {
			fmt.Printf("%s is not a valid prefix block subnet\n",
				availableBlock)
			continue
		}
		availableBlockAddrs = append(availableBlockAddrs, block)
	}
	if allocs, unusedBlocks, err :=
		allocate(availableBlockAddrs, toAllocate, reservedPerBlock); err != nil {
		fmt.Println("unable to allocate:", err)
	} else {
		fmt.Println("\nallocated subnets:\n", allocs,
			"\nremaining unused blocks:\n", unusedBlocks)
	}
}

func allocate(
	availableBlocks []*ipaddr.IPAddress,
	toAllocate []hostGroup,
	reservedPerBlock uint) (
	allocated []allocation, unused []*ipaddr.IPAddress, err error) {

	// sort requests by size, largest first
	sort.Slice(toAllocate, func(i, j int) bool {
		return toAllocate[i].count > toAllocate[j].count
	})

	// sort available blocks by size, largest prefix length (smallest block) first
	sort.Slice(availableBlocks, func(i, j int) bool {
		return availableBlocks[i].GetPrefixLen().Compare(
			availableBlocks[j].GetPrefixLen()) > 0
	})

	allocated = make([]allocation, 0, len(toAllocate))
	unused = availableBlocks
	for _, group := range toAllocate { // allocate each group
		alloc := allocation{hostGroup: group}
		if alloc.subnet, unused, err =
			assign(unused, group, reservedPerBlock); err != nil {
			break
		}
		allocated = append(allocated, alloc)
	}
	return
}

We iterate through the available blocks until we find one large enough, a subnet whose address count matches or exceeds the required number. Each subnet may have a number of reserved addresses, which for IPv4 may include the network and broadcast addresses, so the subnet size must match or exceed the total of required hosts and reserved addresses. The selected block will be partitioned so that we do not waste address space.

func assign(
	availableBlocks []*ipaddr.IPAddress,
	group hostGroup,
	reserved uint) (
	*ipaddr.IPAddress, []*ipaddr.IPAddress, error) {

	requiredSize := new(big.Int).SetUint64(uint64(group.count + reserved))
	for i, candidate := range availableBlocks {
		if candidate.GetCount().Cmp(requiredSize) < 0 {
			continue // candidate not big enough
		}
		// use the candidate for the allocation
		allocated, unused := partition(candidate, group, reserved)

		// remove the partitioned block from the list
		availableBlocks = append(availableBlocks[:i], availableBlocks[i+1:]...)

		// insert the unused back into the available list
		if unused != nil {
			availableBlocks = insertUnused(availableBlocks, unused, i)
		}

		return allocated, availableBlocks, nil
	}
	return nil, availableBlocks, fmt.Errorf("no block big enough for %s", group)
}

To partition the selected block, we calculate the prefix bit-length for the required block size. Then we iterate through the equal-sized blocks of that prefix length, keeping the first for the host group, and returning the remaining blocks for insertion back into the list of available blocks.

func partition(
	block *ipaddr.IPAddress,
	group hostGroup,
	reserved uint) (
	allocated *ipaddr.IPAddress, unused *ipaddr.IPAddressSeqRange) {

	sizeRequired := group.count + reserved
	bitsRequired := math.Ceil(math.Log2(float64(sizeRequired)))
	newPrefixBitCount := block.GetBitCount() - ipaddr.BitCount(bitsRequired)
	newPrefixLen := ipaddr.PrefixBitCount(newPrefixBitCount)
	if !block.GetPrefixLen().Equal(&newPrefixLen) {
		iterator := block.SetPrefixLen(newPrefixBitCount).PrefixBlockIterator()
		allocated = iterator.Next()
		unused = iterator.Next().GetLower().SpanWithRange(block.GetUpper())
	} else {
		allocated = block
	}
	return
}

func insertUnused(
	availableBlocks,
	unused []*ipaddr.IPAddress,
	index int) []*ipaddr.IPAddress {

	unusedBlocks := unused.SpanWithPrefixBlocks()

	// sort unusedBlocks blocks by size, smallest block first, same as availableBlocks
	sort.Slice(unusedBlocks, func(i, j int) bool {
		return unusedBlocks[i].GetPrefixLen().Compare(
			unusedBlocks[j].GetPrefixLen()) > 0
	})

	for len(unusedBlocks) > 0 {
		lastIndex := len(unusedBlocks) - 1
		lastUnused := unusedBlocks[lastIndex]
		for ; index > 0; index-- {
			if availableBlocks[index-1].GetPrefixLen().Compare(
				lastUnused.GetPrefixLen()) >= 0 {
				break
			}
		}
		availableBlocks = append(availableBlocks[:index+1], availableBlocks[index:]...)
		availableBlocks[index] = lastUnused
		unusedBlocks = unusedBlocks[:lastIndex]
	}

	return availableBlocks
}

Now we are ready to run the code with a couple of examples. In the first example from Study-CCNA.com we have 5 host groups (of host group sizes 50, 30, 20, 2, 2, and 2) to assign from the single block 192.168.10.0/24.

var adjustment uint = 2 // reserve the network and broadcast address in each IPv4 subnet

allocateHosts(
	[]string{
		"192.168.10.0/24",
	},
	[]hostGroup{
		{"HQ LAN", 50},
		{"BRANCH 1", 30},
		{"BRANCH 2", 20},
		{"WAN 1", 2},
		{"WAN 2", 2},
		{"WAN 3", 2},
	},
	adjustment)

Output:

allocated subnets:
 [HQ LAN of size 50 from 192.168.10.0/26 BRANCH 1 of size 30 from 192.168.10.64/27 BRANCH 2 of size 20 from 192.168.10.96/27 WAN 1 of size 2 from 192.168.10.128/30 WAN 2 of size 2 from 192.168.10.132/30 WAN 3 of size 2 from 192.168.10.136/30]
remaining unused blocks:
 [192.168.10.140/30 192.168.10.144/30 192.168.10.148/30 192.168.10.152/30 192.168.10.156/30 192.168.10.160/30 192.168.10.164/30 192.168.10.168/30 192.168.10.172/30 192.168.10.176/30 192.168.10.180/30 192.168.10.184/30 192.168.10.188/30 192.168.10.192/26]

In the second example we have 4 host groups (of host group sizes 60, 12, 12, and 28) to assign from the same example block 192.168.10.0/24.

allocateHosts(
	[]string{
		"192.168.10.0/24",
	},
	[]hostGroup{
		{"Perth", 60},
		{"Sydney", 12},
		{"Singapore", 12},
		{"Kuala Lumpur", 28},
	},
	adjustment) // reserve the network and broadcast addresses

Output:

allocated subnets:
 [Perth of size 60 from 192.168.10.0/26 Kuala Lumpur of size 28 from 192.168.10.64/27 Sydney of size 12 from 192.168.10.96/28 Singapore of size 12 from 192.168.10.112/28]
remaining unused blocks:
 [192.168.10.128/26 192.168.10.192/26]

Also note that this code works equally well with IPv6, although with IPv6 the address space is much larger, and thus splitting up the blocks efficiently may result in a large number of unused blocks. Using variable length prefixes for efficiency is typically not as important with IPv6.

Automatic Subnetting

The previous example illustrates variable-length subnetting that can be customized as desired.

However, the algorithm outlined in that example is frequently suitable without any customization. If no further customization is required, you can simply use the PrefixBlockAllocator, which follows the same algorithm.

type allocatedBlock = ipaddr.AllocatedBlock[*ipaddr.IPAddress]

allocatedBlocks := alloc([]string{"192.168.10.0/24"}, 30, 20, 2, 2, 2)
for i, allocated := range allocatedBlocks {
	fmt.Println(i+1, allocated)
}

func alloc(blockStrs []string, blockSizes ...uint64) []allocatedBlock {
	blocks := make([]*ipaddr.IPAddress, len(blockStrs))
	for i, str := range blockStrs {
		blocks[i] = ipaddr.NewIPAddressString(str).GetAddress()
	}
	fmt.Println("Allocating subnets from", blocks, 
		"for blocks of size", blockSizes)
	var allocator ipaddr.PrefixBlockAllocator[*ipaddr.IPAddress]
	allocator.AddAvailable(blocks...)
	allocator.SetReserved(2) // 2 reserved per block
	return allocator.AllocateSizes(blockSizes...)
}

Output:

Allocating subnets from [192.168.10.0/24] for blocks of size [30 20 2 2 2]
1 192.168.10.0/27 for 30 hosts and 2 reserved addresses
2 192.168.10.32/27 for 20 hosts and 2 reserved addresses
3 192.168.10.64/30 for 2 hosts and 2 reserved addresses
4 192.168.10.68/30 for 2 hosts and 2 reserved addresses
5 192.168.10.72/30 for 2 hosts and 2 reserved addresses

Allocate Multi-Nested Equal-Size Subnet Blocks

In this example we assign address blocks to subnets in cloud availability zones.

Suppose we had a list of cloud accounts, and suppose we had the private blocks 10.0.0.0/8 and fd00::/64 available to use. For each account, we wish to allocate a subnet of about 2000 addresses for each availability zone in a list of cloud regions.

var accounts = []string{"Account A", "Account B"}

type Region struct {
	name              string
	availabilityZones []string
}

var regions = []Region{{
	name:              "eu-west-1",
	availabilityZones: []string{"a", "b", "c"},
}, {
	name:              "ap-southeast-1",
	availabilityZones: []string{"a", "b"},
}}

We want to organize the addresses so there is a subnet for each region, from which we allocate subnets for each account, from which we allocate subnets for each availability zone.

Each subnet would be associated with an instance of AssignedBlock.

type AssignedBlock struct {
	account,
	region,
	availabilityZone string
}

func (block AssignedBlock) String() (str string) {
	if str = block.availabilityZone; str == "" {
		if str = block.account; str == "" {
			str = block.region // a regional subnet
		} // else an account subnet
	} else {
		// an availability zone subnet
		str = block.account + " " + block.region + block.availabilityZone
	}
	return
}

To do this, we will map the allocated subnets to instances of AssignedBlock. To store the mappings, we will use IPv4 and IPv6 tries, which will maintain the subnet containment structure. We will use the dual trie type DualIPv4v6AssociativeTries, allowing the allocateSubnets function to be IP-version agnostic. At each level, given a block to be used for allocation, it adjusts the prefix length, and then dishes out blocks with a prefix block iterator.

The initial bits in the subnets' prefix designate the region, the next 6 bits designate the account, the next 3 bits designate the availability zone, and the final 11 host bits form the availability zone subnet block.

func allocate() {
	tries := &ipaddr.DualIPv4v6AssociativeTries[AssignedBlock]{}

	allocateSubnets("10.0.0.0/8", accounts, regions, tries)
	allocateSubnets("fd00::/64", accounts, regions, tries)

	fmt.Println(tries)
}

func allocateSubnets(addressBlockStr string, accounts []string, regions []Region,
	tries *ipaddr.DualIPv4v6AssociativeTries[AssignedBlock]) {

	block := ipaddr.NewIPAddressString(addressBlockStr).GetAddress()

	// add the base block to the tries
	tries.Add(block)

	// add the root ::0/0 or 0.0.0.0/0 to be a part of the hierarchy
	tries.Add(block.SetPrefixLen(0).ToPrefixBlock())

	// 11-bit blocks for each availability zone,
	// 3 preceding prefix bits for each availability zone,
	// 6 preceding prefix bits for each account, 
	// the remaining initial bits for each region
	addrBlockSize, avZoneBlockSize, accountBlockSize := 11, 3, 6
	totalBlockSize := addrBlockSize + avZoneBlockSize + accountBlockSize

	// set the prefix length for the regional subnets, then create the subnet iterator
	regionPrefixLen := block.GetBitCount() - totalBlockSize
	regionBlocks := block.SetPrefixLen(regionPrefixLen)
	regionIterator := regionBlocks.PrefixBlockIterator()

	for _, region := range regions {
		// each region gets a block
		regionBlock := regionIterator.Next()
		tries.Put(regionBlock, AssignedBlock{region: region.name})

		accountPrefixLen := regionPrefixLen + accountBlockSize
		accountBlocks := regionBlock.SetPrefixLen(accountPrefixLen)
		accountBlockIterator := accountBlocks.PrefixBlockIterator()

		for _, account := range accounts {
			// each account gets a block in each region
			accountBlock := accountBlockIterator.Next()
			tries.Put(accountBlock, AssignedBlock{account: account})

			avZonePrefixLen := accountPrefixLen + avZoneBlockSize
			avZoneBlocks := accountBlock.SetPrefixLen(avZonePrefixLen)
			avZoneBlockIterator := avZoneBlocks.PrefixBlockIterator()

			for _, avZone := range region.availabilityZones {
				// each availability zone gets a block in each account
				tries.Put(avZoneBlockIterator.Next(), AssignedBlock{
					region:           region.name,
					availabilityZone: avZone,
					account:          account,
				})
			}
		}
	}
}

The nested block allocations are clear when the trie is printed.

○ * (36)
├─● 0.0.0.0/0 =  (18)
│ └─● 10.0.0.0/8 =  (17)
│   └─○ 10.0.0.0/11 (16)
│     ├─● 10.0.0.0/12 = eu-west-1 (9)
│     │ └─○ 10.0.0.0/17 (8)
│     │   ├─● 10.0.0.0/18 = Account A (4)
│     │   │ └─○ 10.0.0.0/19 (3)
│     │   │   ├─○ 10.0.0.0/20 (2)
│     │   │   │ ├─● 10.0.0.0/21 = Account A eu-west-1a (1)
│     │   │   │ └─● 10.0.8.0/21 = Account A eu-west-1b (1)
│     │   │   └─● 10.0.16.0/21 = Account A eu-west-1c (1)
│     │   └─● 10.0.64.0/18 = Account B (4)
│     │     └─○ 10.0.64.0/19 (3)
│     │       ├─○ 10.0.64.0/20 (2)
│     │       │ ├─● 10.0.64.0/21 = Account B eu-west-1a (1)
│     │       │ └─● 10.0.72.0/21 = Account B eu-west-1b (1)
│     │       └─● 10.0.80.0/21 = Account B eu-west-1c (1)
│     └─● 10.16.0.0/12 = ap-southeast-1 (7)
│       └─○ 10.16.0.0/17 (6)
│         ├─● 10.16.0.0/18 = Account A (3)
│         │ └─○ 10.16.0.0/20 (2)
│         │   ├─● 10.16.0.0/21 = Account A ap-southeast-1a (1)
│         │   └─● 10.16.8.0/21 = Account A ap-southeast-1b (1)
│         └─● 10.16.64.0/18 = Account B (3)
│           └─○ 10.16.64.0/20 (2)
│             ├─● 10.16.64.0/21 = Account B ap-southeast-1a (1)
│             └─● 10.16.72.0/21 = Account B ap-southeast-1b (1)
└─● ::/0 =  (18)
  └─● fd00::/64 =  (17)
    └─○ fd00::/107 (16)
      ├─● fd00::/108 = eu-west-1 (9)
      │ └─○ fd00::/113 (8)
      │   ├─● fd00::/114 = Account A (4)
      │   │ └─○ fd00::/115 (3)
      │   │   ├─○ fd00::/116 (2)
      │   │   │ ├─● fd00::/117 = Account A eu-west-1a (1)
      │   │   │ └─● fd00::800/117 = Account A eu-west-1b (1)
      │   │   └─● fd00::1000/117 = Account A eu-west-1c (1)
      │   └─● fd00::4000/114 = Account B (4)
      │     └─○ fd00::4000/115 (3)
      │       ├─○ fd00::4000/116 (2)
      │       │ ├─● fd00::4000/117 = Account B eu-west-1a (1)
      │       │ └─● fd00::4800/117 = Account B eu-west-1b (1)
      │       └─● fd00::5000/117 = Account B eu-west-1c (1)
      └─● fd00::10:0/108 = ap-southeast-1 (7)
        └─○ fd00::10:0/113 (6)
          ├─● fd00::10:0/114 = Account A (3)
          │ └─○ fd00::10:0/116 (2)
          │   ├─● fd00::10:0/117 = Account A ap-southeast-1a (1)
          │   └─● fd00::10:800/117 = Account A ap-southeast-1b (1)
          └─● fd00::10:4000/114 = Account B (3)
            └─○ fd00::10:4000/116 (2)
              ├─● fd00::10:4000/117 = Account B ap-southeast-1a (1)
              └─● fd00::10:4800/117 = Account B ap-southeast-1b (1)

From Start and End Address, Get Minimal List of Spanning Subnets with Single-Valued or Full-Range Segments

Given an address range, we can first split into a minimal list of sequential blocks. From there, we can split them further so that no segment in the final list is neither single-valued nor full-range. As in many examples, this code works for both IPv4 and IPv6.

func example(lowerStr, upperStr string) {
	lower, upper := ipaddr.NewIPAddressString(lowerStr),
		ipaddr.NewIPAddressString(upperStr)
	rng := lower.GetAddress().SpanWithRange(
		upper.GetAddress())

	// first we split into sequential address blocks
	blocks := rng.SpanWithSequentialBlocks()
	fmt.Printf(
		"Starting with %v, the minimal list of sequential blocks is: %v\n",
		rng, blocks)

	// now we split further
	fmt.Println("Splitting each so all segments" +
		" are either single or full range:")
	allSplits := splitBlocks(blocks)
	fmt.Printf("Total block count: %d\n", len(allSplits))

	// we now go in the reverse direction
	reverse(rng, allSplits)
}

The code to split each block further finds the single segment that is neither single-valued nor full-range. We iterate over the segment values in that segment to produce the subnets for the final list. Earlier examples iterated over prefix blocks, which was iterating over the network bits, the initial part of a subnet. Here we iterate over the initial segments.

func splitBlocks(blocks []*ipaddr.IPAddress) []*ipaddr.IPAddress {
	// A sequential block can have at most one segment
	// that is not single nor full size,
	// so for each block we iterate over that one segment
	//ArrayList<IPAddress> allSplits = new ArrayList<IPAddress>();
	var allSplits []*ipaddr.IPAddress
	for _, block := range blocks {
		isSplit := false
		if block.IsMultiple() {
			for i := 0; i < block.GetSegmentCount(); i++ {
				segment := block.GetSegment(i)
				if segment.IsMultiple() {
					if segment.IsFullRange() {
						break
					}
					// segment is neither single nor full-range
					allSplits = split(block, i, allSplits)
					isSplit = true
					break
				}
			}
		}
		if !isSplit {
			label := "address"
			if block.IsMultiple() {
				label = "subnet"
			}
			fmt.Printf("\tNot splitting %s %v\n", label, block)
			allSplits = append(allSplits, block)
		}
	}
	return allSplits
}

func split(block *ipaddr.IPAddress, 
	index int,
	allSplits []*ipaddr.IPAddress) []*ipaddr.IPAddress {

	fmt.Println("\tSplitting", block)
	iterator := block.BlockIterator(index + 1)
	addrs := make([]*ipaddr.IPAddress, 0, block.GetMaxSegmentValue()+1)
	for next := iterator.Next(); next != nil; next = iterator.Next() {
		addrs = append(addrs, next)
	}
	isLastSegment := index == block.GetSegmentCount()-1
	label := "subnets"
	if isLastSegment {
		label = "addresses"
	}
	fmt.Printf("\t\t%d %s: %v\n", len(addrs), label, addrs)
	allSplits = append(allSplits, addrs...)
	return allSplits
}

This additional code shows how to go in the reverse direction, first merging to a minimal list of blocks, then merging to a minimal list of ranges. Alternatively, the code could be shortened by switching the blocks to ranges right away, following with just a single merge of ranges.

func reverse(rng *ipaddr.IPAddressSeqRange, allSplits []*ipaddr.IPAddress) {
	// merge to a minimal list of blocks
	merged := rng.GetLower().MergeToSequentialBlocks(allSplits...)
	fmt.Printf("Merged back to minimal sequential blocks again:\n%v\n", merged)

	// switch to ranges so we can merge to a minimal list of ranges
	ranges := make([]*ipaddr.IPAddressSeqRange, 0, len(merged))
	for _, addr := range merged {
		ranges = append(ranges, addr.ToSequentialRange())
	}
	fmt.Printf("Merged back to range again:\n%v\n\n", ranges[0].Join(ranges...))
}

We show the output for a couple of sample ranges, the first a CIDR prefix block:

example("41.216.124.0", "41.216.127.255")

Output:

Starting with 41.216.124.0 -> 41.216.127.255, the minimal list of sequential blocks is: [41.216.124-127.*]
Splitting each so all segments are either single or full range:
        Splitting 41.216.124-127.*
                4 subnets: [41.216.124.* 41.216.125.* 41.216.126.* 41.216.127.*]
Total block count: 4
Merged back to minimal sequential blocks again:
[41.216.124-127.*]
Merged back to range again:
[41.216.124.0 -> 41.216.127.255]

The second example is not a block, resulting in a much larger list:

example("128.1.0.1", "191.255.255.255")

Output:

Starting with 128.1.0.1 -> 191.255.255.255, the minimal list of sequential blocks is: [128.1.0.1-255 128.1.1-255.* 128.2-255.*.* 129-191.*.*.*]
Splitting each so all segments are either single or full range:
        Splitting 128.1.0.1-255
                255 addresses: [128.1.0.1 128.1.0.2 128.1.0.3 128.1.0.4 128.1.0.5 128.1.0.6 128.1.0.7 128.1.0.8 128.1.0.9 128.1.0.10 128.1.0.11 128.1.0.12 128.1.0.13 128.1.0.14 128.1.0.15 128.1.0.16 128.1.0.17 128.1.0.18 128.1.0.19 128.1.0.20 128.1.0.21 128.1.0.22 128.1.0.23 128.1.0.24 128.1.0.25 128.1.0.26 128.1.0.27 128.1.0.28 128.1.0.29 128.1.0.30 128.1.0.31 128.1.0.32 128.1.0.33 128.1.0.34 128.1.0.35 128.1.0.36 128.1.0.37 128.1.0.38 128.1.0.39 128.1.0.40 128.1.0.41 128.1.0.42 128.1.0.43 128.1.0.44 128.1.0.45 128.1.0.46 128.1.0.47 128.1.0.48 128.1.0.49 128.1.0.50 128.1.0.51 128.1.0.52 128.1.0.53 128.1.0.54 128.1.0.55 128.1.0.56 128.1.0.57 128.1.0.58 128.1.0.59 128.1.0.60 128.1.0.61 128.1.0.62 128.1.0.63 128.1.0.64 128.1.0.65 128.1.0.66 128.1.0.67 128.1.0.68 128.1.0.69 128.1.0.70 128.1.0.71 128.1.0.72 128.1.0.73 128.1.0.74 128.1.0.75 128.1.0.76 128.1.0.77 128.1.0.78 128.1.0.79 128.1.0.80 128.1.0.81 128.1.0.82 128.1.0.83 128.1.0.84 128.1.0.85 128.1.0.86 128.1.0.87 128.1.0.88 128.1.0.89 128.1.0.90 128.1.0.91 128.1.0.92 128.1.0.93 128.1.0.94 128.1.0.95 128.1.0.96 128.1.0.97 128.1.0.98 128.1.0.99 128.1.0.100 128.1.0.101 128.1.0.102 128.1.0.103 128.1.0.104 128.1.0.105 128.1.0.106 128.1.0.107 128.1.0.108 128.1.0.109 128.1.0.110 128.1.0.111 128.1.0.112 128.1.0.113 128.1.0.114 128.1.0.115 128.1.0.116 128.1.0.117 128.1.0.118 128.1.0.119 128.1.0.120 128.1.0.121 128.1.0.122 128.1.0.123 128.1.0.124 128.1.0.125 128.1.0.126 128.1.0.127 128.1.0.128 128.1.0.129 128.1.0.130 128.1.0.131 128.1.0.132 128.1.0.133 128.1.0.134 128.1.0.135 128.1.0.136 128.1.0.137 128.1.0.138 128.1.0.139 128.1.0.140 128.1.0.141 128.1.0.142 128.1.0.143 128.1.0.144 128.1.0.145 128.1.0.146 128.1.0.147 128.1.0.148 128.1.0.149 128.1.0.150 128.1.0.151 128.1.0.152 128.1.0.153 128.1.0.154 128.1.0.155 128.1.0.156 128.1.0.157 128.1.0.158 128.1.0.159 128.1.0.160 128.1.0.161 128.1.0.162 128.1.0.163 128.1.0.164 128.1.0.165 128.1.0.166 128.1.0.167 128.1.0.168 128.1.0.169 128.1.0.170 128.1.0.171 128.1.0.172 128.1.0.173 128.1.0.174 128.1.0.175 128.1.0.176 128.1.0.177 128.1.0.178 128.1.0.179 128.1.0.180 128.1.0.181 128.1.0.182 128.1.0.183 128.1.0.184 128.1.0.185 128.1.0.186 128.1.0.187 128.1.0.188 128.1.0.189 128.1.0.190 128.1.0.191 128.1.0.192 128.1.0.193 128.1.0.194 128.1.0.195 128.1.0.196 128.1.0.197 128.1.0.198 128.1.0.199 128.1.0.200 128.1.0.201 128.1.0.202 128.1.0.203 128.1.0.204 128.1.0.205 128.1.0.206 128.1.0.207 128.1.0.208 128.1.0.209 128.1.0.210 128.1.0.211 128.1.0.212 128.1.0.213 128.1.0.214 128.1.0.215 128.1.0.216 128.1.0.217 128.1.0.218 128.1.0.219 128.1.0.220 128.1.0.221 128.1.0.222 128.1.0.223 128.1.0.224 128.1.0.225 128.1.0.226 128.1.0.227 128.1.0.228 128.1.0.229 128.1.0.230 128.1.0.231 128.1.0.232 128.1.0.233 128.1.0.234 128.1.0.235 128.1.0.236 128.1.0.237 128.1.0.238 128.1.0.239 128.1.0.240 128.1.0.241 128.1.0.242 128.1.0.243 128.1.0.244 128.1.0.245 128.1.0.246 128.1.0.247 128.1.0.248 128.1.0.249 128.1.0.250 128.1.0.251 128.1.0.252 128.1.0.253 128.1.0.254 128.1.0.255]
        Splitting 128.1.1-255.*
                255 subnets: [128.1.1.* 128.1.2.* 128.1.3.* 128.1.4.* 128.1.5.* 128.1.6.* 128.1.7.* 128.1.8.* 128.1.9.* 128.1.10.* 128.1.11.* 128.1.12.* 128.1.13.* 128.1.14.* 128.1.15.* 128.1.16.* 128.1.17.* 128.1.18.* 128.1.19.* 128.1.20.* 128.1.21.* 128.1.22.* 128.1.23.* 128.1.24.* 128.1.25.* 128.1.26.* 128.1.27.* 128.1.28.* 128.1.29.* 128.1.30.* 128.1.31.* 128.1.32.* 128.1.33.* 128.1.34.* 128.1.35.* 128.1.36.* 128.1.37.* 128.1.38.* 128.1.39.* 128.1.40.* 128.1.41.* 128.1.42.* 128.1.43.* 128.1.44.* 128.1.45.* 128.1.46.* 128.1.47.* 128.1.48.* 128.1.49.* 128.1.50.* 128.1.51.* 128.1.52.* 128.1.53.* 128.1.54.* 128.1.55.* 128.1.56.* 128.1.57.* 128.1.58.* 128.1.59.* 128.1.60.* 128.1.61.* 128.1.62.* 128.1.63.* 128.1.64.* 128.1.65.* 128.1.66.* 128.1.67.* 128.1.68.* 128.1.69.* 128.1.70.* 128.1.71.* 128.1.72.* 128.1.73.* 128.1.74.* 128.1.75.* 128.1.76.* 128.1.77.* 128.1.78.* 128.1.79.* 128.1.80.* 128.1.81.* 128.1.82.* 128.1.83.* 128.1.84.* 128.1.85.* 128.1.86.* 128.1.87.* 128.1.88.* 128.1.89.* 128.1.90.* 128.1.91.* 128.1.92.* 128.1.93.* 128.1.94.* 128.1.95.* 128.1.96.* 128.1.97.* 128.1.98.* 128.1.99.* 128.1.100.* 128.1.101.* 128.1.102.* 128.1.103.* 128.1.104.* 128.1.105.* 128.1.106.* 128.1.107.* 128.1.108.* 128.1.109.* 128.1.110.* 128.1.111.* 128.1.112.* 128.1.113.* 128.1.114.* 128.1.115.* 128.1.116.* 128.1.117.* 128.1.118.* 128.1.119.* 128.1.120.* 128.1.121.* 128.1.122.* 128.1.123.* 128.1.124.* 128.1.125.* 128.1.126.* 128.1.127.* 128.1.128.* 128.1.129.* 128.1.130.* 128.1.131.* 128.1.132.* 128.1.133.* 128.1.134.* 128.1.135.* 128.1.136.* 128.1.137.* 128.1.138.* 128.1.139.* 128.1.140.* 128.1.141.* 128.1.142.* 128.1.143.* 128.1.144.* 128.1.145.* 128.1.146.* 128.1.147.* 128.1.148.* 128.1.149.* 128.1.150.* 128.1.151.* 128.1.152.* 128.1.153.* 128.1.154.* 128.1.155.* 128.1.156.* 128.1.157.* 128.1.158.* 128.1.159.* 128.1.160.* 128.1.161.* 128.1.162.* 128.1.163.* 128.1.164.* 128.1.165.* 128.1.166.* 128.1.167.* 128.1.168.* 128.1.169.* 128.1.170.* 128.1.171.* 128.1.172.* 128.1.173.* 128.1.174.* 128.1.175.* 128.1.176.* 128.1.177.* 128.1.178.* 128.1.179.* 128.1.180.* 128.1.181.* 128.1.182.* 128.1.183.* 128.1.184.* 128.1.185.* 128.1.186.* 128.1.187.* 128.1.188.* 128.1.189.* 128.1.190.* 128.1.191.* 128.1.192.* 128.1.193.* 128.1.194.* 128.1.195.* 128.1.196.* 128.1.197.* 128.1.198.* 128.1.199.* 128.1.200.* 128.1.201.* 128.1.202.* 128.1.203.* 128.1.204.* 128.1.205.* 128.1.206.* 128.1.207.* 128.1.208.* 128.1.209.* 128.1.210.* 128.1.211.* 128.1.212.* 128.1.213.* 128.1.214.* 128.1.215.* 128.1.216.* 128.1.217.* 128.1.218.* 128.1.219.* 128.1.220.* 128.1.221.* 128.1.222.* 128.1.223.* 128.1.224.* 128.1.225.* 128.1.226.* 128.1.227.* 128.1.228.* 128.1.229.* 128.1.230.* 128.1.231.* 128.1.232.* 128.1.233.* 128.1.234.* 128.1.235.* 128.1.236.* 128.1.237.* 128.1.238.* 128.1.239.* 128.1.240.* 128.1.241.* 128.1.242.* 128.1.243.* 128.1.244.* 128.1.245.* 128.1.246.* 128.1.247.* 128.1.248.* 128.1.249.* 128.1.250.* 128.1.251.* 128.1.252.* 128.1.253.* 128.1.254.* 128.1.255.*]
        Splitting 128.2-255.*.*
                254 subnets: [128.2.*.* 128.3.*.* 128.4.*.* 128.5.*.* 128.6.*.* 128.7.*.* 128.8.*.* 128.9.*.* 128.10.*.* 128.11.*.* 128.12.*.* 128.13.*.* 128.14.*.* 128.15.*.* 128.16.*.* 128.17.*.* 128.18.*.* 128.19.*.* 128.20.*.* 128.21.*.* 128.22.*.* 128.23.*.* 128.24.*.* 128.25.*.* 128.26.*.* 128.27.*.* 128.28.*.* 128.29.*.* 128.30.*.* 128.31.*.* 128.32.*.* 128.33.*.* 128.34.*.* 128.35.*.* 128.36.*.* 128.37.*.* 128.38.*.* 128.39.*.* 128.40.*.* 128.41.*.* 128.42.*.* 128.43.*.* 128.44.*.* 128.45.*.* 128.46.*.* 128.47.*.* 128.48.*.* 128.49.*.* 128.50.*.* 128.51.*.* 128.52.*.* 128.53.*.* 128.54.*.* 128.55.*.* 128.56.*.* 128.57.*.* 128.58.*.* 128.59.*.* 128.60.*.* 128.61.*.* 128.62.*.* 128.63.*.* 128.64.*.* 128.65.*.* 128.66.*.* 128.67.*.* 128.68.*.* 128.69.*.* 128.70.*.* 128.71.*.* 128.72.*.* 128.73.*.* 128.74.*.* 128.75.*.* 128.76.*.* 128.77.*.* 128.78.*.* 128.79.*.* 128.80.*.* 128.81.*.* 128.82.*.* 128.83.*.* 128.84.*.* 128.85.*.* 128.86.*.* 128.87.*.* 128.88.*.* 128.89.*.* 128.90.*.* 128.91.*.* 128.92.*.* 128.93.*.* 128.94.*.* 128.95.*.* 128.96.*.* 128.97.*.* 128.98.*.* 128.99.*.* 128.100.*.* 128.101.*.* 128.102.*.* 128.103.*.* 128.104.*.* 128.105.*.* 128.106.*.* 128.107.*.* 128.108.*.* 128.109.*.* 128.110.*.* 128.111.*.* 128.112.*.* 128.113.*.* 128.114.*.* 128.115.*.* 128.116.*.* 128.117.*.* 128.118.*.* 128.119.*.* 128.120.*.* 128.121.*.* 128.122.*.* 128.123.*.* 128.124.*.* 128.125.*.* 128.126.*.* 128.127.*.* 128.128.*.* 128.129.*.* 128.130.*.* 128.131.*.* 128.132.*.* 128.133.*.* 128.134.*.* 128.135.*.* 128.136.*.* 128.137.*.* 128.138.*.* 128.139.*.* 128.140.*.* 128.141.*.* 128.142.*.* 128.143.*.* 128.144.*.* 128.145.*.* 128.146.*.* 128.147.*.* 128.148.*.* 128.149.*.* 128.150.*.* 128.151.*.* 128.152.*.* 128.153.*.* 128.154.*.* 128.155.*.* 128.156.*.* 128.157.*.* 128.158.*.* 128.159.*.* 128.160.*.* 128.161.*.* 128.162.*.* 128.163.*.* 128.164.*.* 128.165.*.* 128.166.*.* 128.167.*.* 128.168.*.* 128.169.*.* 128.170.*.* 128.171.*.* 128.172.*.* 128.173.*.* 128.174.*.* 128.175.*.* 128.176.*.* 128.177.*.* 128.178.*.* 128.179.*.* 128.180.*.* 128.181.*.* 128.182.*.* 128.183.*.* 128.184.*.* 128.185.*.* 128.186.*.* 128.187.*.* 128.188.*.* 128.189.*.* 128.190.*.* 128.191.*.* 128.192.*.* 128.193.*.* 128.194.*.* 128.195.*.* 128.196.*.* 128.197.*.* 128.198.*.* 128.199.*.* 128.200.*.* 128.201.*.* 128.202.*.* 128.203.*.* 128.204.*.* 128.205.*.* 128.206.*.* 128.207.*.* 128.208.*.* 128.209.*.* 128.210.*.* 128.211.*.* 128.212.*.* 128.213.*.* 128.214.*.* 128.215.*.* 128.216.*.* 128.217.*.* 128.218.*.* 128.219.*.* 128.220.*.* 128.221.*.* 128.222.*.* 128.223.*.* 128.224.*.* 128.225.*.* 128.226.*.* 128.227.*.* 128.228.*.* 128.229.*.* 128.230.*.* 128.231.*.* 128.232.*.* 128.233.*.* 128.234.*.* 128.235.*.* 128.236.*.* 128.237.*.* 128.238.*.* 128.239.*.* 128.240.*.* 128.241.*.* 128.242.*.* 128.243.*.* 128.244.*.* 128.245.*.* 128.246.*.* 128.247.*.* 128.248.*.* 128.249.*.* 128.250.*.* 128.251.*.* 128.252.*.* 128.253.*.* 128.254.*.* 128.255.*.*]
        Splitting 129-191.*.*.*
                63 subnets: [129.*.*.* 130.*.*.* 131.*.*.* 132.*.*.* 133.*.*.* 134.*.*.* 135.*.*.* 136.*.*.* 137.*.*.* 138.*.*.* 139.*.*.* 140.*.*.* 141.*.*.* 142.*.*.* 143.*.*.* 144.*.*.* 145.*.*.* 146.*.*.* 147.*.*.* 148.*.*.* 149.*.*.* 150.*.*.* 151.*.*.* 152.*.*.* 153.*.*.* 154.*.*.* 155.*.*.* 156.*.*.* 157.*.*.* 158.*.*.* 159.*.*.* 160.*.*.* 161.*.*.* 162.*.*.* 163.*.*.* 164.*.*.* 165.*.*.* 166.*.*.* 167.*.*.* 168.*.*.* 169.*.*.* 170.*.*.* 171.*.*.* 172.*.*.* 173.*.*.* 174.*.*.* 175.*.*.* 176.*.*.* 177.*.*.* 178.*.*.* 179.*.*.* 180.*.*.* 181.*.*.* 182.*.*.* 183.*.*.* 184.*.*.* 185.*.*.* 186.*.*.* 187.*.*.* 188.*.*.* 189.*.*.* 190.*.*.* 191.*.*.*]
Total block count: 827
Merged back to minimal sequential blocks again:
[128.1.0.1-255 128.1.1-255.* 128.2-255.*.* 129-191.*.*.*]
Merged back to range again:
[128.1.0.1 -> 191.255.255.255]