/** * NFTables Rule Validator * * Handles validation of settings and inputs for nftables operations. * Prevents command injection and ensures valid values. */ import type { PortRange, NfTableProxyOptions } from '../models/index.js'; import { NftValidationError } from '../models/index.js'; import { validatePorts } from './nft-port-spec-normalizer.js'; // IP address validation patterns const IPV4_REGEX = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))?$/; const IPV6_REGEX = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/; const HOSTNAME_REGEX = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; const TABLE_NAME_REGEX = /^[a-zA-Z0-9_]+$/; const RATE_REGEX = /^[0-9]+[kKmMgG]?bps$/; /** * Validates an IP address (IPv4 or IPv6) */ export function isValidIP(ip: string): boolean { return IPV4_REGEX.test(ip) || IPV6_REGEX.test(ip); } /** * Validates an IPv4 address */ export function isValidIPv4(ip: string): boolean { return IPV4_REGEX.test(ip); } /** * Validates an IPv6 address */ export function isValidIPv6(ip: string): boolean { return IPV6_REGEX.test(ip); } /** * Validates a hostname */ export function isValidHostname(hostname: string): boolean { return HOSTNAME_REGEX.test(hostname); } /** * Validates a table name for nftables */ export function isValidTableName(tableName: string): boolean { return TABLE_NAME_REGEX.test(tableName); } /** * Validates a rate specification (e.g., "10mbps") */ export function isValidRate(rate: string): boolean { return RATE_REGEX.test(rate); } /** * Validates an array of IP addresses */ export function validateIPs(ips?: string[]): void { if (!ips) return; for (const ip of ips) { if (!isValidIP(ip)) { throw new NftValidationError(`Invalid IP address format: ${ip}`); } } } /** * Validates a host (can be hostname or IP) */ export function validateHost(host?: string): void { if (!host) return; if (!isValidHostname(host) && !isValidIP(host)) { throw new NftValidationError(`Invalid host format: ${host}`); } } /** * Validates a table name */ export function validateTableName(tableName?: string): void { if (!tableName) return; if (!isValidTableName(tableName)) { throw new NftValidationError( `Invalid table name: ${tableName}. Only alphanumeric characters and underscores are allowed.` ); } } /** * Validates QoS settings */ export function validateQosSettings(qos?: NfTableProxyOptions['qos']): void { if (!qos?.enabled) return; if (qos.maxRate && !isValidRate(qos.maxRate)) { throw new NftValidationError( `Invalid rate format: ${qos.maxRate}. Use format like "10mbps", "1gbps", etc.` ); } if (qos.priority !== undefined) { if (qos.priority < 1 || qos.priority > 10 || !Number.isInteger(qos.priority)) { throw new NftValidationError( `Invalid priority: ${qos.priority}. Must be an integer between 1 and 10.` ); } } } /** * Validates all NfTablesProxy settings */ export function validateSettings(settings: NfTableProxyOptions): void { // Validate port numbers validatePorts(settings.fromPort); validatePorts(settings.toPort); // Validate IP addresses validateIPs(settings.ipAllowList); validateIPs(settings.ipBlockList); // Validate target host validateHost(settings.toHost); // Validate table name validateTableName(settings.tableName); // Validate QoS settings validateQosSettings(settings.qos); } /** * Check if an IP matches the given family (ip or ip6) */ export function isIPForFamily(ip: string, family: 'ip' | 'ip6'): boolean { if (family === 'ip6') { return ip.includes(':'); } return ip.includes('.'); } /** * Filter IPs by family */ export function filterIPsByFamily(ips: string[], family: 'ip' | 'ip6'): string[] { return ips.filter(ip => isIPForFamily(ip, family)); }