Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Guust Nieuwenhuis
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Guust Nieuwenhuis

Breaking An IPv4 Address Range Up Into CIDR Ranges In Lucee CFML 5.3.9.141

By
Published in

The other day, this blog was being attacked by a malicious actor in Australia. I identified 38 unique IP addresses that were all poking and prodding the application, looking for weaknesses. Thankfully, this caused zero issues for the site itself or its visitors. However, in the heat of the moment, as I was adding these IPs to Cloudflare's Web Application Firewall (WAF) rules, I realized that my understanding of how IP addresses work was quite lacking. I needed to create CIDR ranges for the WAF; but, wasn't sure how to do that. As such, I wanted to take a moment and play around with IP addresses, taking a given range and breaking it up into the tightest possible CIDR ranges in Lucee CFML.

For this exploration, I'm going to use the Commons IP Math Java library created by Yannis Gonianakis. I've used this library in the past to see if a given IP address falls within a CIDR range; and, it's quite convenient! On top of that, I'm going to use Lucee CFML's ability to dynamically load JAR files on the fly in createObject() calls.

I don't really know networking protocols at all. So, I'm not going to try and explain how IP addresses work in any detail - that's best left to people who know what the heck they're actually talking about. But, for the sake of this exploration, I'll say that a CIDR (Classless Inter-Domain Routing) range is an IP address notation that denotes a contiguous range of IPs that can be represented using a number of right-hand bits within an IP address.

The number of bits "locked down" is defined by the slash in the CIDR range. For example, in this CIDR range:

10.0.0.2/31

... the "31" at the end means that the left 31 bits of the IP address are "locked down", ie cannot be changed. As such, all of the IP addresses in this range are represented by a single dynamic bit at the end:

00001010.00000000.00000000.0000001*

That last bit can either be a 1 or a 0, which means that this CIDR range can only represents two unique IP addresses:

  • 00001010.00000000.00000000.00000010 - ending in .2
  • 00001010.00000000.00000000.00000011 - ending in .3

The fewer "locked down" bits in the CIDR range, the more dynamic bits there at the end, the larger the number of IPs that can be represented by a single range. My goal in this experiment is to find CIDR ranges that represent a given set of IPs without creating ranges that encompass more IPs than we want.

To do this, I'm going to sequentially traverse from the lowest IP to the highest IP and look at the "common prefix" (ie, the number of "locked down" bits) between each pair of sibling addresses. If both siblings share a common prefix - and if that prefix doesn't allow for IP addresses outside our desired range - then the two sibling addresses can be part of the same CIDR range. Otherwise, the two sibling addresses have to be separated and represented by two different CIDR ranges.

Here's my algorithm for doing this in ColdFusion using the Commons-Ip-Math. At this point, I'll defer all explanation to the comments in the code:

<cfscript>

	// https://www.ipaddressguide.com/cidr

	// Some IP utility classes that will help us calculate our ranges.
	// --
	// https://github.com/jgonian/commons-ip-math
	Ipv4 = javaNew( "com.github.jgonian.ipmath.Ipv4" );
	Ipv4Range = javaNew( "com.github.jgonian.ipmath.Ipv4Range" );

	// Imagine that we want to create CIDR (Classless Inter-Domain Routing) ranges that
	// encompass the given min/max IP addresses. If we tried to create a "single range"
	// to rule them all, we'd end up having to create a range larger than we wanted to
	// (which would include IPs outside this min/max). As such, we have to break this
	// range up into a set of tighter ranges that include only the IPs we have here.
	minIpAddress = Ipv4.of( "10.0.0.3" );
	maxIpAddress = Ipv4.of( "10.0.0.72" );

	// CIDR ranges work by locking down a given number of "prefix bits" in the IP address.
	// The IPs in a given range are then represented by the exhaustive combination of the
	// remaining "suffix bits". To create "tight" ranges (ie, ranges that only include the
	// IPs that we want and nothing extra) we have create segments in which the "lowest"
	// and the "highest" IPs for a given prefix fall within the range of IPs that we want
	// to target. To calculate this, I'm going to start at the lowest IP and incrementally
	// step towards the highest IP, checking to see if the sub-slice of IPs fit into a
	// tight range. If they do, they can be part of the same CIDR range; but, if they
	// don't, I have to split the sub-slice further, creating two tighter ranges.
	segment = {
		from: minIpAddress,
		to: minIpAddress
	};
	// Keep track of all the segments that go into our range.
	ranges = [ segment ];

	// Keep stepping up in the IP range while our current segment's TO IP is less than the
	// maximum IP in our desired range.
	while ( isIpLessThan( segment.to, maxIpAddress ) ) {

		next = segment.to.next();

		// To see if the "next" IP fits in the current segment, we have to get the common
		// prefix of the two IPs (ie, the number of locked-down "prefix bits" in the IP
		// address that encompasses both IPs); and, we have to make sure that said prefix
		// doesn't encompass any IPs OUTSIDE the current segment.
		commonPrefix = segment.from.getCommonPrefixLength( next );
		minPrefixIpAddress = next.lowerBoundForPrefix( commonPrefix );
		maxPrefixIpAddress = next.upperBoundForPrefix( commonPrefix );

		// If the lower-bound IP encompassed by the common prefix is smaller than our
		// current segment; or, if the upper-bound IP encompassed by the common prefix is
		// larger than our overall IP-range; then, we need to split into a new segment
		// with a tighter prefix.
		if (
			isIpLessThan( minPrefixIpAddress, segment.from ) ||
			isIpGreaterThan( maxPrefixIpAddress, maxIpAddress )
			) {

			segment = {
				from: next,
				to: next
			};
			ranges.append( segment );

		// If the lower/upper-bound IPs fall within our overall IP-range, than we can just
		// extend the current segment to include the given IP and then move onto the next
		// IP in the range.
		} else {

			segment.to = next;

		}

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	// At this point, we've taken our min/man IP-range and split it up into a set of CIDR
	// ranges that represent ONLY the IPs that we care about.
	// --
	// NOTE: I've double-checked this using the CIDR IP-Address utilities provided by
	// https://www.ipaddressguide.com/cidr
	for ( segment in ranges ) {

		segmentRange = Ipv4Range
			.from( segment.from )
			.to( segment.to )
		;

		echo( segmentRange.toStringInCidrNotation() );
		echo( " ... which contains ... " );
		echo( segmentRange.toStringInRangeNotationWithSpaces() );
		echo( "<br />" );

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I determine if the first Commons-Ip-Math address is smaller than the second one.
	*/
	public boolean function isIpLessThan(
		required any a,
		required any b
		) {

		return( a.compareTo( b ) < 0 );

	}


	/**
	* I determine if the first Commons-Ip-Math address is greater than the second one.
	*/
	public boolean function isIpGreaterThan(
		required any a,
		required any b
		) {

		return( a.compareTo( b ) > 0 );

	}


	/**
	* I create the Commons-Ip-Math class definition with the given name.
	*/
	public any function javaNew( required string className ) {

		var jarPaths = [
			expandPath( "/vendor/commons-ip-math-1.32.jar" )
		];

		return( createObject( "java", className, jarPaths ) );

	}

</cfscript>

This code steps from 10.0.0.3 up to 10.0.0.72, breaking the range down into "tight" CIDR ranges. And, when we run this ColdFusion code, we get the following output:

10.0.0.3/32 ... which contains ... 10.0.0.310.0.0.3
10.0.0.4/30 ... which contains ... 10.0.0.410.0.0.7
10.0.0.8/29 ... which contains ... 10.0.0.810.0.0.15
10.0.0.16/28 ... which contains ... 10.0.0.1610.0.0.31
10.0.0.32/27 ... which contains ... 10.0.0.3210.0.0.63
10.0.0.64/29 ... which contains ... 10.0.0.6410.0.0.71
10.0.0.72/32 ... which contains ... 10.0.0.7210.0.0.72

As you can see, the range of IPs cannot be represented by one large CIDR range. Instead, we have to break it up into several smaller CIDR ranges that encompass the limited set of IPs we are targeting and nothing more.

ASIDE: As I was working on this ColdFusion algorithm, I was using the IP Address Guide CIDR tools to both understand the constraints of the problem and to confirm that my output matched their output. Much iteration was needed to get this right!

This post uses IPv4 addresses; but, I assume that an algorithm for IPv6 addresses would work the same way, only with a vastly wider range of possible addresses. Hopefully, I didn't get any of this too wrong, technically. Like I said before, I don't really understand anything about networking. But, baby steps like this are slowly filling in the gaps.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel