Skip to content

Instantly share code, notes, and snippets.

@Blizzardo1
Created September 6, 2025 22:07
Show Gist options
  • Select an option

  • Save Blizzardo1/bd91cda40a8bb475a91af85c2bae7a52 to your computer and use it in GitHub Desktop.

Select an option

Save Blizzardo1/bd91cda40a8bb475a91af85c2bae7a52 to your computer and use it in GitHub Desktop.
/**
* @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