Created
September 6, 2025 22:07
-
-
Save Blizzardo1/bd91cda40a8bb475a91af85c2bae7a52 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @author Aleksi Asikainen | |
| * @link https://github.com/salieri/IPSubnetCalculator | |
| * | |
| * IpSubnetCalculator 2.0.0 | |
| * | |
| * Copyright (c) 2013-2024, Aleksi Asikainen | |
| * Contributor: Adonis Deliannis <[email protected]> | |
| * All rights reserved. | |
| * | |
| * Released under MIT License | |
| * https://opensource.org/licenses/MIT | |
| * | |
| * Designed for: | |
| * | |
| * 1) Calculating optimal and exact subnet masks for an | |
| * unrestricted range of IP addresses. | |
| * | |
| * E.g. range `10.0.1.255 - 10.0.3.255` should result in: | |
| * | |
| * `10.0.1.255/32` | |
| * `10.0.2.0/24` | |
| * `10.0.3.0/24` | |
| * | |
| * 2) Calculating subnets from an IP and bitmask size | |
| * | |
| * 3) Calculating subnets and bitmask sizes from an IP and subnet mask | |
| * | |
| * | |
| * Use `calculate()`, `calculateSubnetMask()`, and `calculateCIDRPrefix()` for easy access. | |
| * | |
| */ | |
| export type IPAny = string | number; | |
| export type IPString = string; | |
| export type IPNumber = number | bigint; | |
| export type BitCount = number; | |
| export interface SubnetAnalysis { | |
| ipLow: IPNumber; | |
| ipLowStr: IPString; | |
| ipHigh: IPNumber; | |
| ipHighStr: IPString; | |
| prefixMask: IPNumber; | |
| prefixMaskStr: IPString; | |
| prefixSize: BitCount; | |
| invertedMask: IPNumber; | |
| invertedMaskStr: IPString; | |
| invertedMaskSize: BitCount; | |
| } | |
| export enum IpType { | |
| IPv4 = 'IPv4', | |
| IPv6 = 'IPv6' | |
| } | |
| let IPType: IpType = IpType.IPv4; // Default IP type, can be changed to IPv6 if needed | |
| export const getIpType = (ip: IPString) => { | |
| return ip.includes(':') ? IpType.IPv6 : IpType.IPv4; | |
| }; | |
| /** | |
| * Creates a bitmask with maskSize leftmost bits set to one | |
| * | |
| * @param {int} prefixSize Number of bits to be set | |
| * @return {int} Returns the bitmask | |
| * @private | |
| */ | |
| export const getPrefixMask = (prefixSize: BitCount): IPNumber => { | |
| if (IPType === IpType.IPv6) { | |
| let mask = BigInt(0); | |
| for (let i = 0; i < prefixSize; i++) { | |
| mask |= BigInt(1) << BigInt(128 - (i + 1)); | |
| } | |
| return mask; | |
| } else { | |
| let mask = 0; | |
| for (let i = 0; i < prefixSize; i++) { | |
| mask |= 1 << (32 - (i + 1)) >>> 0; | |
| } | |
| return mask; | |
| } | |
| }; | |
| /** | |
| * Creates a bitmask with maskSize rightmost bits set to one | |
| * | |
| * @param {int} maskSize Number of bits to be set | |
| * @return {int} Returns the bitmask | |
| * @private | |
| */ | |
| export const getMask = (maskSize: BitCount): IPNumber => { | |
| if (IPType === IpType.IPv6) { | |
| let mask = BigInt(0); | |
| for (let i = 0; i < maskSize; i++) { | |
| mask |= BigInt(1) << BigInt(i); | |
| } | |
| return mask; | |
| } else { | |
| let mask = 0; | |
| for (let i = 0; i < maskSize; i++) { | |
| mask |= 1 << i >>> 0; | |
| } | |
| return mask; | |
| } | |
| }; | |
| /** | |
| * Expands a compressed IPv6 address | |
| * @param ip IPv6 address string | |
| * @returns Expanded IPv6 address or null if invalid | |
| */ | |
| function expandIPv6(ip: string): string | null { | |
| if (!isIPv6(ip)) return null; | |
| // Handle IPv4-mapped addresses separately | |
| if (ip.includes('.')) { | |
| const parts = ip.split(':'); | |
| const ipv4Part = parts[parts.length - 1]; | |
| if (isIPv4(ipv4Part)) { | |
| const ipv4 = ipv4Part.split('.').map(n => parseInt(n, 10).toString(16).padStart(2, '0')); | |
| parts[parts.length - 1] = `${ipv4[0]}${ipv4[1]}:${ipv4[2]}${ipv4[3]}`; | |
| ip = parts.join(':'); | |
| } | |
| } | |
| const segments = ip.split('::'); | |
| if (segments.length > 2) return null; // Invalid: multiple :: | |
| let hextets = segments[0].split(':').filter(h => h !== ''); | |
| if (segments.length === 2) { | |
| const endHextets = segments[1].split(':').filter(h => h !== ''); | |
| const zeroCount = 8 - (hextets.length + endHextets.length); | |
| if (zeroCount < 0) return null; | |
| hextets = [...hextets, ...Array(zeroCount).fill('0'), ...endHextets]; | |
| } | |
| if (hextets.length !== 8) return null; | |
| return hextets.map(h => h.padStart(4, '0')).join(':'); | |
| } | |
| /** | |
| * Test whether string is an IP address | |
| * @param {string} ip | |
| * @returns {boolean} | |
| * @public | |
| */ | |
| export const isIp = (ip: IPString): boolean => { | |
| if (typeof ip !== 'string') return false; | |
| IPType = isIPv6(ip) ? IpType.IPv6 : IpType.IPv4; | |
| if (IPType === IpType.IPv4) { | |
| const parts = ip.match(/^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/); | |
| if (parts === null) return false; | |
| for (let i = 1; i <= 4; i++) { | |
| const n = parseInt(parts[i], 10); | |
| if (n > 255 || n < 0) return false; | |
| } | |
| return true; | |
| } else { | |
| const expanded = expandIPv6(ip); | |
| if (!expanded) return false; | |
| const parts = expanded.split(':'); | |
| if (parts.length !== 8) return false; | |
| let i = 0; | |
| for (const part of parts) { | |
| const n = parseInt(part, 16); | |
| if (isNaN(n) || n < 0 || n > 0xffff) { | |
| console.error(`Invalid IPv6 hextet: Position ${i} Value ${part} in ${ip}`); | |
| return false; | |
| } | |
| i++; | |
| } | |
| return true; | |
| } | |
| }; | |
| /** | |
| * Converts string formatted IPs to decimal representation | |
| * | |
| * @link http://javascript.about.com/library/blipconvert.htm | |
| * @param {string|number} ipString IP address in string format. If a decimal representation given, it is returned unmodified. | |
| * @return {int} Returns the IP address in decimal format | |
| * @throws {Error} Throws an error, if `ipString` does not contain an IP address. | |
| * @private | |
| */ | |
| export const toDecimal = (ipString: IPString | IPNumber): IPNumber => { | |
| if (typeof ipString === 'number' && isDecimalIp(ipString)) { | |
| return ipString; | |
| } | |
| if (typeof ipString === 'bigint') { | |
| return ipString; // Handle BigInt input for IPv6 | |
| } | |
| if (typeof ipString !== 'string' || !isIp(ipString)) { | |
| throw new Error(`Not an IP address: ${ipString}`); | |
| } | |
| if (IPType === IpType.IPv4) { | |
| const d = ipString.split('.'); | |
| if (d.length !== 4) { | |
| throw new Error(`Invalid IPv4 address: ${ipString}`); | |
| } | |
| const octets = d.map(n => parseInt(n, 10)); | |
| if (octets.some(n => isNaN(n) || n < 0 || n > 255)) { | |
| throw new Error(`Invalid IPv4 octets: ${ipString}`); | |
| } | |
| return ((octets[0] * 256 + octets[1]) * 256 + octets[2]) * 256 + octets[3]; | |
| } else { | |
| const expanded = expandIPv6(ipString); | |
| if (!expanded) { | |
| throw new Error(`Failed to expand IPv6 address: ${ipString}`); | |
| } | |
| const hextets = expanded.split(':').map(h => { | |
| const n = parseInt(h, 16); | |
| if (isNaN(n) || n < 0 || n > 0xffff) { | |
| throw new Error(`Invalid IPv6 hextet: ${h} in ${ipString}`); | |
| } | |
| return n; | |
| }); | |
| if (hextets.length !== 8) { | |
| throw new Error(`Invalid IPv6 hextet count: ${hextets.length} in ${ipString}`); | |
| } | |
| let result = BigInt(0); | |
| for (let i = 0; i < 8; i++) { | |
| result = (result << BigInt(16)) + BigInt(hextets[i]); | |
| } | |
| return result; | |
| } | |
| }; | |
| /** | |
| * Test whether number is an IP address | |
| * @param {number} ipNum | |
| * @returns {boolean} | |
| * @public | |
| */ | |
| export const isDecimalIp = (ipNum: IPNumber): boolean => { | |
| return ( | |
| typeof ipNum === 'number' && // is this a number? | |
| ipNum % 1 === 0 && // does the number have a decimal place? | |
| ipNum >= 0 && ( | |
| ipNum <= 4294967295 | |
| || ipNum <= 340282366920938463463374607431768211455n // IPv6 max | |
| ) | |
| ); | |
| }; | |
| /** | |
| * Converts decimal IPs to string representation | |
| * | |
| * @link http://javascript.about.com/library/blipconvert.htm | |
| * @param {int} ipNum IP address in decimal format. If a string representation is given, it is returned unmodified. | |
| * @return {string} Returns the IP address in string format | |
| * @throws {Error} Throws an error, if `ipNum` is out of range, not a decimal, or not a number | |
| * @private | |
| */ | |
| export const toString = (ipNum: IPNumber | IPString): IPString => { | |
| if (typeof ipNum === 'string' && isIp(ipNum)) { | |
| return ipNum; | |
| } | |
| if (IPType === IpType.IPv4) { | |
| if (typeof ipNum !== 'number' || !isDecimalIp(ipNum)) { | |
| throw new Error(`Not a numeric IP address: ${ipNum}`); | |
| } | |
| let d = `${ipNum % 256}`; | |
| let curIp = ipNum; | |
| for (let i = 3; i > 0; i--) { | |
| curIp = Math.floor(curIp / 256); | |
| d = `${curIp % 256}.${d}`; | |
| } | |
| return d; | |
| } else { | |
| if (typeof ipNum !== 'bigint') { | |
| throw new Error(`Not a numeric IP address: ${ipNum}`); | |
| } | |
| let d = ''; | |
| let curIp = ipNum; | |
| const hextets: string[] = []; | |
| for (let i = 0; i < 8; i++) { | |
| const hextet = Number(curIp & BigInt(0xffff)).toString(16).padStart(4, '0'); | |
| hextets.unshift(hextet); | |
| curIp >>= BigInt(16); | |
| } | |
| return hextets.join(':'); | |
| } | |
| }; | |
| /** | |
| * Calculates details of a CIDR subnet | |
| * | |
| * @param {int} ipNum Decimal IP address | |
| * @param {int} prefixSize Subnet mask size in bits | |
| * @return {object} Returns an object with the following fields: | |
| * | |
| * ipLow - Decimal representation of the lowest IP address in the subnet | |
| * ipLowStr - String representation of the lowest IP address in the subnet | |
| * ipHigh - Decimal representation of the highest IP address in the subnet | |
| * ipHighStr - String representation of the highest IP address in the subnet | |
| * prefixMask - Bitmask matching prefixSize | |
| * prefixMaskStr - String / IP representation of the bitmask | |
| * prefixSize - Size of the prefix | |
| * invertedMask - Bitmask matching the inverted subnet mask | |
| * invertedMaskStr - String / IP representation of the inverted mask | |
| * invertedMaskSize - Number of relevant bits in the inverted mask | |
| * @private | |
| */ | |
| export const getMaskRange = (ipNum: IPNumber, prefixSize: BitCount): SubnetAnalysis => { | |
| const prefixMask: IPNumber = getPrefixMask(prefixSize); | |
| const bitCount = IPType === IpType.IPv4 ? 32 : 128; | |
| const lowMask: IPNumber = getMask(bitCount - prefixSize); | |
| let ipLow: IPNumber; | |
| let ipHigh: IPNumber; | |
| if (IPType === IpType.IPv6) { | |
| ipLow = (BigInt(ipNum) & BigInt(prefixMask)); | |
| ipHigh = (BigInt(ipNum) & BigInt(prefixMask)) + BigInt(lowMask); | |
| } else { | |
| ipLow = ((ipNum as number) & (prefixMask as number)) >>> 0; | |
| ipHigh = (((ipNum as number) & (prefixMask as number)) + (lowMask as number)) >>> 0; | |
| } | |
| console.log("IP Low: ", ipLow.toString()); | |
| console.log("IP Low: ", toString(ipLow)); | |
| console.log("IP High: ", ipHigh.toString()); | |
| console.log("IP High: ", toString(ipHigh)); | |
| console.log("Prefix Mask: ", prefixMask.toString()); | |
| console.log("Prefix Mask: ", toString(prefixMask)); | |
| console.log("Prefix Size: ", prefixSize); | |
| console.log("Low Mask (Inverted Mask): ", lowMask.toString()); | |
| console.log("Low Mask (Inverted Mask): ", toString(lowMask)); | |
| console.log("Bit Count: ", bitCount); | |
| console.log("Inverted Mask Size: ", bitCount - prefixSize); | |
| console.log("IP Type: ", IPType); | |
| return { | |
| ipLow, | |
| ipLowStr: toString(ipLow), | |
| ipHigh, | |
| ipHighStr: toString(ipHigh), | |
| prefixMask, | |
| prefixMaskStr: toString(prefixMask), | |
| prefixSize, | |
| invertedMask: lowMask, | |
| invertedMaskStr: toString(lowMask), | |
| invertedMaskSize: bitCount - prefixSize, // Correct for IPv6 | |
| }; | |
| }; | |
| /** | |
| * Finds the largest subnet mask that begins from ipNum and does not | |
| * exceed ipEndNum. | |
| * | |
| * @param {int} ipNum IP start point (inclusive) | |
| * @param {int} ipEndNum IP end point (inclusive) | |
| * @return {object|null} Returns `null` on failure, otherwise an object with the following fields: | |
| * | |
| * ipLow - Decimal representation of the lowest IP address in the subnet | |
| * ipLowStr - String representation of the lowest IP address in the subnet | |
| * ipHigh - Decimal representation of the highest IP address in the subnet | |
| * ipHighStr - String representation of the highest IP address in the subnet | |
| * prefixMask - Bitmask matching prefixSize | |
| * prefixMaskStr - String / IP representation of the bitmask | |
| * prefixSize - Size of the prefix | |
| * invertedMask - Bitmask matching the inverted subnet mask | |
| * invertedMaskStr - String / IP representation of the inverted mask | |
| * invertedMaskSize - Number of relevant bits in the inverted mask | |
| * @private | |
| */ | |
| export const getOptimalRange = (ipNum: IPNumber, ipEndNum: IPNumber): SubnetAnalysis | null => { | |
| let prefixSize: number; | |
| let optimalRange: SubnetAnalysis | null = null; | |
| for (prefixSize = (IPType == IpType.IPv4 ? 32 : 128); prefixSize >= 0; prefixSize -= 1) { | |
| const maskRange = getMaskRange(ipNum, prefixSize); | |
| if (maskRange.ipLow === ipNum && maskRange.ipHigh <= ipEndNum) { | |
| optimalRange = maskRange; | |
| } else { | |
| break; | |
| } | |
| } | |
| return optimalRange; | |
| }; | |
| function isIPv4(ip: IPAny): boolean { | |
| if (typeof ip !== 'string') { | |
| return false; | |
| } | |
| const parts = ip.match(/^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/); | |
| if (parts === null) { | |
| return false; | |
| } | |
| for (let i = 1; i <= 4; i += 1) { | |
| const n = parseInt(parts[i], 10); | |
| if (n > 255 || n < 0) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| function isIPv6(ip: IPAny): boolean { | |
| if (typeof ip !== 'string') { | |
| return false; | |
| } | |
| const parts = ip.match(/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/); | |
| return parts !== null; | |
| } | |
| /** | |
| * Calculates an optimal set of IP masks for the given IP address range | |
| * | |
| * @param {string|number} ipStart Lowest IP in the range to be calculated | |
| * @param {string|number} ipEnd Highest IP (inclusive) in the range to be calculated | |
| * | |
| * @return The function returns `null` in case of an error. Otherwise, an array containing one or more subnet | |
| * masks is returned: | |
| * | |
| * ``` | |
| * const result = [ | |
| * { | |
| * ipLow : 2071689984, | |
| * ipLowStr : "123.123.123.0", | |
| * ipHigh : 2071690239, | |
| * ipHighStr : "123.123.123.255", | |
| * prefixMask : 4294967040, | |
| * prefixMaskStr : "255.255.255.0", | |
| * prefixSize : 24, | |
| * invertedMask : 255, | |
| * invertedMaskStr : "0.0.0.255", | |
| * invertedMaskSize : 8 | |
| * }, | |
| * | |
| * ... | |
| * ]; | |
| * ``` | |
| * @public | |
| */ | |
| export const calculate = (ipStart: IPAny, ipEnd: IPAny): SubnetAnalysis[] | null => { | |
| let ipStartNum: IPNumber; | |
| let ipEndNum: IPNumber; | |
| let ipCurNum: IPNumber; | |
| const rangeCollection: SubnetAnalysis[] = []; | |
| // Determine the IP type based on the input | |
| if (isIPv4(ipStart) && isIPv4(ipEnd)) { | |
| IPType = IpType.IPv4; | |
| } else if (isIPv6(ipStart) && isIPv6(ipEnd)) { | |
| IPType = IpType.IPv6; | |
| } else { | |
| return null; | |
| } | |
| try { | |
| ipStartNum = toDecimal(ipStart); | |
| ipEndNum = toDecimal(ipEnd); | |
| } catch (err) { | |
| return null; | |
| } | |
| if (ipEndNum < ipStartNum) { | |
| return null; | |
| } | |
| ipCurNum = ipStartNum; | |
| while (ipCurNum <= ipEndNum) { | |
| const optimalRange = getOptimalRange(ipCurNum, ipEndNum); | |
| if (optimalRange === null) { | |
| return null; | |
| } | |
| rangeCollection.push(optimalRange); | |
| ipCurNum = typeof optimalRange.ipHigh === 'bigint' ? (optimalRange.ipHigh + BigInt(1)) : (optimalRange.ipHigh + 1); | |
| } | |
| return rangeCollection; | |
| }; | |
| /** | |
| * Calculates a subnet mask from CIDR prefix. | |
| * | |
| * @param {string|number} ip IP address ("2.3.4.5") | |
| * @param {int} prefixSize Number of relevant bits in the subnet mask (24) | |
| * @return {SubnetAnalysis|null} Returns null in case of an error, and a subnet data object otherwise. | |
| * For details about the subnet data object, see documentation of | |
| * getMaskRange() | |
| * @public | |
| */ | |
| export const calculateSubnetMask = (ip: IPAny, prefixSize: BitCount): SubnetAnalysis | null => { | |
| if (typeof ip !== 'string' && typeof ip !== 'number' && typeof ip !== 'bigint') { | |
| console.error(`Invalid IP type: ${typeof ip}, value: ${ip}`); | |
| return null; | |
| } | |
| if (typeof prefixSize !== 'number' || isNaN(prefixSize) || prefixSize < 0 || prefixSize > (IPType === IpType.IPv4 ? 32 : 128)) { | |
| console.error(`Invalid prefix size: ${prefixSize}`); | |
| return null; | |
| } | |
| let ipNum: IPNumber; | |
| try { | |
| ipNum = toDecimal(ip); | |
| } catch (err) { | |
| console.error("Error converting IP to decimal: ", err); | |
| return null; | |
| } | |
| return getMaskRange(ipNum, prefixSize); | |
| }; | |
| /** | |
| * Calculates a CIDR prefix from subnet mask. | |
| * | |
| * @param {string|number} ip IP address ("2.3.4.5") | |
| * @param {string|number} subnetMask IP subnet mask ("255.255.255.0") | |
| * @return {SubnetAnalysis|null} Returns `null` in case of an error, and a subnet data object otherwise. | |
| * For details about the subnet data object, see documentation of | |
| * getMaskRange() | |
| * @public | |
| */ | |
| export const calculateCIDRPrefix = (ip: IPAny, subnetMask: IPAny): SubnetAnalysis | null => { | |
| let ipNum: IPNumber; | |
| let subnetMaskNum: IPNumber; | |
| let prefix: IPNumber = 0; | |
| let newPrefix: IPNumber = 0; | |
| let prefixSize: BitCount; | |
| try { | |
| ipNum = toDecimal(ip); | |
| subnetMaskNum = toDecimal(subnetMask); | |
| } catch (err) { | |
| return null; | |
| } | |
| for (prefixSize = 0; prefixSize < 32; prefixSize += 1) { | |
| // eslint-disable-next-line no-bitwise | |
| newPrefix = (prefix + (1 << (32 - (prefixSize + 1)))) >>> 0; | |
| // eslint-disable-next-line no-bitwise | |
| if (typeof subnetMaskNum === 'bigint' || typeof newPrefix === 'bigint') { | |
| if ((BigInt(subnetMaskNum) & BigInt(newPrefix)) !== BigInt(newPrefix)) { | |
| break; | |
| } | |
| } else { | |
| if (((subnetMaskNum as number) & (newPrefix as number)) >>> 0 !== newPrefix) { | |
| break; | |
| } | |
| } | |
| prefix = newPrefix; | |
| } | |
| return getMaskRange(ipNum, prefixSize); | |
| }; | |
| /** | |
| * Checks if two IP addresses are of the same type (IPv4 or IPv6). | |
| * | |
| * @param ip1 The first IP address. | |
| * @param ip2 The second IP address. | |
| * @returns True if both IP addresses are of the same type, false otherwise. | |
| */ | |
| export const areIpsSameType = (ip1: IPAny, ip2: IPAny): boolean => { | |
| return getIpType(String(ip1)) === getIpType(String(ip2)); | |
| } | |
| if (typeof window !== 'undefined') { | |
| // @ts-expect-error browser export | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | |
| window.IPSubnetCalculator = { | |
| calculate, | |
| calculateSubnetMask, | |
| calculateCIDRPrefix, | |
| getOptimalRange, | |
| getMaskRange, | |
| toString, | |
| toDecimal, | |
| isDecimalIp, | |
| isIp, | |
| getMask, | |
| getPrefixMask, | |
| getIpType, | |
| areIpsSameType | |
| }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment