There are many services online that allow you to download IP ranges for a specified country, but these ranges usually come in CIDR notation format.
I wanted to be able to combine these ranges into larger ranges, to reduce the number of rows I would need to add to iptables in Linux.
What is CIDR notation?
CIDR stands for Classless Inter-Domain Routing and was introduced in 1993 to solve the problems of so-called "classful" routing.
CIDR notation represents an IP address range in the format a.b.c.d/s where s is the suffix which corresponds to the subnet mask.
The value of the suffix must be from 0 to 32 inclusive, and essentially represents the number of common bits, from left to right, in the entire IP range.
As IPv4 is 32-bit, a value of 32 indicates a range of exactly one address, as all 32 bits of the start address must correspond to all 32 bits of the end address.
A value of 0 indicates that there are no common bits. In this case, 0.0.0.0/0 is the only valid CIDR range with a suffix of 0, and it represents the entire IPv4 address space.
A value of 24 means that there are 24 common bits. That leaves 8-bits (the last octet) which varies. That is to say, a CIDR notation of 192.168.0.0/24 gives the range from 192.168.0.0 to 192.168.0.255
IP Addresses are actually integers
It may not seem obvious at first, but an IPv4 address is simply a 32-bit integer written in a different way. A byte is 8-bit, so it’s four bytes (a.b.c.d)
2^32 = 4294967295 = 255.255.255.255
It is therefore possible to display or store any IPv4 address as a simple unsigned-integer.
Which CIDR ranges are valid?
A CIDR range is only valid if the last n bits indicated by the suffix are zeros at the first address and ones at the last address. More precisely, the number of bits n where:
n = 32 - s
So if s is 24 then n is 8, and therefore the binary representation of the first address in the range will end with:
…00000000
And the last address will end with:
…11111111
How to represent a CIDR range in code
Since a CIDR range is basically a start address and an end address, it is possible to calculate the subnet that both addresses belong to. If the first address in the subnet is exactly the same as the start address, and if the last address in the subnet is exactly the same as the end address, then the IP range is a valid CIDR range.
Here’s a way we can represent this in C# code:
using System;
using System.Net;
namespace ACA.Net
{
public class IPv4Range
{
public IPAddress From { get; }
public IPAddress To { get; }
public uint FromAddressInteger => this.From.ToInteger();
public uint ToAddressInteger => this.To.ToInteger();
public IPv4Range(IPAddress from, IPAddress to)
{
if (Compare(from, to) > 0) throw new ArgumentException("The to address must be after the from address", nameof(to));
this.From = from;
this.To = to;
}
public IPv4Range(string cidr)
{
if (String.IsNullOrWhiteSpace(cidr)) throw new ArgumentNullException(nameof(cidr));
var sections = cidr.Split('/', 2);
if (sections.Length != 2) throw new ArgumentException("Invalid CIDR");
var ip = IPAddress.Parse(sections[0]);
byte suffix = Convert.ToByte(sections[1]);
var fromBinary = ip.GetBinaryRepresentation();
var subnetStartBinary = fromBinary.Substring(0, suffix).PadRight(32, '0');
var subnetEndBinary = fromBinary.Substring(0, suffix).PadRight(32, '1');
var firstAddress = FromBinary(subnetStartBinary);
var lastAddress = FromBinary(subnetEndBinary);
this.From = firstAddress;
this.To = lastAddress;
}
public byte CidrSuffix => CompareNumberOfEqualBits(this.From, this.To);
public string Cidr => this.SubnetStart + "/" + this.CidrSuffix;
public IPv4Range Subnet => new IPv4Range(this.SubnetStart, this.SubnetEnd);
public IPAddress SubnetStart
=> FromBinary(
this.From.GetBinaryRepresentation()
.Substring(0, CompareNumberOfEqualBits(this.From, this.To))
.PadRight(32, '0'));
public IPAddress SubnetEnd
=> FromBinary(
this.From.GetBinaryRepresentation()
.Substring(0, CompareNumberOfEqualBits(this.From, this.To))
.PadRight(32, '1'));
public bool IsRangeExactCidr
=> Compare(this.From, this.SubnetStart) == 0
&& Compare(this.To, this.SubnetEnd) == 0;
internal static IPAddress FromBinary(string binary)
{
if (String.IsNullOrWhiteSpace(binary)) throw new ArgumentNullException(nameof(binary));
if (binary.Length != 32) throw new ArgumentException("Binary must be 32 bits!");
byte[] values = new byte[4];
for (byte i=0; i<4; i++)
{
var bits = binary.Substring(i*8, 8);
var value = Convert.ToByte(bits, 2);
values[i] = value;
}
return IPAddress.Parse(String.Join('.', values));
}
private static byte CompareNumberOfEqualBits(IPAddress address1, IPAddress address2)
{
var fromBinary = address1.GetBinaryRepresentation();
var toBinary = address2.GetBinaryRepresentation();
if (fromBinary.Length != toBinary.Length) throw new ArgumentException("Binary representations must be of equal length", nameof(address2));
for (byte i=0; i<fromBinary.Length; i++)
{
if (fromBinary[i] == toBinary[i]) continue;
else return i;
}
return (byte)fromBinary.Length;
}
public static int Compare(IPAddress from, IPAddress to)
{
if (from is null) throw new ArgumentNullException(nameof(from));
if (to is null) throw new ArgumentNullException(nameof(to));
if (!from.IsV4()) throw new ArgumentException("IP address must be V4!", nameof(from));
if (!to.IsV4()) throw new ArgumentException("IP address must be V4!", nameof(to));
var bytesfrom = from.GetAddressBytes();
var bytesto = to.GetAddressBytes();
for (int i = 0; i < 4; i++)
{
if (bytesfrom[i] < bytesto[i]) return -1;
if (bytesfrom[i] == bytesto[i]) continue;
if (bytesfrom[i] > bytesto[i]) return 1;
}
return 0;
}
public override string ToString()
{
if (Compare(this.From, this.To) == 0) return this.From.ToString();
return this.From + "-" + this.To + " (" + this.Cidr + ")";
}
public static bool RangesOverlap(IPv4Range range1, IPv4Range range2)
=> Compare(range2.From, range1.To) <= 0
&& Compare(range2.To, range1.From) >= 0;
public static bool RangesAreAdjacent(IPv4Range range1, IPv4Range range2)
=> range1.ToAddressInteger + 1 == range2.FromAddressInteger
|| range2.ToAddressInteger + 1 == range1.FromAddressInteger;
public static bool RangesOverlapOrAreAdjacent(IPv4Range range1, IPv4Range range2)
=> RangesOverlap(range1, range2)
|| RangesAreAdjacent(range1, range2);
}
}
The above class also requires the use of some extension methods:
using System;
using System.Linq;
using System.Net;
namespace ACA.Net
{
public static class Extensions
{
internal static string GetBinaryRepresentation(this IPAddress ip, bool includeDot = false)
{
string seperator = includeDot ? "." : "";
return String.Join(seperator, ip.GetAddressBytes().Select(x => Convert.ToString(x, 2).PadLeft(8, '0')));
}
internal static bool IsV4(this IPAddress ip)
=> ip != null
&& !ip.IsIPv4MappedToIPv6
&& !ip.IsIPv6LinkLocal
&& !ip.IsIPv6Multicast
&& !ip.IsIPv6SiteLocal
&& !ip.IsIPv6Teredo;
public static uint ToInteger(this IPAddress ip)
=> Convert.ToUInt32(ip.GetBinaryRepresentation(), 2);
public static IPAddress GetNextAddress(this IPAddress ip)
{
var integer = ip.ToInteger() + 1;
var binary = Convert.ToString(integer, 2).PadLeft(32, '0');
return IPv4Range.FromBinary(binary);
}
public static IPAddress GetPreviousAddress(this IPAddress ip)
{
var integer = ip.ToInteger() - 1;
var binary = Convert.ToString(integer, 2).PadLeft(32, '0');
return IPv4Range.FromBinary(binary);
}
}
}
I hope this article helps you to understand CIDR and work with IP ranges in your .NET code!
Happy coding! 😊