Files
smartproxy/ts/core/utils/ip-utils.ts

303 lines
9.5 KiB
TypeScript

import * as plugins from '../../plugins.js';
/**
* Utility class for IP address operations
*/
export class IpUtils {
/**
* Check if the IP matches any of the glob patterns
*
* This method checks IP addresses against glob patterns and handles IPv4/IPv6 normalization.
* It's used to implement IP filtering based on security configurations.
*
* @param ip - The IP address to check
* @param patterns - Array of glob patterns
* @returns true if IP matches any pattern, false otherwise
*/
public static isGlobIPMatch(ip: string, patterns: string[]): boolean {
if (!ip || !patterns || patterns.length === 0) return false;
// Normalize the IP being checked
const normalizedIPVariants = this.normalizeIP(ip);
if (normalizedIPVariants.length === 0) return false;
// Check each pattern
for (const pattern of patterns) {
// Handle CIDR notation
if (pattern.includes('/')) {
if (this.matchCIDR(ip, pattern)) {
return true;
}
continue;
}
// Handle range notation
if (pattern.includes('-') && !pattern.includes('*')) {
if (this.matchIPRange(ip, pattern)) {
return true;
}
continue;
}
// Expand shorthand patterns for glob matching
let expandedPattern = pattern;
if (pattern.includes('*') && !pattern.includes(':')) {
const parts = pattern.split('.');
while (parts.length < 4) {
parts.push('*');
}
expandedPattern = parts.join('.');
}
// Normalize and check with minimatch
const normalizedPatterns = this.normalizeIP(expandedPattern);
for (const ipVariant of normalizedIPVariants) {
for (const normalizedPattern of normalizedPatterns) {
if (plugins.minimatch(ipVariant, normalizedPattern)) {
return true;
}
}
}
}
return false;
}
/**
* Normalize IP addresses for consistent comparison
*
* @param ip The IP address to normalize
* @returns Array of normalized IP forms
*/
public static normalizeIP(ip: string): string[] {
if (!ip) return [];
// Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1)
if (ip.startsWith('::ffff:')) {
const ipv4 = ip.slice(7);
return [ip, ipv4];
}
// Handle IPv4 addresses by also checking IPv4-mapped form
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
return [ip, `::ffff:${ip}`];
}
return [ip];
}
/**
* Check if an IP is authorized using security rules
*
* @param ip - The IP address to check
* @param allowedIPs - Array of allowed IP patterns
* @param blockedIPs - Array of blocked IP patterns
* @returns true if IP is authorized, false if blocked
*/
public static isIPAuthorized(ip: string, allowedIPs: string[] = [], blockedIPs: string[] = []): boolean {
// Skip IP validation if no rules are defined
if (!ip || (allowedIPs.length === 0 && blockedIPs.length === 0)) {
return true;
}
// First check if IP is blocked - blocked IPs take precedence
if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) {
return false;
}
// Then check if IP is allowed (if no allowed IPs are specified, all non-blocked IPs are allowed)
return allowedIPs.length === 0 || this.isGlobIPMatch(ip, allowedIPs);
}
/**
* Check if an IP address is a private network address
*
* @param ip The IP address to check
* @returns true if the IP is a private network address, false otherwise
*/
public static isPrivateIP(ip: string): boolean {
if (!ip) return false;
// Handle IPv4-mapped IPv6 addresses
if (ip.startsWith('::ffff:')) {
ip = ip.slice(7);
}
// Check IPv4 private ranges
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
const parts = ip.split('.').map(Number);
// Check common private ranges
// 10.0.0.0/8
if (parts[0] === 10) return true;
// 172.16.0.0/12
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
// 192.168.0.0/16
if (parts[0] === 192 && parts[1] === 168) return true;
// 127.0.0.0/8 (localhost)
if (parts[0] === 127) return true;
return false;
}
// IPv6 local addresses
return ip === '::1' || ip.startsWith('fc00:') || ip.startsWith('fd00:') || ip.startsWith('fe80:');
}
/**
* Check if an IP address is a public network address
*
* @param ip The IP address to check
* @returns true if the IP is a public network address, false otherwise
*/
public static isPublicIP(ip: string): boolean {
return !this.isPrivateIP(ip);
}
/**
* Check if an IP matches a CIDR notation
*
* @param ip The IP address to check
* @param cidr The CIDR notation (e.g., "192.168.1.0/24")
* @returns true if IP is within the CIDR range
*/
private static matchCIDR(ip: string, cidr: string): boolean {
if (!cidr.includes('/')) return false;
const [networkAddr, prefixStr] = cidr.split('/');
const prefix = parseInt(prefixStr, 10);
// Handle IPv4-mapped IPv6 in the IP being checked
let checkIP = ip;
if (checkIP.startsWith('::ffff:')) {
checkIP = checkIP.slice(7);
}
// Handle IPv6 CIDR
if (networkAddr.includes(':')) {
// TODO: Implement IPv6 CIDR matching
return false;
}
// IPv4 CIDR matching
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(checkIP)) return false;
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(networkAddr)) return false;
if (isNaN(prefix) || prefix < 0 || prefix > 32) return false;
const ipParts = checkIP.split('.').map(Number);
const netParts = networkAddr.split('.').map(Number);
// Validate IP parts
for (const part of [...ipParts, ...netParts]) {
if (part < 0 || part > 255) return false;
}
// Convert to 32-bit integers
const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
const netNum = (netParts[0] << 24) | (netParts[1] << 16) | (netParts[2] << 8) | netParts[3];
// Create mask
const mask = (-1 << (32 - prefix)) >>> 0;
// Check if IP is in network range
return (ipNum & mask) === (netNum & mask);
}
/**
* Check if an IP matches a range notation
*
* @param ip The IP address to check
* @param range The range notation (e.g., "192.168.1.1-192.168.1.100")
* @returns true if IP is within the range
*/
private static matchIPRange(ip: string, range: string): boolean {
if (!range.includes('-')) return false;
const [startIP, endIP] = range.split('-').map(s => s.trim());
// Handle IPv4-mapped IPv6 in the IP being checked
let checkIP = ip;
if (checkIP.startsWith('::ffff:')) {
checkIP = checkIP.slice(7);
}
// Only handle IPv4 for now
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(checkIP)) return false;
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(startIP)) return false;
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(endIP)) return false;
const ipParts = checkIP.split('.').map(Number);
const startParts = startIP.split('.').map(Number);
const endParts = endIP.split('.').map(Number);
// Validate parts
for (const part of [...ipParts, ...startParts, ...endParts]) {
if (part < 0 || part > 255) return false;
}
// Convert to 32-bit integers for comparison
const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
const startNum = (startParts[0] << 24) | (startParts[1] << 16) | (startParts[2] << 8) | startParts[3];
const endNum = (endParts[0] << 24) | (endParts[1] << 16) | (endParts[2] << 8) | endParts[3];
// Convert to unsigned for proper comparison
const ipUnsigned = ipNum >>> 0;
const startUnsigned = startNum >>> 0;
const endUnsigned = endNum >>> 0;
return ipUnsigned >= startUnsigned && ipUnsigned <= endUnsigned;
}
/**
* Convert a subnet CIDR to an IP range for filtering
*
* @param cidr The CIDR notation (e.g., "192.168.1.0/24")
* @returns Array of glob patterns that match the CIDR range
*/
public static cidrToGlobPatterns(cidr: string): string[] {
if (!cidr || !cidr.includes('/')) return [];
const [ipPart, prefixPart] = cidr.split('/');
const prefix = parseInt(prefixPart, 10);
if (isNaN(prefix) || prefix < 0 || prefix > 32) return [];
// For IPv4 only for now
if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(ipPart)) return [];
const ipParts = ipPart.split('.').map(Number);
const fullMask = Math.pow(2, 32 - prefix) - 1;
// Convert IP to a numeric value
const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
// Calculate network address (IP & ~fullMask)
const networkNum = ipNum & ~fullMask;
// For large ranges, return wildcard patterns
if (prefix <= 8) {
return [`${(networkNum >>> 24) & 255}.*.*.*`];
} else if (prefix <= 16) {
return [`${(networkNum >>> 24) & 255}.${(networkNum >>> 16) & 255}.*.*`];
} else if (prefix <= 24) {
return [`${(networkNum >>> 24) & 255}.${(networkNum >>> 16) & 255}.${(networkNum >>> 8) & 255}.*`];
}
// For small ranges, create individual IP patterns
const patterns = [];
const maxAddresses = Math.min(256, Math.pow(2, 32 - prefix));
for (let i = 0; i < maxAddresses; i++) {
const currentIpNum = networkNum + i;
patterns.push(
`${(currentIpNum >>> 24) & 255}.${(currentIpNum >>> 16) & 255}.${(currentIpNum >>> 8) & 255}.${currentIpNum & 255}`
);
}
return patterns;
}
}