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); } }