Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 146 additions & 24 deletions lib/ip.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,46 +305,168 @@ ip.isEqual = function (a, b) {
return true;
};

/**
* Extract IPv4 address from IPv6-mapped address if applicable.
* Handles formats like:
* - ::ffff:127.0.0.1 (dot notation)
* - ::ffff:7f00:1 (hex notation)
* - 0:0:0:0:0:ffff:127.0.0.1 (expanded)
* - 0:0:0:0:0:ffff:7f00:1 (expanded hex)
* Returns null if not an IPv6-mapped IPv4 address.
* @private
*/
function _extractIPv4FromMapped(addr) {
// Match ::ffff: prefix with dot-notation IPv4
const dotMatch = addr.match(/^(?:0:){0,5}(?:::)?f{4}:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
if (dotMatch) {
return dotMatch[1];
}

// Match ::ffff: prefix with hex-notation IPv4 (e.g., ::ffff:7f00:1)
const hexMatch = addr.match(/^(?:0:){0,5}(?:::)?f{4}:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
if (hexMatch) {
const high = parseInt(hexMatch[1], 16);
const low = parseInt(hexMatch[2], 16);
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
}

return null;
}

/**
* Check if an IPv6 address is a loopback address.
* Handles various representations:
* - ::1 (compressed)
* - 0:0:0:0:0:0:0:1 (expanded)
* - 0000:0000:0000:0000:0000:0000:0000:0001 (full)
* - :: (unspecified, treated as loopback for safety)
* @private
*/
function _isIPv6Loopback(addr) {
// Normalize: remove leading zeros in each segment, lowercase
const normalized = addr.toLowerCase().split(':').map(seg => {
if (seg === '') return '';
return parseInt(seg, 16).toString(16);
}).join(':');

// Check for ::1 or expanded forms
if (normalized === '::1' || normalized === '0:0:0:0:0:0:0:1') {
return true;
}

// Check for :: (unspecified address - treat as private for safety)
if (normalized === '::' || normalized === '0:0:0:0:0:0:0:0' || addr === '::') {
return true;
}

// Handle compressed forms like 0::1, ::0:1, etc.
// Expand :: and check if result is ::1
if (addr.includes('::')) {
const parts = addr.split('::');
const left = parts[0] ? parts[0].split(':') : [];
const right = parts[1] ? parts[1].split(':') : [];
const missing = 8 - left.length - right.length;
const full = [...left, ...Array(missing).fill('0'), ...right];
const expanded = full.map(s => parseInt(s || '0', 16)).join(':');
if (expanded === '0:0:0:0:0:0:0:1' || expanded === '0:0:0:0:0:0:0:0') {
return true;
}
}

return false;
}

ip.isPrivate = function (addr) {
// check loopback addresses first
if (ip.isLoopback(addr)) {
return true;
}

// ensure the ipv4 address is valid
if (!ip.isV6Format(addr)) {
const ipl = ip.normalizeToLong(addr);
if (ipl < 0) {
throw new Error('invalid ipv4 address');
// Handle IPv6 addresses
if (ip.isV6Format(addr)) {
// Check for IPv6-mapped IPv4 addresses
const mappedIPv4 = _extractIPv4FromMapped(addr);
if (mappedIPv4) {
// Recursively check the extracted IPv4 address
return ip.isPrivate(mappedIPv4);
}
// normalize the address for the private range checks that follow
addr = ip.fromLong(ipl);
}

// check private ranges
return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
|| /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
|| /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i
.test(addr)
|| /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr)
|| /^f[cd][0-9a-f]{2}:/i.test(addr)
|| /^fe80:/i.test(addr)
|| /^::1$/.test(addr)
|| /^::$/.test(addr);

// Check IPv6 private ranges
return /^f[cd][0-9a-f]{2}:/i.test(addr) // fc00::/7 - Unique local addresses
|| /^fe80:/i.test(addr) // fe80::/10 - Link-local addresses
|| /^::1$/.test(addr) // ::1 - Loopback
|| /^::$/.test(addr) // :: - Unspecified (treat as private)
|| _isIPv6Loopback(addr); // Other loopback representations
}

// Handle IPv4 addresses (including non-standard formats)
const ipl = ip.normalizeToLong(addr);
if (ipl < 0) {
throw new Error('invalid ipv4 address');
}

// Normalize the address for the private range checks
const normalizedAddr = ip.fromLong(ipl);

// Check private IPv4 ranges:
// 0.0.0.0/8 - "This" network (null route, local)
// 10.0.0.0/8 - Private
// 127.0.0.0/8 - Loopback
// 169.254.0.0/16 - Link-local
// 172.16.0.0/12 - Private
// 192.168.0.0/16 - Private

// Check 0.0.0.0/8 (null route / "this" network)
if (/^0\./.test(normalizedAddr)) {
return true;
}

// Check standard private ranges
return /^10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(normalizedAddr)
|| /^192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/.test(normalizedAddr)
|| /^172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/.test(normalizedAddr)
|| /^169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/.test(normalizedAddr)
|| /^127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(normalizedAddr);
};

ip.isPublic = function (addr) {
return !ip.isPrivate(addr);
};

ip.isLoopback = function (addr) {
// If addr is an IPv4 address in long integer form (no dots and no colons), convert it
if (!/\./.test(addr) && !/:/.test(addr)) {
addr = ip.fromLong(Number(addr));
// Handle IPv6 addresses
if (ip.isV6Format(addr)) {
// Check for IPv6-mapped IPv4 loopback
const mappedIPv4 = _extractIPv4FromMapped(addr);
if (mappedIPv4) {
return ip.isLoopback(mappedIPv4);
}

// Check IPv6 loopback representations
return _isIPv6Loopback(addr) || /^fe80::1$/i.test(addr);
}

// Handle IPv4 addresses (including non-standard formats)
// First, try to normalize the address
const ipl = ip.normalizeToLong(addr);

if (ipl >= 0) {
// Successfully normalized - check if it's in 127.0.0.0/8
// 127.0.0.0/8 = 2130706432 to 2147483647
// (127 << 24) = 2130706432, (128 << 24) - 1 = 2147483647
if (ipl >= 2130706432 && ipl <= 2147483647) {
return true;
}
// Also check for 0.0.0.0 (sometimes used as loopback equivalent)
if (ipl === 0) {
return true;
}
return false;
}

return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/
.test(addr)
// Fallback for addresses that couldn't be normalized
// (shouldn't happen with valid addresses, but keep for safety)
return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/.test(addr)
|| /^0177\./.test(addr)
|| /^0x7f\./i.test(addr)
|| /^fe80::1$/i.test(addr)
Expand Down
Loading