Skip to content

Instantly share code, notes, and snippets.

@wolfenrain
Created July 20, 2025 13:01
Show Gist options
  • Save wolfenrain/74fc831ad4f8ffacdb657f0559c5919a to your computer and use it in GitHub Desktop.
Save wolfenrain/74fc831ad4f8ffacdb657f0559c5919a to your computer and use it in GitHub Desktop.
CIDR network range interface for Dart
import 'dart:io';
import 'dart:typed_data';
import 'package:meta/meta.dart';
/// Represents a range of [InternetAddress] in a network.
///
/// {@template cidr}
/// Any instance of [CIDR] is lazy iterable of [InternetAddress] and looping
/// over it can take a long time as the range of IPs can be astronomically high.
///
/// The [contains] method can however be safely used as it is optimized to check
/// using the bytes representation of an [InternetAddress].
/// {@endtemplate}
///
/// Usage:
///
/// ```dart
/// // You can create an instance using an InternetAddress directly.
/// final cidr4 = CIDR(InternetAddress('192.168.1.0', prefixLength: 24));
///
/// // You can use an instance of InternetAddress or a String to check if it is
/// // within the range.
/// print(cidr4.contains(InternetAddress('192.168.1.5'))); // true
/// print(cidr4.containsString('192.168.2.5')); // false
///
/// // You can create an instance by parsing a CIDR notation.
/// final cidr6 = CIDR.parse('2001:db8::/126');
/// print(cidr6.containsString('2001:db8::1')); // true
/// print(cidr6.contains(InternetAddress('2001:db9::1'))); // false
/// ```
@immutable
class CIDR extends Iterable<InternetAddress> {
/// Create a new [CIDR] instance from an [ipAddress] and the [prefixLength]
///
/// {@macro cidr}.
const CIDR(this.ipAddress, {required this.prefixLength});
/// Parse a CIDR [notation] string into a [CIDR] instance.
///
/// {@macro cidr}
factory CIDR.parse(String notation) {
final parts = notation.split('/');
if (parts.length != 2) {
throw const FormatException('Notation has to be a valid CIDR notation');
}
final [address, bytes] = parts;
// Try to parse the address.
final ipAddress = InternetAddress.tryParse(address);
if (ipAddress == null) {
throw const FormatException('Ip address must be a valid address');
}
// Try to parse the prefix value.
final prefixLength = int.tryParse(bytes);
if (prefixLength == null) {
throw const FormatException('Prefix length must be a decimal value');
}
return CIDR(ipAddress, prefixLength: prefixLength);
}
/// Parse a CIDR [notation] string into a [CIDR] instance.
///
/// Like [CIDR.parse] except that this function returns `null` where a
/// similar call to [CIDR.parse] would throw a [FormatException].
static CIDR? tryParse(String notation) {
try {
return CIDR.parse(notation);
} on FormatException {
return null;
}
}
/// The starting address of the network.
///
/// It represents the network itself and serves as the base for the range of
/// addresses covered by this [CIDR] instance.
final InternetAddress ipAddress;
/// The number of significant bits in the [ipAddress] that represent the
/// portion of the network.
final int prefixLength;
/// The number of [InternetAddress]es in this [CIDR].
///
/// Unlike the [length], which gets computed by iterating through each element
/// this returns a computed value.
BigInt get computedLength {
return BigInt.one << ((ipAddress.rawAddress.length * 8) - prefixLength);
}
/// The number of [InternetAddress]es in this [CIDR].
///
/// This will iterate through all the elements in the iterable and will most
/// likely be slow as IP ranges can have many hosts. And the returned value
/// might be clamped to the max int value of the platform.
///
/// For a correct and fast length value use the [computedLength].
@override
int get length => super.length;
@override
bool operator ==(Object other) {
return switch (other) {
CIDR(:final ipAddress, :final prefixLength) =>
this.ipAddress == ipAddress && prefixLength == prefixLength,
_ => false,
};
}
@override
int get hashCode => Object.hash(ipAddress, prefixLength);
@override
Iterator<InternetAddress> get iterator => _CIDRIterator(this);
/// Check if [address] is in the range.
///
/// The [containsString] call this by converting the string input to a
/// [InternetAddress].
@override
bool contains(covariant InternetAddress? address) {
// If it is not an internet address, then there is no need to keep checking
// the rest.
if (address is! InternetAddress) {
return false;
}
// If the types are not the same then they are not comparable at all.
if (ipAddress.type != address.type) {
return false;
}
// Get the bytes of both addresses.
final ipBytes = ipAddress.rawAddress;
final otherBytes = address.rawAddress;
final prefixBytes = prefixLength ~/ 8;
final remainingBits = prefixLength % 8;
// Compare the full bytes first. If any is not equal we know they aren't the
// same.
for (var i = 0; i < prefixBytes; i++) {
if (ipBytes[i] != otherBytes[i]) {
return false;
}
}
// If there are any remaining bits we can compare the last byte.
if (remainingBits > 0) {
final mask = 0xFF << (8 - remainingBits) & 0xFF;
if ((ipBytes[prefixBytes] & mask) != (otherBytes[prefixBytes] & mask)) {
return false;
}
}
return true;
}
/// Check if [address] is in the range.
///
/// See [contains] for the [InternetAddress] implementation.
bool containsString(String address) => contains(InternetAddress(address));
/// Copy this instance into a new instance with different [ipAddress] or
/// [prefixLength] values.
CIDR copyWith({InternetAddress? ipAddress, int? prefixLength}) => CIDR(
ipAddress ?? this.ipAddress,
prefixLength: prefixLength ?? this.prefixLength,
);
}
class _CIDRIterator implements Iterator<InternetAddress> {
_CIDRIterator(CIDR cidr)
: _base = _toInt(cidr.ipAddress.rawAddress),
_length = cidr.ipAddress.rawAddress.length,
_total = cidr.computedLength,
_current = BigInt.zero;
/// An [InternetAddress]'s raw representation as a big integer.
final BigInt _base;
/// The total length of bytes in [InternetAddress]'s raw representation.
final int _length;
/// The total amount of hosts within this range.
final BigInt _total;
/// The current big int value, which should be added to [_base] to get the
/// [current] [InternetAddress].
BigInt _current;
@override
InternetAddress get current {
final hex = (_base + _current).toRadixString(16).padLeft(_length * 2, '0');
return InternetAddress.fromRawAddress(
Uint8List.fromList([
for (var i = 0; i < _length; i++)
int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
]),
);
}
@override
bool moveNext() {
if (_current == _total) return false;
_current += BigInt.one;
return true;
}
/// Convert a list of [bytes] to a [BigInt].
static BigInt _toInt(Uint8List bytes) => BigInt.parse(
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(),
radix: 16,
);
}
void main() {
final cidr = CIDR.parse('10.0.0.0/24');
print(cidr.containsString('10.0.0.2')); // true
print(cidr.containsString('10.0.1.2')); // false
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment