Files
devicemanager/ts/helpers/helpers.iprange.ts

173 lines
4.6 KiB
TypeScript
Raw Normal View History

2026-01-09 07:14:39 +00:00
/**
* IP Range utility functions for network scanning
*/
import * as os from 'os';
2026-01-09 07:14:39 +00:00
/**
* Validates an IPv4 address
*/
export function isValidIp(ip: string): boolean {
const parts = ip.split('.');
if (parts.length !== 4) return false;
return parts.every((part) => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
});
}
/**
* Converts an IPv4 address to a 32-bit number
*/
export function ipToNumber(ip: string): number {
const parts = ip.split('.').map((p) => parseInt(p, 10));
return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
}
/**
* Converts a 32-bit number to an IPv4 address
*/
export function numberToIp(num: number): string {
return [
(num >>> 24) & 255,
(num >>> 16) & 255,
(num >>> 8) & 255,
num & 255,
].join('.');
}
/**
* Generates an array of IP addresses from a start to end range
* Excludes network address (.0) and broadcast address (.255) for /24 ranges
*/
export function ipRangeToIps(startIp: string, endIp: string): string[] {
if (!isValidIp(startIp) || !isValidIp(endIp)) {
throw new Error(`Invalid IP address: ${!isValidIp(startIp) ? startIp : endIp}`);
}
const start = ipToNumber(startIp);
const end = ipToNumber(endIp);
if (start > end) {
throw new Error(`Start IP (${startIp}) must be less than or equal to end IP (${endIp})`);
}
const ips: string[] = [];
for (let i = start; i <= end; i++) {
ips.push(numberToIp(i));
}
return ips;
}
/**
* Parses CIDR notation and returns an array of usable host IPs
* Excludes network address and broadcast address
* Example: "192.168.1.0/24" returns 192.168.1.1 through 192.168.1.254
*/
export function cidrToIps(cidr: string): string[] {
const match = cidr.match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
if (!match) {
throw new Error(`Invalid CIDR notation: ${cidr}`);
}
const [, networkIp, prefixStr] = match;
const prefix = parseInt(prefixStr, 10);
if (!isValidIp(networkIp)) {
throw new Error(`Invalid network address: ${networkIp}`);
}
if (prefix < 0 || prefix > 32) {
throw new Error(`Invalid prefix length: ${prefix}`);
}
// For /32, just return the single IP
if (prefix === 32) {
return [networkIp];
}
// For /31 (point-to-point), return both IPs
if (prefix === 31) {
const networkNum = ipToNumber(networkIp);
const mask = (0xffffffff << (32 - prefix)) >>> 0;
const network = (networkNum & mask) >>> 0;
return [numberToIp(network), numberToIp(network + 1)];
}
// Calculate network and broadcast addresses
const networkNum = ipToNumber(networkIp);
const mask = (0xffffffff << (32 - prefix)) >>> 0;
const network = (networkNum & mask) >>> 0;
const broadcast = (network | (~mask >>> 0)) >>> 0;
// Generate usable host IPs (exclude network and broadcast)
const ips: string[] = [];
for (let i = network + 1; i < broadcast; i++) {
ips.push(numberToIp(i));
}
return ips;
}
/**
* Gets the local network interfaces and returns the first non-loopback IPv4 subnet
* Returns CIDR notation (e.g., "192.168.1.0/24")
*/
export function getLocalSubnet(): string | null {
try {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
const iface = interfaces[name];
if (!iface) continue;
for (const info of iface) {
// Skip loopback and non-IPv4
if (info.family !== 'IPv4' || info.internal) continue;
// Calculate the network address from IP and netmask
const ip = ipToNumber(info.address);
const mask = ipToNumber(info.netmask);
const network = (ip & mask) >>> 0;
// Calculate prefix length from netmask
const maskBits = info.netmask.split('.').reduce((acc: number, octet: string) => {
const byte = parseInt(octet, 10);
let bits = 0;
for (let i = 7; i >= 0; i--) {
if ((byte >> i) & 1) bits++;
else break;
}
return acc + bits;
}, 0);
return `${numberToIp(network)}/${maskBits}`;
}
}
} catch {
// Failed to get network interfaces
2026-01-09 07:14:39 +00:00
}
return null;
}
/**
* Counts the number of IPs in a CIDR range (excluding network and broadcast)
*/
export function countIpsInCidr(cidr: string): number {
const match = cidr.match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
if (!match) {
throw new Error(`Invalid CIDR notation: ${cidr}`);
}
const prefix = parseInt(match[2], 10);
if (prefix === 32) return 1;
if (prefix === 31) return 2;
// 2^(32-prefix) - 2 (minus network and broadcast)
return Math.pow(2, 32 - prefix) - 2;
}