Support v6

This commit is contained in:
Owen 2025-02-14 12:32:10 -05:00
parent 4c1366ef91
commit 40922fedb8
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD

View file

@ -3,24 +3,98 @@ interface IPRange {
end: bigint; 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 { function ipToBigInt(ip: string): bigint {
const version = detectIpVersion(ip);
if (version === 4) {
return ip.split('.') return ip.split('.')
.reduce((acc, octet) => BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(parseInt(octet))), BigInt(0)); .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 * Converts BigInt to IP address string
*/ */
function bigIntToIp(num: bigint): string { function bigIntToIp(num: bigint, version: IPVersion): string {
if (version === 4) {
const octets: number[] = []; const octets: number[] = [];
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
octets.unshift(Number(num & BigInt(255))); octets.unshift(Number(num & BigInt(255)));
num = num >> BigInt(8); num = num >> BigInt(8);
} }
return octets.join('.'); 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(':');
}
} }
/** /**
@ -28,33 +102,56 @@ function bigIntToIp(num: bigint): string {
*/ */
function cidrToRange(cidr: string): IPRange { function cidrToRange(cidr: string): IPRange {
const [ip, prefix] = cidr.split('/'); const [ip, prefix] = cidr.split('/');
const version = detectIpVersion(ip);
const prefixBits = parseInt(prefix); const prefixBits = parseInt(prefix);
const ipBigInt = ipToBigInt(ip); 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 start = ipBigInt & ~mask;
const end = start | mask; const end = start | mask;
return { start, end }; return { start, end };
} }
/** /**
* Finds the next available CIDR block given existing allocations * Finds the next available CIDR block given existing allocations
* @param existingCidrs Array of existing CIDR blocks * @param existingCidrs Array of existing CIDR blocks
* @param blockSize Desired prefix length for the new block (e.g., 24 for /24) * @param blockSize Desired prefix length for the new block
* @param startCidr Optional CIDR to start searching from (default: "0.0.0.0/0") * @param startCidr Optional CIDR to start searching from
* @returns Next available CIDR block or null if none found * @returns Next available CIDR block or null if none found
*/ */
export function findNextAvailableCidr( export function findNextAvailableCidr(
existingCidrs: string[], existingCidrs: string[],
blockSize: number, blockSize: number,
startCidr: string = "0.0.0.0/0" startCidr?: string
): string | null { ): string | null {
if (existingCidrs.length === 0) return null;
// Determine IP version from first CIDR
const version = detectIpVersion(existingCidrs[0].split('/')[0]);
// Use appropriate default startCidr if none provided
startCidr = startCidr || (version === 4 ? "0.0.0.0/0" : "::/0");
// Ensure all CIDRs are same version
if (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 // Convert existing CIDRs to ranges and sort them
const existingRanges = existingCidrs const existingRanges = existingCidrs
.map(cidr => cidrToRange(cidr)) .map(cidr => cidrToRange(cidr))
.sort((a, b) => (a.start < b.start ? -1 : 1)); .sort((a, b) => (a.start < b.start ? -1 : 1));
// Calculate block size // 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 // Start from the beginning of the given CIDR
let current = cidrToRange(startCidr).start; let current = cidrToRange(startCidr).start;
@ -63,7 +160,6 @@ export function findNextAvailableCidr(
// Iterate through existing ranges // Iterate through existing ranges
for (let i = 0; i <= existingRanges.length; i++) { for (let i = 0; i <= existingRanges.length; i++) {
const nextRange = existingRanges[i]; const nextRange = existingRanges[i];
// Align current to block size // Align current to block size
const alignedCurrent = current + ((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt); const alignedCurrent = current + ((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt);
@ -74,7 +170,7 @@ export function findNextAvailableCidr(
// If we're at the end of existing ranges or found a gap // If we're at the end of existing ranges or found a gap
if (!nextRange || alignedCurrent + blockSizeBigInt - BigInt(1) < nextRange.start) { 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 // Move current pointer to after the current range
@ -85,12 +181,19 @@ export function findNextAvailableCidr(
} }
/** /**
* Checks if a given IP address is within a CIDR range * Checks if a given IP address is within a CIDR range
* @param ip IP address to check * @param ip IP address to check
* @param cidr CIDR range to check against * @param cidr CIDR range to check against
* @returns boolean indicating if IP is within the CIDR range * @returns boolean indicating if IP is within the CIDR range
*/ */
export function isIpInCidr(ip: string, cidr: string): boolean { 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 ipBigInt = ipToBigInt(ip);
const range = cidrToRange(cidr); const range = cidrToRange(cidr);
return ipBigInt >= range.start && ipBigInt <= range.end; return ipBigInt >= range.start && ipBigInt <= range.end;