UNPKG

ip-subnet-calculator

Version:

Calculate optimal subnet masks for standard and non-standard IP ranges

341 lines (340 loc) 10.7 kB
/** * @author Aleksi Asikainen * @link https://github.com/salieri/IPSubnetCalculator * * IpSubnetCalculator 2.0.0 * * Copyright (c) 2013-2024, Aleksi Asikainen * 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. * */ /** * 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) => { let mask = 0; let i; for (i = 0; i < prefixSize; i += 1) { // eslint-disable-next-line no-bitwise 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) => { let mask = 0; let i; for (i = 0; i < maskSize; i += 1) { // eslint-disable-next-line no-bitwise mask += (1 << i) >>> 0; } return mask; }; /** * Test whether string is an IP address * @param {string} ip * @returns {boolean} * @public */ export const isIp = (ip) => { 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; }; /** * Test whether number is an IP address * @param {number} ipNum * @returns {boolean} * @public */ export const isDecimalIp = (ipNum) => { return (typeof ipNum === 'number' && // is this a number? ipNum % 1 === 0 && // does the number have a decimal place? ipNum >= 0 && ipNum <= 4294967295); }; /** * 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) => { if (typeof ipString === 'number' && isDecimalIp(ipString)) { return ipString; } if (typeof ipString !== 'string' || !isIp(ipString)) { throw new Error(`Not an IP address: ${ipString}`); } const d = ipString.split('.'); return ((+d[0] * 256 + +d[1]) * 256 + +d[2]) * 256 + +d[3]; }; /** * 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) => { if (typeof ipNum === 'string' && isIp(ipNum)) { return ipNum; } 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 -= 1) { curIp = Math.floor(curIp / 256); d = `${curIp % 256}.${d}`; } return d; }; /** * 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, prefixSize) => { const prefixMask = getPrefixMask(prefixSize); const lowMask = getMask(32 - prefixSize); // eslint-disable-next-line no-bitwise const ipLow = (ipNum & prefixMask) >>> 0; // eslint-disable-next-line no-bitwise const ipHigh = (((ipNum & prefixMask) >>> 0) + lowMask) >>> 0; return { ipLow, ipLowStr: toString(ipLow), ipHigh, ipHighStr: toString(ipHigh), prefixMask, prefixMaskStr: toString(prefixMask), prefixSize, invertedMask: lowMask, invertedMaskStr: toString(lowMask), invertedMaskSize: 32 - prefixSize, }; }; /** * 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, ipEndNum) => { let prefixSize; let optimalRange = null; for (prefixSize = 32; prefixSize >= 0; prefixSize -= 1) { const maskRange = getMaskRange(ipNum, prefixSize); if (maskRange.ipLow === ipNum && maskRange.ipHigh <= ipEndNum) { optimalRange = maskRange; } else { break; } } return optimalRange; }; /** * 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, ipEnd) => { let ipStartNum; let ipEndNum; let ipCurNum; const rangeCollection = []; 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 = 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, prefixSize) => { let ipNum; try { ipNum = toDecimal(ip); } catch (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, subnetMask) => { let ipNum; let subnetMaskNum; let prefix = 0; let newPrefix = 0; let prefixSize; 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 ((subnetMaskNum & newPrefix) >>> 0 !== newPrefix) { break; } prefix = newPrefix; } return getMaskRange(ipNum, prefixSize); }; // @ts-expect-error browser export 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, }; }