/** * NFTables Port Specification Normalizer * * Handles normalization and validation of port specifications * for nftables rules. */ import type { PortRange } from '../models/index.js'; import { NftValidationError } from '../models/index.js'; /** * Normalizes port specifications into an array of port ranges */ export function normalizePortSpec(portSpec: number | PortRange | Array): PortRange[] { const result: PortRange[] = []; if (Array.isArray(portSpec)) { // If it's an array, process each element for (const spec of portSpec) { result.push(...normalizePortSpec(spec)); } } else if (typeof portSpec === 'number') { // Single port becomes a range with the same start and end result.push({ from: portSpec, to: portSpec }); } else { // Already a range result.push(portSpec); } return result; } /** * Validates port numbers or ranges */ export function validatePorts(port: number | PortRange | Array): void { if (Array.isArray(port)) { port.forEach(p => validatePorts(p)); return; } if (typeof port === 'number') { if (port < 1 || port > 65535) { throw new NftValidationError(`Invalid port number: ${port}`); } } else if (typeof port === 'object') { if (port.from < 1 || port.from > 65535 || port.to < 1 || port.to > 65535 || port.from > port.to) { throw new NftValidationError(`Invalid port range: ${port.from}-${port.to}`); } } } /** * Format port range for nftables rule */ export function formatPortRange(range: PortRange): string { if (range.from === range.to) { return String(range.from); } return `${range.from}-${range.to}`; } /** * Convert port spec to nftables expression */ export function portSpecToNftExpr(portSpec: number | PortRange | Array): string { const ranges = normalizePortSpec(portSpec); if (ranges.length === 1) { return formatPortRange(ranges[0]); } // Multiple ports/ranges need to use a set const ports = ranges.map(formatPortRange); return `{ ${ports.join(', ')} }`; } /** * Check if two port ranges overlap */ export function rangesOverlap(range1: PortRange, range2: PortRange): boolean { return range1.from <= range2.to && range2.from <= range1.to; } /** * Merge overlapping port ranges */ export function mergeOverlappingRanges(ranges: PortRange[]): PortRange[] { if (ranges.length <= 1) return ranges; // Sort by start port const sorted = [...ranges].sort((a, b) => a.from - b.from); const merged: PortRange[] = [sorted[0]]; for (let i = 1; i < sorted.length; i++) { const current = sorted[i]; const lastMerged = merged[merged.length - 1]; if (current.from <= lastMerged.to + 1) { // Ranges overlap or are adjacent, merge them lastMerged.to = Math.max(lastMerged.to, current.to); } else { // No overlap, add as new range merged.push(current); } } return merged; } /** * Calculate the total number of ports in a port specification */ export function countPorts(portSpec: number | PortRange | Array): number { const ranges = normalizePortSpec(portSpec); return ranges.reduce((total, range) => total + (range.to - range.from + 1), 0); } /** * Check if a port is within the given specification */ export function isPortInSpec(port: number, portSpec: number | PortRange | Array): boolean { const ranges = normalizePortSpec(portSpec); return ranges.some(range => port >= range.from && port <= range.to); }