using System;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using Swan.Net.Internal;
namespace Swan.Net
{
// NOTE TO CONTRIBUTORS: When adding a check on a public method parameter,
// please do not just "throw new ArgumentException(...)".
// Instead, look at the exception-returning private methods at the bottom of this file
// and either find one suitable for your case, or add a new one.
// This way we can keep the exception messages consistent.
///
/// Represents an inclusive range of IP addresses.
///
///
/// This class makes no distinction between IPv4 addresses and the same addresses mapped to IPv6
/// for the purpose of determining whether it belongs to a range: that is, the method
/// of an instance initialized with IPv4 addresses, or with the same addresses mapped to IPv6,
/// will return for both an in-range IPv4 address and the same address mapped to IPv6.
/// The constructor, however,
/// does make such distinction: you cannot initialize a range using an IPv4 address and an IPv6 address,
/// even if the latter is an IPv4 address mapped to IPv6, nor the other way around.
///
///
[Serializable]
public sealed class IPAddressRange : IEquatable
{
///
/// Gets an instance of that contains no addresses.
/// The method of the returned instance will always return .
/// This property is useful to initialize non-nullable properties
/// of type .
///
public static readonly IPAddressRange None = new IPAddressRange(IPAddressValue.MaxValue, IPAddressValue.MinValue, true, 0);
///
/// Gets an instance of that contains all possible IP addresses.
/// The method of the returned instance will always return .
///
public static readonly IPAddressRange All = new IPAddressRange(IPAddressValue.MinValue, IPAddressValue.MaxValue, true, 128);
///
/// Gets an instance of that contains all IPv4 addresses.
/// The method of the returned instance will return
/// for all IPv4 addresses, as well as their IPv6 mapped counterparts, and
/// for all other IPv6 addresses.
///
public static readonly IPAddressRange AllIPv4 = new IPAddressRange(IPAddressValue.MinIPv4Value, IPAddressValue.MaxIPv4Value, false, 32);
private readonly IPAddressValue _start;
private readonly IPAddressValue _end;
private readonly bool _isV6;
private readonly byte _prefixLength;
///
/// Initializes a new instance of the class,
/// representing a single IP address.
///
/// The IP address.
/// is .
public IPAddressRange(IPAddress address)
{
if (address == null)
throw new ArgumentNullException(nameof(address));
_start = _end = new IPAddressValue(address);
_isV6 = address.AddressFamily == AddressFamily.InterNetworkV6;
_prefixLength = 0;
}
///
/// Initializes a new instance of the class,
/// representing a range of IP addresses between
/// and , extremes included.
///
/// The starting address of the range.
/// The ending address of the range.
///
/// is .
/// - or -
/// is .
///
///
/// has a different AddressFamily
/// from .
/// - or -
/// is a lower address than ,
/// i.e. the binary representation of in network byte order
/// is a lower number than the same representation of .
///
public IPAddressRange(IPAddress start, IPAddress end)
{
if (start == null)
throw new ArgumentNullException(nameof(start));
if (end == null)
throw new ArgumentNullException(nameof(end));
var startFamily = start.AddressFamily;
_isV6 = startFamily == AddressFamily.InterNetworkV6;
if (end.AddressFamily != startFamily)
throw MismatchedEndFamily(nameof(end));
_start = new IPAddressValue(start);
_end = new IPAddressValue(end);
if (_end.CompareTo(_start) < 0)
throw EndLowerThanStart(nameof(end));
_prefixLength = 0;
}
///
/// Initializes a new instance of the class,
/// representing a CIDR subnet.
///
/// The base address of the subnet.
/// The prefix length of the subnet.
/// is .
///
/// is zero.
/// - or -
/// is greater than the number of bits in
/// the binary representation of (32 for IPv4 addresses,
/// 128 for IPv6 addresses.)
/// - or -
/// cannot be the base address of a subnet with a prefix length
/// equal to , because the remaining bits after the prefix
/// are not all zeros.
///
public IPAddressRange(IPAddress baseAddress, byte prefixLength)
{
if (baseAddress == null)
throw new ArgumentNullException(nameof(baseAddress));
byte maxPrefixLength;
if (baseAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
_isV6 = true;
maxPrefixLength = 128;
}
else
{
_isV6 = false;
maxPrefixLength = 32;
}
if (prefixLength < 1 || prefixLength > maxPrefixLength)
throw InvalidPrefixLength(nameof(prefixLength));
_start = new IPAddressValue(baseAddress);
if (!_start.IsStartOfSubnet(prefixLength))
throw InvalidSubnetBaseAddress(nameof(baseAddress));
_end = _start.GetEndOfSubnet(prefixLength);
_prefixLength = prefixLength;
}
private IPAddressRange(IPAddressValue start, IPAddressValue end, bool isV6, byte prefixLength)
{
_start = start;
_end = end;
_isV6 = isV6;
_prefixLength = prefixLength;
}
///
/// Gets the address family of the IP address range.
///
///
/// Regardless of the value of this property, IPv4 addresses
/// and their IPv6 mapped counterparts will be considered the same
/// for the purposes of the method.
///
public AddressFamily AddressFamily => _isV6 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork;
///
/// Gets a value indicating whether this instance represents a CIDR subnet.
///
///
/// This property is only for instances
/// initialized via the constructor.
/// Instances constructed by specifying a range will have this property
/// set to even when they actually represent a subnet.
/// For example, the instance returned by IPAddressRange.Parse("192.168.0.0-192.168.0.255")
/// will have this property set to ; for this property to be ,
/// the string passed to should instead be "192.168.0.0/24"
/// (a CIDR subnet specification) or "192.168.0.0/255.255.255.0" (a base address / netmask pair,
/// only accepted by and for IPv4 addresses.)
///
public bool IsSubnet => _prefixLength > 0;
///
/// Gets an instance of representing
/// the first address in the range.
///
public IPAddress Start => _start.ToIPAddress(_isV6);
///
/// Gets an instance of representing
/// the last address in the range.
///
public IPAddress End => _end.ToIPAddress(_isV6);
///
/// Tries to convert the string representation of a range of IP addresses
/// to an instance of .
///
/// The string to convert.
/// When this method returns ,
/// an instance of representing the same range of
/// IP addresses represented by .
/// if the conversion was successful;
/// otherwise, .
/// See the "Remarks" section of
/// for an overview of the formats accepted for .
///
public static bool TryParse(string str, out IPAddressRange result)
=> TryParseInternal(nameof(str), str, out result) == null;
///
/// Converts the string representation of a range of IP addresses
/// to an instance of .
///
/// The string to convert.
/// An instance of representing the same range of
/// IP addresses represented by .
/// is .
/// is in none of the supported formats.
///
/// This method supports the following formats for :
///
///
/// Format
/// Description
/// Examples
///
/// -
/// Single address
/// A single IP address.
///
/// 192.168.23.199
/// 2001:db8:a0b:12f0::1
///
///
/// -
/// Range of addresses
/// Start and end address, separated by a hyphen (-).
///
/// 192.168.0.100-192.168.11.255
/// 2001:db8:a0b:12f0::-2001:db8:a0b:12f0::ffff
///
///
/// -
/// CIDR subnet
/// Base address and prefix length, separated by a slash (/).
///
/// 169.254.0.0/16
/// 192.168.123.0/24
/// 2001:db8:a0b:12f0::/64
///
///
/// -
/// "Legacy" subnet
///
/// Base address and netmask, separated by a slash (/).
/// Only accepted for IPv4 addresses.
///
///
/// 169.254.0.0/255.255.0.0
/// 192.168.123.0/255.255.255.0
///
///
///
///
///
public static IPAddressRange Parse(string str)
{
var exception = TryParseInternal(nameof(str), str, out var result);
if (exception != null)
throw exception;
return result;
}
///
///
/// The result of this method will be a string that,
/// if passed to the or method,
/// will result in an instance identical to this one.
/// If this instance has been created by means of the
/// or method, the returned string will not
/// necessarily be identical to the parsed string. The possible differences
/// include the following:
///
/// - ranges consisting of just one IP address will result in a
/// string representing that single address;
/// - addresses in the returned string are passed to the
/// method, resulting in standardized
/// representations that may be different from the originally parsed
/// strings;
/// - the returned string will contain no blank characters;
/// - address ranges parsed as address/netmask will be
/// rendered as CIDR subnets: for example,
/// IPAddressRange.Parse("192.168.19.0/255.255.255.0").ToString()
/// will return "192.168.19.0/24".
///
///
public override string ToString()
=> _prefixLength > 0
? $"{Start}/{_prefixLength}"
: _start.CompareTo(_end) == 0
? Start.ToString()
: $"{Start}-{End}";
///
/// Determines whether the given
/// sa contained in this range.
///
/// The IP address to check.
/// if
/// is between and , inclusive;
/// otherwise, .
/// is .
///
/// This method treats IPv4 addresses and their IPv6-mapped counterparts
/// the same; that is, given a range obtained by parsing the string 192.168.1.0/24,
/// Contains(IPAddress.Parse("192.168.1.55")) will return ,
/// as will Contains(IPAddress.Parse("192.168.1.55").MapToIPv6()). This is true
/// as well if a range is initialized with IPv6 addresses.
///
public bool Contains(IPAddress address)
{
if (address == null)
throw new ArgumentNullException(nameof(address));
var addressValue = new IPAddressValue(address);
return addressValue.CompareTo(_start) >= 0
&& addressValue.CompareTo(_end) <= 0;
}
///
public override bool Equals(object? obj) => obj is IPAddressRange other && Equals(other);
///
public bool Equals(IPAddressRange? other)
=> other != null
&& other._start.Equals(_start)
&& other._end.Equals(_end)
&& other._isV6 == _isV6
&& other._prefixLength == _prefixLength;
///
public override int GetHashCode() => CompositeHashCode.Using(_start, _end, _isV6, _prefixLength);
private static bool TryNetmaskToCidrPrefixLength(byte[] bytes, out byte result)
{
result = 0;
var length = bytes.Length;
var prefixFound = false;
for (var i = 0; i < length; i++)
{
if (prefixFound)
{
if (bytes[i] != 0)
return false;
}
else
{
switch (bytes[i])
{
case 0x00:
if (result == 0)
return false;
prefixFound = true;
break;
case 0x80:
result += 1;
prefixFound = true;
break;
case 0xC0:
result += 2;
prefixFound = true;
break;
case 0xE0:
result += 3;
prefixFound = true;
break;
case 0xF0:
result += 4;
prefixFound = true;
break;
case 0xF8:
result += 5;
prefixFound = true;
break;
case 0xFC:
result += 6;
prefixFound = true;
break;
case 0xFE:
result += 7;
prefixFound = true;
break;
case 0xFF:
result += 8;
break;
default:
return false;
}
}
}
return true;
}
private static Exception? TryParseInternal(string paramName, string? str, out IPAddressRange result)
{
result = None;
if (str == null)
return new ArgumentNullException(paramName);
// Try CIDR format (e.g. 192.168.99.0/24) and address/netmask format (192.168.99.0/255.255.255.0)
var separatorPos = str.IndexOf('/');
if (separatorPos >= 0)
return TryParseCidrOrAddressNetmaskFormat(str, separatorPos, out result);
// Try range format (e.g. 192.168.99.100-192.168.99.199)
separatorPos = str.IndexOf('-');
if (separatorPos >= 0)
return TryParseStartEndFormat(str, separatorPos, out result);
// Try single address format (e.g. 192.168.99.123)
return TryParseSingleAddressFormat(str, out result);
}
private static Exception? TryParseCidrOrAddressNetmaskFormat(string str, int separatorPos, out IPAddressRange result)
{
result = None;
var s = str.Substring(0, separatorPos).Trim();
if (!IPAddressUtility.TryParse(s, out var address))
return InvalidIPAddress();
var addressValue = new IPAddressValue(address);
s = str.Substring(separatorPos + 1).Trim();
if (byte.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var prefixLength))
{
var maxPrefixLength = address.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32;
if (prefixLength < 1 || prefixLength > maxPrefixLength)
return InvalidPrefixLength();
if (!addressValue.IsStartOfSubnet(prefixLength))
return InvalidSubnetBaseAddress();
result = new IPAddressRange(addressValue, addressValue.GetEndOfSubnet(prefixLength), address.AddressFamily == AddressFamily.InterNetworkV6, prefixLength);
return null;
}
// Only accept a netmask for IPv4
if (address.AddressFamily != AddressFamily.InterNetwork)
return InvalidPrefixLength();
if (!IPAddressUtility.TryParse(s, out var netmask))
return InvalidPrefixLengthOrNetmask();
var addressFamily = address.AddressFamily;
if (netmask.AddressFamily != addressFamily)
return MismatchedNetmaskAddressFamily();
var netmaskBytes = netmask.GetAddressBytes();
if (!TryNetmaskToCidrPrefixLength(netmaskBytes, out prefixLength))
return InvalidNetmask();
if (!addressValue.IsStartOfSubnet(prefixLength))
return InvalidSubnetBaseAddress();
result = new IPAddressRange(addressValue, addressValue.GetEndOfSubnet(prefixLength), false, prefixLength);
return null;
}
private static Exception? TryParseStartEndFormat(string str, int separatorPos, out IPAddressRange result)
{
result = None;
var s = str.Substring(0, separatorPos).Trim();
if (!IPAddressUtility.TryParse(s, out var startAddress))
return InvalidStartAddress();
s = str.Substring(separatorPos + 1).Trim();
if (!IPAddressUtility.TryParse(s, out var endAddress))
return InvalidEndAddress();
var addressFamily = startAddress.AddressFamily;
if (endAddress.AddressFamily != addressFamily)
return MismatchedStartEndFamily();
var start = new IPAddressValue(startAddress);
var end = new IPAddressValue(endAddress);
if (end.CompareTo(start) < 0)
return EndLowerThanStart();
result = new IPAddressRange(start, end, addressFamily == AddressFamily.InterNetworkV6, 0);
return null;
}
private static Exception? TryParseSingleAddressFormat(string str, out IPAddressRange result)
{
result = None;
if (!IPAddressUtility.TryParse(str, out var address))
return InvalidIPAddress();
var addressValue = new IPAddressValue(address);
result = new IPAddressRange(addressValue, addressValue, address.AddressFamily == AddressFamily.InterNetworkV6, 0);
return null;
}
private static Exception InvalidIPAddress() => new FormatException("An invalid IP address was specified.");
private static Exception InvalidPrefixLengthOrNetmask() => new FormatException("An invalid prefix length or netmask was specified.");
private static Exception MismatchedNetmaskAddressFamily() => new FormatException("Address and netmask are different types of addresses.");
private static Exception InvalidPrefixLength() => new FormatException("An invalid prefix length was specified.");
private static Exception InvalidPrefixLength(string paramName) => new ArgumentException("The prefix length is invalid.", paramName);
private static Exception InvalidNetmask() => new FormatException("An invalid netmask was specified.");
private static Exception InvalidSubnetBaseAddress() => new FormatException("The specified address is not the base address of the specified subnet.");
private static Exception InvalidSubnetBaseAddress(string paramName) => new ArgumentException("The specified address is not the base address of the specified subnet.", paramName);
private static Exception InvalidStartAddress() => new FormatException("An invalid start address was specified for a range.");
private static Exception InvalidEndAddress() => new FormatException("An invalid end address was specified for a range.");
private static Exception MismatchedStartEndFamily() => new FormatException("Start and end are different types of addresses.");
private static Exception MismatchedEndFamily(string paramName) => new ArgumentException("The end address of a range must be of the same family as the start address.", paramName);
private static Exception EndLowerThanStart() => new FormatException("An end address was specified for a range that is lower than the start address.");
private static Exception EndLowerThanStart(string paramName) => new ArgumentException("The end address of a range cannot be lower than the start address.", paramName);
}
}