|
@@ -3,58 +3,162 @@ interface IPRange {
|
|
|
end: bigint;
|
|
|
}
|
|
|
|
|
|
+type IPVersion = 4 | 6;
|
|
|
+
|
|
|
/**
|
|
|
- * Converts IP address string to BigInt for numerical operations
|
|
|
+ * Detects IP version from address string
|
|
|
+ */
|
|
|
+function detectIpVersion(ip: string): IPVersion {
|
|
|
+ return ip.includes(':') ? 6 : 4;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Converts IPv4 or IPv6 address string to BigInt for numerical operations
|
|
|
*/
|
|
|
function ipToBigInt(ip: string): bigint {
|
|
|
- return ip.split('.')
|
|
|
- .reduce((acc, octet) => BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(parseInt(octet))), BigInt(0));
|
|
|
+ const version = detectIpVersion(ip);
|
|
|
+
|
|
|
+ if (version === 4) {
|
|
|
+ return ip.split('.')
|
|
|
+ .reduce((acc, octet) => {
|
|
|
+ const num = parseInt(octet);
|
|
|
+ if (isNaN(num) || num < 0 || num > 255) {
|
|
|
+ throw new Error(`Invalid IPv4 octet: ${octet}`);
|
|
|
+ }
|
|
|
+ return BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(num));
|
|
|
+ }, BigInt(0));
|
|
|
+ } else {
|
|
|
+ // Handle IPv6
|
|
|
+ // Expand :: notation
|
|
|
+ let fullAddress = ip;
|
|
|
+ if (ip.includes('::')) {
|
|
|
+ const parts = ip.split('::');
|
|
|
+ if (parts.length > 2) throw new Error('Invalid IPv6 address: multiple :: found');
|
|
|
+ const missing = 8 - (parts[0].split(':').length + parts[1].split(':').length);
|
|
|
+ const padding = Array(missing).fill('0').join(':');
|
|
|
+ fullAddress = `${parts[0]}:${padding}:${parts[1]}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ return fullAddress.split(':')
|
|
|
+ .reduce((acc, hextet) => {
|
|
|
+ const num = parseInt(hextet || '0', 16);
|
|
|
+ if (isNaN(num) || num < 0 || num > 65535) {
|
|
|
+ throw new Error(`Invalid IPv6 hextet: ${hextet}`);
|
|
|
+ }
|
|
|
+ return BigInt.asUintN(128, (acc << BigInt(16)) + BigInt(num));
|
|
|
+ }, BigInt(0));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Converts BigInt to IP address string
|
|
|
*/
|
|
|
-function bigIntToIp(num: bigint): string {
|
|
|
- const octets: number[] = [];
|
|
|
- for (let i = 0; i < 4; i++) {
|
|
|
- octets.unshift(Number(num & BigInt(255)));
|
|
|
- num = num >> BigInt(8);
|
|
|
+function bigIntToIp(num: bigint, version: IPVersion): string {
|
|
|
+ if (version === 4) {
|
|
|
+ const octets: number[] = [];
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ octets.unshift(Number(num & BigInt(255)));
|
|
|
+ num = num >> BigInt(8);
|
|
|
+ }
|
|
|
+ return octets.join('.');
|
|
|
+ } else {
|
|
|
+ const hextets: string[] = [];
|
|
|
+ for (let i = 0; i < 8; i++) {
|
|
|
+ hextets.unshift(Number(num & BigInt(65535)).toString(16).padStart(4, '0'));
|
|
|
+ num = num >> BigInt(16);
|
|
|
+ }
|
|
|
+ // Compress zero sequences
|
|
|
+ let maxZeroStart = -1;
|
|
|
+ let maxZeroLength = 0;
|
|
|
+ let currentZeroStart = -1;
|
|
|
+ let currentZeroLength = 0;
|
|
|
+
|
|
|
+ for (let i = 0; i < hextets.length; i++) {
|
|
|
+ if (hextets[i] === '0000') {
|
|
|
+ if (currentZeroStart === -1) currentZeroStart = i;
|
|
|
+ currentZeroLength++;
|
|
|
+ if (currentZeroLength > maxZeroLength) {
|
|
|
+ maxZeroLength = currentZeroLength;
|
|
|
+ maxZeroStart = currentZeroStart;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ currentZeroStart = -1;
|
|
|
+ currentZeroLength = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (maxZeroLength > 1) {
|
|
|
+ hextets.splice(maxZeroStart, maxZeroLength, '');
|
|
|
+ if (maxZeroStart === 0) hextets.unshift('');
|
|
|
+ if (maxZeroStart + maxZeroLength === 8) hextets.push('');
|
|
|
+ }
|
|
|
+
|
|
|
+ return hextets.map(h => h === '0000' ? '0' : h.replace(/^0+/, '')).join(':');
|
|
|
}
|
|
|
- return octets.join('.');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Converts CIDR to IP range
|
|
|
*/
|
|
|
-function cidrToRange(cidr: string): IPRange {
|
|
|
+export function cidrToRange(cidr: string): IPRange {
|
|
|
const [ip, prefix] = cidr.split('/');
|
|
|
+ const version = detectIpVersion(ip);
|
|
|
const prefixBits = parseInt(prefix);
|
|
|
const ipBigInt = ipToBigInt(ip);
|
|
|
- const mask = BigInt.asUintN(64, (BigInt(1) << BigInt(32 - prefixBits)) - BigInt(1));
|
|
|
+
|
|
|
+ // Validate prefix length
|
|
|
+ const maxPrefix = version === 4 ? 32 : 128;
|
|
|
+ if (prefixBits < 0 || prefixBits > maxPrefix) {
|
|
|
+ throw new Error(`Invalid prefix length for IPv${version}: ${prefix}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const shiftBits = BigInt(maxPrefix - prefixBits);
|
|
|
+ const mask = BigInt.asUintN(version === 4 ? 64 : 128, (BigInt(1) << shiftBits) - BigInt(1));
|
|
|
const start = ipBigInt & ~mask;
|
|
|
const end = start | mask;
|
|
|
+
|
|
|
return { start, end };
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Finds the next available CIDR block given existing allocations
|
|
|
* @param existingCidrs Array of existing CIDR blocks
|
|
|
- * @param blockSize Desired prefix length for the new block (e.g., 24 for /24)
|
|
|
- * @param startCidr Optional CIDR to start searching from (default: "0.0.0.0/0")
|
|
|
+ * @param blockSize Desired prefix length for the new block
|
|
|
+ * @param startCidr Optional CIDR to start searching from
|
|
|
* @returns Next available CIDR block or null if none found
|
|
|
*/
|
|
|
export function findNextAvailableCidr(
|
|
|
existingCidrs: string[],
|
|
|
blockSize: number,
|
|
|
- startCidr: string = "0.0.0.0/0"
|
|
|
+ startCidr?: string
|
|
|
): string | null {
|
|
|
+
|
|
|
+ if (!startCidr && existingCidrs.length === 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If no existing CIDRs, use the IP version from startCidr
|
|
|
+ const version = startCidr
|
|
|
+ ? detectIpVersion(startCidr.split('/')[0])
|
|
|
+ : 4; // Default to IPv4 if no startCidr provided
|
|
|
+
|
|
|
+ // Use appropriate default startCidr if none provided
|
|
|
+ startCidr = startCidr || (version === 4 ? "0.0.0.0/0" : "::/0");
|
|
|
+
|
|
|
+ // If there are existing CIDRs, ensure all are same version
|
|
|
+ if (existingCidrs.length > 0 &&
|
|
|
+ existingCidrs.some(cidr => detectIpVersion(cidr.split('/')[0]) !== version)) {
|
|
|
+ throw new Error('All CIDRs must be of the same IP version');
|
|
|
+ }
|
|
|
+
|
|
|
// Convert existing CIDRs to ranges and sort them
|
|
|
const existingRanges = existingCidrs
|
|
|
.map(cidr => cidrToRange(cidr))
|
|
|
.sort((a, b) => (a.start < b.start ? -1 : 1));
|
|
|
|
|
|
// Calculate block size
|
|
|
- const blockSizeBigInt = BigInt(1) << BigInt(32 - blockSize);
|
|
|
+ const maxPrefix = version === 4 ? 32 : 128;
|
|
|
+ const blockSizeBigInt = BigInt(1) << BigInt(maxPrefix - blockSize);
|
|
|
|
|
|
// Start from the beginning of the given CIDR
|
|
|
let current = cidrToRange(startCidr).start;
|
|
@@ -63,7 +167,6 @@ export function findNextAvailableCidr(
|
|
|
// Iterate through existing ranges
|
|
|
for (let i = 0; i <= existingRanges.length; i++) {
|
|
|
const nextRange = existingRanges[i];
|
|
|
-
|
|
|
// Align current to block size
|
|
|
const alignedCurrent = current + ((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt);
|
|
|
|
|
@@ -74,7 +177,7 @@ export function findNextAvailableCidr(
|
|
|
|
|
|
// If we're at the end of existing ranges or found a gap
|
|
|
if (!nextRange || alignedCurrent + blockSizeBigInt - BigInt(1) < nextRange.start) {
|
|
|
- return `${bigIntToIp(alignedCurrent)}/${blockSize}`;
|
|
|
+ return `${bigIntToIp(alignedCurrent, version)}/${blockSize}`;
|
|
|
}
|
|
|
|
|
|
// Move current pointer to after the current range
|
|
@@ -85,12 +188,19 @@ export function findNextAvailableCidr(
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
-* Checks if a given IP address is within a CIDR range
|
|
|
-* @param ip IP address to check
|
|
|
-* @param cidr CIDR range to check against
|
|
|
-* @returns boolean indicating if IP is within the CIDR range
|
|
|
-*/
|
|
|
+ * Checks if a given IP address is within a CIDR range
|
|
|
+ * @param ip IP address to check
|
|
|
+ * @param cidr CIDR range to check against
|
|
|
+ * @returns boolean indicating if IP is within the CIDR range
|
|
|
+ */
|
|
|
export function isIpInCidr(ip: string, cidr: string): boolean {
|
|
|
+ const ipVersion = detectIpVersion(ip);
|
|
|
+ const cidrVersion = detectIpVersion(cidr.split('/')[0]);
|
|
|
+
|
|
|
+ if (ipVersion !== cidrVersion) {
|
|
|
+ throw new Error('IP address and CIDR must be of the same version');
|
|
|
+ }
|
|
|
+
|
|
|
const ipBigInt = ipToBigInt(ip);
|
|
|
const range = cidrToRange(cidr);
|
|
|
return ipBigInt >= range.start && ipBigInt <= range.end;
|