126 lines
3.5 KiB
TypeScript
126 lines
3.5 KiB
TypeScript
/**
|
|
* 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<number | PortRange>): 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<number | PortRange>): 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<number | PortRange>): 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 | PortRange>): 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<number | PortRange>): boolean {
|
|
const ranges = normalizePortSpec(portSpec);
|
|
return ranges.some(range => port >= range.from && port <= range.to);
|
|
}
|