import * as plugins from './smartnetwork.plugins.js'; import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js'; import { PublicIp } from './smartnetwork.classes.publicip.js'; import { IpIntelligence, type IIpIntelligenceResult } from './smartnetwork.classes.ipintelligence.js'; import { getLogger } from './logging.js'; import { NetworkError } from './errors.js'; import { RustNetworkBridge } from './smartnetwork.classes.rustbridge.js'; /** * Configuration options for SmartNetwork */ export interface SmartNetworkOptions { /** Cache time-to-live in milliseconds for gateway and public IP lookups */ cacheTtl?: number; } /** * A hop in a traceroute result */ export interface Hop { ttl: number; ip: string; rtt: number | null; } /** * Options for the findFreePort method */ export interface IFindFreePortOptions { /** If true, selects a random available port within the range instead of the first one */ randomize?: boolean; /** Array of port numbers to exclude from the search */ exclude?: number[]; } /** * SmartNetwork simplifies actions within the network. * Uses a Rust binary for system-dependent operations (ping, traceroute, port scanning, gateway detection). */ export class SmartNetwork { /** Static registry for external plugins */ public static pluginsRegistry: Map = new Map(); /** Register a plugin by name */ public static registerPlugin(name: string, ctor: any): void { SmartNetwork.pluginsRegistry.set(name, ctor); } /** Unregister a plugin by name */ public static unregisterPlugin(name: string): void { SmartNetwork.pluginsRegistry.delete(name); } private options: SmartNetworkOptions; private cache: Map; private rustBridge: RustNetworkBridge; private bridgeStarted = false; private ipIntelligence: IpIntelligence | null = null; constructor(options?: SmartNetworkOptions) { this.options = options || {}; this.cache = new Map(); this.rustBridge = RustNetworkBridge.getInstance(); } /** * Start the Rust binary bridge. Must be called before using ping, traceroute, * port scanning, or gateway operations. Safe to call multiple times. */ public async start(): Promise { if (!this.bridgeStarted) { await this.rustBridge.start(); this.bridgeStarted = true; } } /** * Stop the Rust binary bridge. */ public async stop(): Promise { if (this.bridgeStarted) { await this.rustBridge.stop(); this.bridgeStarted = false; } } /** * Ensure the Rust bridge is running before sending commands. */ private async ensureBridge(): Promise { if (!this.bridgeStarted) { await this.start(); } } /** * Get network speed via Cloudflare speed test (pure TS, no Rust needed). */ public async getSpeed(opts?: { parallelStreams?: number; duration?: number }) { const cloudflareSpeedInstance = new CloudflareSpeed(opts); return cloudflareSpeedInstance.speedTest(); } /** * Send ICMP pings to a host. Optionally specify count for multiple pings. */ public async ping(host: string, opts?: { timeout?: number; count?: number }): Promise { await this.ensureBridge(); const timeoutMs = opts?.timeout ?? 5000; const count = opts?.count && opts.count > 1 ? opts.count : 1; let result: Awaited>; try { result = await this.rustBridge.ping(host, count, timeoutMs); } catch { // DNS resolution failure or other error — return dead ping if (count === 1) { return { alive: false, time: NaN }; } return { host, count, times: Array(count).fill(NaN), min: NaN, max: NaN, avg: NaN, stddev: NaN, packetLoss: 100, alive: false, }; } // Map times: replace null with NaN for backward compatibility const times = result.times.map((t) => (t === null ? NaN : t)); const min = result.min === null ? NaN : result.min; const max = result.max === null ? NaN : result.max; const avg = result.avg === null ? NaN : result.avg; const stddev = result.stddev === null ? NaN : result.stddev; if (count === 1) { return { alive: result.alive, time: times[0] ?? NaN, }; } return { host, count, times, min, max, avg, stddev, packetLoss: result.packetLoss, alive: result.alive, }; } /** * Check if a local port is unused (both IPv4 and IPv6) */ public async isLocalPortUnused(port: number): Promise { await this.ensureBridge(); const result = await this.rustBridge.isLocalPortFree(port); return result.free; } /** * Find the first available port within a given range */ public async findFreePort(startPort: number, endPort: number, options?: IFindFreePortOptions): Promise { // Validate port range if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { throw new NetworkError('Port numbers must be between 1 and 65535', 'EINVAL'); } if (startPort > endPort) { throw new NetworkError('Start port must be less than or equal to end port', 'EINVAL'); } const excludedPorts = new Set(options?.exclude || []); if (options?.randomize) { const availablePorts: number[] = []; for (let port = startPort; port <= endPort; port++) { if (excludedPorts.has(port)) continue; const isUnused = await this.isLocalPortUnused(port); if (isUnused) { availablePorts.push(port); } } if (availablePorts.length > 0) { const randomIndex = Math.floor(Math.random() * availablePorts.length); return availablePorts[randomIndex]; } return null; } else { for (let port = startPort; port <= endPort; port++) { if (excludedPorts.has(port)) continue; const isUnused = await this.isLocalPortUnused(port); if (isUnused) { return port; } } return null; } } /** * Check if a remote port is available */ public async isRemotePortAvailable( target: string, portOrOpts?: | number | { port?: number; protocol?: 'tcp' | 'udp'; timeout?: number; retries?: number }, ): Promise { let hostPart: string; let port: number | undefined; let protocol: string = 'tcp'; let retries = 1; let timeout: number | undefined; if (typeof portOrOpts === 'number') { [hostPart] = target.split(':'); port = portOrOpts; } else { const opts = portOrOpts || {}; protocol = opts.protocol ?? 'tcp'; retries = opts.retries ?? 1; timeout = opts.timeout; [hostPart] = target.split(':'); const portPart = target.split(':')[1]; port = opts.port ?? (portPart ? parseInt(portPart, 10) : undefined); } if (protocol === 'udp') { throw new NetworkError('UDP port check not supported', 'ENOTSUP'); } if (!port) { throw new NetworkError('Port not specified', 'EINVAL'); } await this.ensureBridge(); let last = false; for (let attempt = 0; attempt < retries; attempt++) { try { const result = await this.rustBridge.tcpPortCheck(hostPart, port, timeout); last = result.isOpen; if (last) return true; } catch { // DNS resolution failure or connection error — treat as not available last = false; } } return last; } /** * List network interfaces (gateways) — pure TS, no Rust needed. */ public async getGateways(): Promise> { const fetcher = async () => plugins.os.networkInterfaces() as Record; if (this.options.cacheTtl && this.options.cacheTtl > 0) { return this.getCached('gateways', fetcher); } return fetcher(); } /** * Get the default gateway interface and its addresses. */ public async getDefaultGateway(): Promise<{ ipv4: plugins.os.NetworkInterfaceInfo; ipv6: plugins.os.NetworkInterfaceInfo; } | null> { await this.ensureBridge(); const result = await this.rustBridge.defaultGateway(); const interfaceName = result.interfaceName; if (!interfaceName) { getLogger().warn?.('Cannot determine default gateway'); return null; } // Use os.networkInterfaces() to get rich interface info const gateways = await this.getGateways(); const defaultGateway = gateways[interfaceName]; if (!defaultGateway) { getLogger().warn?.(`Interface ${interfaceName} not found in os.networkInterfaces()`); return null; } return { ipv4: defaultGateway[0], ipv6: defaultGateway[1], }; } /** * Lookup public IPv4 and IPv6 — pure TS, no Rust needed. */ public async getPublicIps(): Promise<{ v4: string | null; v6: string | null }> { const fetcher = async () => { const publicIp = new PublicIp({ timeout: 1000 }); return publicIp.getPublicIps(); }; if (this.options.cacheTtl && this.options.cacheTtl > 0) { return this.getCached('publicIps', fetcher); } return fetcher(); } /** * Resolve DNS records (A, AAAA, MX) — uses smartdns, no Rust needed. */ public async resolveDns( host: string, ): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> { try { const dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({ strategy: 'prefer-system', allowDohFallback: true, }); const [aRecords, aaaaRecords, mxRecords] = await Promise.all([ dnsClient.getRecordsA(host).catch((): any[] => []), dnsClient.getRecordsAAAA(host).catch((): any[] => []), dnsClient.getRecords(host, 'MX').catch((): any[] => []), ]); const A = aRecords.map((record: any) => record.value); const AAAA = aaaaRecords.map((record: any) => record.value); const MX = mxRecords.map((record: any) => { const parts = record.value.split(' '); return { priority: parseInt(parts[0], 10), exchange: parts[1] || '', }; }); return { A, AAAA, MX }; } catch (err: any) { throw new NetworkError(err.message, err.code); } } /** * Perform a simple HTTP/HTTPS endpoint health check — pure TS. */ public async checkEndpoint( urlString: string, opts?: { timeout?: number; rejectUnauthorized?: boolean }, ): Promise<{ status: number; headers: Record; rtt: number }> { const start = plugins.perfHooks.performance.now(); try { const url = new URL(urlString); const lib = url.protocol === 'https:' ? plugins.https : await import('node:http'); return new Promise((resolve, reject) => { const req = lib.request( url, { method: 'GET', timeout: opts?.timeout, agent: false, rejectUnauthorized: opts?.rejectUnauthorized ?? true }, (res: any) => { res.on('data', () => {}); res.once('end', () => { const rtt = plugins.perfHooks.performance.now() - start; const headers: Record = {}; for (const [k, v] of Object.entries(res.headers)) { headers[k] = Array.isArray(v) ? v.join(',') : String(v); } resolve({ status: res.statusCode, headers, rtt }); }); }, ); req.on('error', (err: any) => reject(new NetworkError(err.message, err.code))); req.end(); }); } catch (err: any) { throw new NetworkError(err.message, err.code); } } /** * Perform a traceroute: hop-by-hop latency using the Rust binary. */ public async traceroute( host: string, opts?: { maxHops?: number; timeout?: number }, ): Promise { await this.ensureBridge(); const maxHops = opts?.maxHops ?? 30; const timeoutMs = opts?.timeout ?? 5000; const result = await this.rustBridge.traceroute(host, maxHops, timeoutMs); return result.hops.map((h) => ({ ttl: h.ttl, ip: h.ip || '*', rtt: h.rtt, })); } /** * Get IP intelligence: ASN, organization, geolocation, and RDAP registration data. * Combines RDAP (RIRs), Team Cymru DNS, and MaxMind GeoLite2 — all run in parallel. * Pure TS, no Rust needed. */ public async getIpIntelligence(ip: string): Promise { if (!this.ipIntelligence) { this.ipIntelligence = new IpIntelligence(); } const fetcher = () => this.ipIntelligence!.getIntelligence(ip); if (this.options.cacheTtl && this.options.cacheTtl > 0) { return this.getCached(`ipIntelligence:${ip}`, fetcher); } return fetcher(); } /** * Internal caching helper */ private async getCached(key: string, fetcher: () => Promise): Promise { const now = Date.now(); const entry = this.cache.get(key); if (entry && entry.expiry > now) { return entry.value; } const value = await fetcher(); const ttl = this.options.cacheTtl || 0; this.cache.set(key, { value, expiry: now + ttl }); return value; } }