157 lines
4.5 KiB
TypeScript
157 lines
4.5 KiB
TypeScript
|
|
/**
|
||
|
|
* 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));
|
||
|
|
}
|