feat(nftables):add nftables support for nftables
This commit is contained in:
		| @@ -209,18 +209,18 @@ export function matchIpPattern(pattern: string, ip: string): boolean { | ||||
|  * Match an IP against allowed and blocked IP patterns | ||||
|  *  | ||||
|  * @param ip IP to check | ||||
|  * @param allowedIps Array of allowed IP patterns | ||||
|  * @param blockedIps Array of blocked IP patterns | ||||
|  * @param ipAllowList Array of allowed IP patterns | ||||
|  * @param ipBlockList Array of blocked IP patterns | ||||
|  * @returns Whether the IP is allowed | ||||
|  */ | ||||
| export function isIpAuthorized( | ||||
|   ip: string,  | ||||
|   allowedIps: string[] = ['*'],  | ||||
|   blockedIps: string[] = [] | ||||
|   ipAllowList: string[] = ['*'],  | ||||
|   ipBlockList: string[] = [] | ||||
| ): boolean { | ||||
|   // Check blocked IPs first | ||||
|   if (blockedIps.length > 0) { | ||||
|     for (const pattern of blockedIps) { | ||||
|   if (ipBlockList.length > 0) { | ||||
|     for (const pattern of ipBlockList) { | ||||
|       if (matchIpPattern(pattern, ip)) { | ||||
|         return false; // IP is blocked | ||||
|       } | ||||
| @@ -228,13 +228,13 @@ export function isIpAuthorized( | ||||
|   } | ||||
|    | ||||
|   // If there are allowed IPs, check them | ||||
|   if (allowedIps.length > 0) { | ||||
|   if (ipAllowList.length > 0) { | ||||
|     // Special case: if '*' is in allowed IPs, all non-blocked IPs are allowed | ||||
|     if (allowedIps.includes('*')) { | ||||
|     if (ipAllowList.includes('*')) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     for (const pattern of allowedIps) { | ||||
|     for (const pattern of ipAllowList) { | ||||
|       if (matchIpPattern(pattern, ip)) { | ||||
|         return true; // IP is allowed | ||||
|       } | ||||
|   | ||||
| @@ -31,8 +31,8 @@ export interface NfTableProxyOptions { | ||||
|   logFormat?: 'plain' | 'json'; // Format for logs | ||||
|    | ||||
|   // Source filtering | ||||
|   allowedSourceIPs?: string[]; // If provided, only these IPs are allowed | ||||
|   bannedSourceIPs?: string[];  // If provided, these IPs are blocked | ||||
|   ipAllowList?: string[]; // If provided, only these IPs are allowed | ||||
|   ipBlockList?: string[];  // If provided, these IPs are blocked | ||||
|   useIPSets?: boolean;        // Use nftables sets for efficient IP management | ||||
|    | ||||
|   // Rule management | ||||
|   | ||||
| @@ -134,8 +134,8 @@ export class NfTablesProxy { | ||||
|       } | ||||
|     }; | ||||
|      | ||||
|     validateIPs(settings.allowedSourceIPs); | ||||
|     validateIPs(settings.bannedSourceIPs); | ||||
|     validateIPs(settings.ipAllowList); | ||||
|     validateIPs(settings.ipBlockList); | ||||
|      | ||||
|     // Validate toHost - only allow hostnames or IPs | ||||
|     if (settings.toHost) { | ||||
| @@ -426,7 +426,7 @@ export class NfTablesProxy { | ||||
|    * Adds source IP filtering rules, potentially using IP sets for efficiency | ||||
|    */ | ||||
|   private async addSourceIPFilters(isIpv6: boolean = false): Promise<boolean> { | ||||
|     if (!this.settings.allowedSourceIPs && !this.settings.bannedSourceIPs) { | ||||
|     if (!this.settings.ipAllowList && !this.settings.ipBlockList) { | ||||
|       return true; // Nothing to do | ||||
|     } | ||||
|      | ||||
| @@ -441,9 +441,9 @@ export class NfTablesProxy { | ||||
|       // Using IP sets for more efficient rule processing with large IP lists | ||||
|       if (this.settings.useIPSets) { | ||||
|         // Create sets for banned and allowed IPs if needed | ||||
|         if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) { | ||||
|         if (this.settings.ipBlockList && this.settings.ipBlockList.length > 0) { | ||||
|           const setName = 'banned_ips'; | ||||
|           await this.createIPSet(family, setName, this.settings.bannedSourceIPs, setType as any); | ||||
|           await this.createIPSet(family, setName, this.settings.ipBlockList, setType as any); | ||||
|            | ||||
|           // Add rule to drop traffic from banned IPs | ||||
|           const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr @${setName} drop comment "${this.ruleTag}:BANNED_SET"`; | ||||
| @@ -458,9 +458,9 @@ export class NfTablesProxy { | ||||
|           }); | ||||
|         } | ||||
|          | ||||
|         if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) { | ||||
|         if (this.settings.ipAllowList && this.settings.ipAllowList.length > 0) { | ||||
|           const setName = 'allowed_ips'; | ||||
|           await this.createIPSet(family, setName, this.settings.allowedSourceIPs, setType as any); | ||||
|           await this.createIPSet(family, setName, this.settings.ipAllowList, setType as any); | ||||
|            | ||||
|           // Add rule to allow traffic from allowed IPs | ||||
|           const rule = `add rule ${family} ${this.tableName} ${chain} ip${isIpv6 ? '6' : ''} saddr @${setName} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED_SET"`; | ||||
| @@ -490,8 +490,8 @@ export class NfTablesProxy { | ||||
|         // Traditional approach without IP sets - less efficient for large IP lists | ||||
|          | ||||
|         // Ban specific IPs first | ||||
|         if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) { | ||||
|           for (const ip of this.settings.bannedSourceIPs) { | ||||
|         if (this.settings.ipBlockList && this.settings.ipBlockList.length > 0) { | ||||
|           for (const ip of this.settings.ipBlockList) { | ||||
|             // Skip IPv4 addresses for IPv6 rules and vice versa | ||||
|             if (isIpv6 && ip.includes('.')) continue; | ||||
|             if (!isIpv6 && ip.includes(':')) continue; | ||||
| @@ -510,9 +510,9 @@ export class NfTablesProxy { | ||||
|         } | ||||
|          | ||||
|         // Allow specific IPs | ||||
|         if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) { | ||||
|         if (this.settings.ipAllowList && this.settings.ipAllowList.length > 0) { | ||||
|           // Add rules to allow specific IPs | ||||
|           for (const ip of this.settings.allowedSourceIPs) { | ||||
|           for (const ip of this.settings.ipAllowList) { | ||||
|             // Skip IPv4 addresses for IPv6 rules and vice versa | ||||
|             if (isIpv6 && ip.includes('.')) continue; | ||||
|             if (!isIpv6 && ip.includes(':')) continue; | ||||
| @@ -1398,28 +1398,28 @@ export class NfTablesProxy { | ||||
|      | ||||
|     // Source IP filters | ||||
|     if (this.settings.useIPSets) { | ||||
|       if (this.settings.bannedSourceIPs?.length) { | ||||
|       if (this.settings.ipBlockList?.length) { | ||||
|         commands.push(`add set ip ${this.tableName} banned_ips { type ipv4_addr; }`); | ||||
|         commands.push(`add element ip ${this.tableName} banned_ips { ${this.settings.bannedSourceIPs.join(', ')} }`); | ||||
|         commands.push(`add element ip ${this.tableName} banned_ips { ${this.settings.ipBlockList.join(', ')} }`); | ||||
|         commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr @banned_ips drop comment "${this.ruleTag}:BANNED_SET"`); | ||||
|       } | ||||
|        | ||||
|       if (this.settings.allowedSourceIPs?.length) { | ||||
|       if (this.settings.ipAllowList?.length) { | ||||
|         commands.push(`add set ip ${this.tableName} allowed_ips { type ipv4_addr; }`); | ||||
|         commands.push(`add element ip ${this.tableName} allowed_ips { ${this.settings.allowedSourceIPs.join(', ')} }`); | ||||
|         commands.push(`add element ip ${this.tableName} allowed_ips { ${this.settings.ipAllowList.join(', ')} }`); | ||||
|         commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr @allowed_ips ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED_SET"`); | ||||
|         commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`); | ||||
|       } | ||||
|     } else if (this.settings.bannedSourceIPs?.length || this.settings.allowedSourceIPs?.length) { | ||||
|     } else if (this.settings.ipBlockList?.length || this.settings.ipAllowList?.length) { | ||||
|       // Traditional approach without IP sets | ||||
|       if (this.settings.bannedSourceIPs?.length) { | ||||
|         for (const ip of this.settings.bannedSourceIPs) { | ||||
|       if (this.settings.ipBlockList?.length) { | ||||
|         for (const ip of this.settings.ipBlockList) { | ||||
|           commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr ${ip} drop comment "${this.ruleTag}:BANNED"`); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       if (this.settings.allowedSourceIPs?.length) { | ||||
|         for (const ip of this.settings.allowedSourceIPs) { | ||||
|       if (this.settings.ipAllowList?.length) { | ||||
|         for (const ip of this.settings.ipAllowList) { | ||||
|           commands.push(`add rule ip ${this.tableName} nat_prerouting ip saddr ${ip} ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} accept comment "${this.ruleTag}:ALLOWED"`); | ||||
|         } | ||||
|         commands.push(`add rule ip ${this.tableName} nat_prerouting ${this.settings.protocol} dport {${this.getAllPorts(this.settings.fromPort)}} drop comment "${this.ruleTag}:DENY_ALL"`); | ||||
|   | ||||
| @@ -19,6 +19,7 @@ export { NetworkProxyBridge } from './network-proxy-bridge.js'; | ||||
| // Export route-based components | ||||
| export { RouteManager } from './route-manager.js'; | ||||
| export { RouteConnectionHandler } from './route-connection-handler.js'; | ||||
| export { NFTablesManager } from './nftables-manager.js'; | ||||
|  | ||||
| // Export all helper functions from the utils directory | ||||
| export * from './utils/index.js'; | ||||
|   | ||||
| @@ -142,4 +142,7 @@ export interface IConnectionRecord { | ||||
|   // Browser connection tracking | ||||
|   isBrowserConnection?: boolean; // Whether this connection appears to be from a browser | ||||
|   domainSwitches?: number; // Number of times the domain has been switched on this connection | ||||
|    | ||||
|   // NFTables tracking | ||||
|   nftablesHandled?: boolean; // Whether this connection is being handled by NFTables at kernel level | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js'; | ||||
| import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; | ||||
| import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js'; | ||||
|  | ||||
| /** | ||||
|  * Supported action types for route configurations | ||||
| @@ -259,6 +260,12 @@ export interface IRouteAction { | ||||
|     backendProtocol?: 'http1' | 'http2'; | ||||
|     [key: string]: any; | ||||
|   }; | ||||
|  | ||||
|   // Forwarding engine specification | ||||
|   forwardingEngine?: 'node' | 'nftables'; | ||||
|  | ||||
|   // NFTables-specific options | ||||
|   nftables?: INfTablesOptions; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -275,6 +282,19 @@ export interface IRouteRateLimit { | ||||
|  | ||||
| // IRouteSecurity is defined above - unified definition is used for all routes | ||||
|  | ||||
| /** | ||||
|  * NFTables-specific configuration options | ||||
|  */ | ||||
| export interface INfTablesOptions { | ||||
|   preserveSourceIP?: boolean;     // Preserve original source IP address | ||||
|   protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward | ||||
|   maxRate?: string;               // QoS rate limiting (e.g. "10mbps") | ||||
|   priority?: number;              // QoS priority (1-10, lower is higher priority) | ||||
|   tableName?: string;             // Optional custom table name | ||||
|   useIPSets?: boolean;            // Use IP sets for performance | ||||
|   useAdvancedNAT?: boolean;       // Use connection tracking for stateful NAT | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * CORS configuration for a route | ||||
|  */ | ||||
|   | ||||
							
								
								
									
										268
									
								
								ts/proxies/smart-proxy/nftables-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								ts/proxies/smart-proxy/nftables-manager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { NfTablesProxy } from '../nftables-proxy/nftables-proxy.js'; | ||||
| import type {  | ||||
|   NfTableProxyOptions,  | ||||
|   PortRange, | ||||
|   NfTablesStatus | ||||
| } from '../nftables-proxy/models/interfaces.js'; | ||||
| import type { | ||||
|   IRouteConfig, | ||||
|   TPortRange, | ||||
|   INfTablesOptions | ||||
| } from './models/route-types.js'; | ||||
| import type { ISmartProxyOptions } from './models/interfaces.js'; | ||||
|  | ||||
| /** | ||||
|  * Manages NFTables rules based on SmartProxy route configurations | ||||
|  *  | ||||
|  * This class bridges the gap between SmartProxy routes and the NFTablesProxy, | ||||
|  * allowing high-performance kernel-level packet forwarding for routes that  | ||||
|  * specify NFTables as their forwarding engine. | ||||
|  */ | ||||
| export class NFTablesManager { | ||||
|   private rulesMap: Map<string, NfTablesProxy> = new Map(); | ||||
|    | ||||
|   /** | ||||
|    * Creates a new NFTablesManager | ||||
|    *  | ||||
|    * @param options The SmartProxy options | ||||
|    */ | ||||
|   constructor(private options: ISmartProxyOptions) {} | ||||
|    | ||||
|   /** | ||||
|    * Provision NFTables rules for a route | ||||
|    *  | ||||
|    * @param route The route configuration | ||||
|    * @returns A promise that resolves to true if successful, false otherwise | ||||
|    */ | ||||
|   public async provisionRoute(route: IRouteConfig): Promise<boolean> { | ||||
|     // Generate a unique ID for this route | ||||
|     const routeId = this.generateRouteId(route); | ||||
|      | ||||
|     // Skip if route doesn't use NFTables | ||||
|     if (route.action.forwardingEngine !== 'nftables') { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     // Create NFTables options from route configuration | ||||
|     const nftOptions = this.createNfTablesOptions(route); | ||||
|      | ||||
|     // Create and start an NFTablesProxy instance | ||||
|     const proxy = new NfTablesProxy(nftOptions); | ||||
|      | ||||
|     try { | ||||
|       await proxy.start(); | ||||
|       this.rulesMap.set(routeId, proxy); | ||||
|       return true; | ||||
|     } catch (err) { | ||||
|       console.error(`Failed to provision NFTables rules for route ${route.name || 'unnamed'}: ${err.message}`); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Remove NFTables rules for a route | ||||
|    *  | ||||
|    * @param route The route configuration | ||||
|    * @returns A promise that resolves to true if successful, false otherwise | ||||
|    */ | ||||
|   public async deprovisionRoute(route: IRouteConfig): Promise<boolean> { | ||||
|     const routeId = this.generateRouteId(route); | ||||
|      | ||||
|     const proxy = this.rulesMap.get(routeId); | ||||
|     if (!proxy) { | ||||
|       return true; // Nothing to remove | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       await proxy.stop(); | ||||
|       this.rulesMap.delete(routeId); | ||||
|       return true; | ||||
|     } catch (err) { | ||||
|       console.error(`Failed to deprovision NFTables rules for route ${route.name || 'unnamed'}: ${err.message}`); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update NFTables rules when route changes | ||||
|    *  | ||||
|    * @param oldRoute The previous route configuration | ||||
|    * @param newRoute The new route configuration | ||||
|    * @returns A promise that resolves to true if successful, false otherwise | ||||
|    */ | ||||
|   public async updateRoute(oldRoute: IRouteConfig, newRoute: IRouteConfig): Promise<boolean> { | ||||
|     // Remove old rules and add new ones | ||||
|     await this.deprovisionRoute(oldRoute); | ||||
|     return this.provisionRoute(newRoute); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Generate a unique ID for a route | ||||
|    *  | ||||
|    * @param route The route configuration | ||||
|    * @returns A unique ID string | ||||
|    */ | ||||
|   private generateRouteId(route: IRouteConfig): string { | ||||
|     // Generate a unique ID based on route properties | ||||
|     // Include the route name, match criteria, and a timestamp | ||||
|     const matchStr = JSON.stringify({ | ||||
|       ports: route.match.ports, | ||||
|       domains: route.match.domains | ||||
|     }); | ||||
|      | ||||
|     return `${route.name || 'unnamed'}-${matchStr}-${route.id || Date.now().toString()}`; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create NFTablesProxy options from a route configuration | ||||
|    *  | ||||
|    * @param route The route configuration | ||||
|    * @returns NFTableProxyOptions object | ||||
|    */ | ||||
|   private createNfTablesOptions(route: IRouteConfig): NfTableProxyOptions { | ||||
|     const { action } = route; | ||||
|      | ||||
|     // Ensure we have a target | ||||
|     if (!action.target) { | ||||
|       throw new Error('Route must have a target to use NFTables forwarding'); | ||||
|     } | ||||
|      | ||||
|     // Convert port specifications | ||||
|     const fromPorts = this.expandPortRange(route.match.ports); | ||||
|      | ||||
|     // Determine target port | ||||
|     let toPorts: number | PortRange | Array<number | PortRange>; | ||||
|      | ||||
|     if (action.target.port === 'preserve') { | ||||
|       // 'preserve' means use the same ports as the source | ||||
|       toPorts = fromPorts; | ||||
|     } else if (typeof action.target.port === 'function') { | ||||
|       // For function-based ports, we can't determine at setup time | ||||
|       // Use the "preserve" approach and let NFTables handle it | ||||
|       toPorts = fromPorts; | ||||
|     } else { | ||||
|       toPorts = action.target.port; | ||||
|     } | ||||
|      | ||||
|     // Determine target host | ||||
|     let toHost: string; | ||||
|     if (typeof action.target.host === 'function') { | ||||
|       // Can't determine at setup time, use localhost as a placeholder | ||||
|       // and rely on run-time handling | ||||
|       toHost = 'localhost'; | ||||
|     } else if (Array.isArray(action.target.host)) { | ||||
|       // Use first host for now - NFTables will do simple round-robin   | ||||
|       toHost = action.target.host[0]; | ||||
|     } else { | ||||
|       toHost = action.target.host; | ||||
|     } | ||||
|      | ||||
|     // Create options | ||||
|     const options: NfTableProxyOptions = { | ||||
|       fromPort: fromPorts, | ||||
|       toPort: toPorts, | ||||
|       toHost: toHost, | ||||
|       protocol: action.nftables?.protocol || 'tcp', | ||||
|       preserveSourceIP: action.nftables?.preserveSourceIP !== undefined ?  | ||||
|                         action.nftables.preserveSourceIP :  | ||||
|                         this.options.preserveSourceIP, | ||||
|       useIPSets: action.nftables?.useIPSets !== false, | ||||
|       useAdvancedNAT: action.nftables?.useAdvancedNAT, | ||||
|       enableLogging: this.options.enableDetailedLogging, | ||||
|       deleteOnExit: true, | ||||
|       tableName: action.nftables?.tableName || 'smartproxy' | ||||
|     }; | ||||
|      | ||||
|     // Add security-related options | ||||
|     const security = action.security || route.security; | ||||
|     if (security?.ipAllowList?.length) { | ||||
|       options.ipAllowList = security.ipAllowList; | ||||
|     } | ||||
|      | ||||
|     if (security?.ipBlockList?.length) { | ||||
|       options.ipBlockList = security.ipBlockList; | ||||
|     } | ||||
|      | ||||
|     // Add QoS options | ||||
|     if (action.nftables?.maxRate || action.nftables?.priority) { | ||||
|       options.qos = { | ||||
|         enabled: true, | ||||
|         maxRate: action.nftables.maxRate, | ||||
|         priority: action.nftables.priority | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     return options; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Expand port range specifications | ||||
|    *  | ||||
|    * @param ports The port range specification | ||||
|    * @returns Expanded port range | ||||
|    */ | ||||
|   private expandPortRange(ports: TPortRange): number | PortRange | Array<number | PortRange> { | ||||
|     // Process different port specifications | ||||
|     if (typeof ports === 'number') { | ||||
|       return ports; | ||||
|     } else if (Array.isArray(ports)) { | ||||
|       const result: Array<number | PortRange> = []; | ||||
|        | ||||
|       for (const item of ports) { | ||||
|         if (typeof item === 'number') { | ||||
|           result.push(item); | ||||
|         } else if ('from' in item && 'to' in item) { | ||||
|           result.push({ from: item.from, to: item.to }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       return result; | ||||
|     } else if (typeof ports === 'object' && ports !== null && 'from' in ports && 'to' in ports) { | ||||
|       return { from: (ports as any).from, to: (ports as any).to }; | ||||
|     } | ||||
|      | ||||
|     // Fallback to port 80 if something went wrong | ||||
|     console.warn('Invalid port range specification, using port 80 as fallback'); | ||||
|     return 80; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get status of all managed rules | ||||
|    *  | ||||
|    * @returns A promise that resolves to a record of NFTables status objects | ||||
|    */ | ||||
|   public async getStatus(): Promise<Record<string, NfTablesStatus>> { | ||||
|     const result: Record<string, NfTablesStatus> = {}; | ||||
|      | ||||
|     for (const [routeId, proxy] of this.rulesMap.entries()) { | ||||
|       result[routeId] = await proxy.getStatus(); | ||||
|     } | ||||
|      | ||||
|     return result; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if a route is currently provisioned | ||||
|    *  | ||||
|    * @param route The route configuration | ||||
|    * @returns True if the route is provisioned, false otherwise | ||||
|    */ | ||||
|   public isRouteProvisioned(route: IRouteConfig): boolean { | ||||
|     const routeId = this.generateRouteId(route); | ||||
|     return this.rulesMap.has(routeId); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Stop all NFTables rules | ||||
|    *  | ||||
|    * @returns A promise that resolves when all rules have been stopped | ||||
|    */ | ||||
|   public async stop(): Promise<void> { | ||||
|     // Stop all NFTables proxies | ||||
|     const stopPromises = Array.from(this.rulesMap.values()).map(proxy => proxy.stop()); | ||||
|     await Promise.all(stopPromises); | ||||
|      | ||||
|     this.rulesMap.clear(); | ||||
|   } | ||||
| } | ||||
| @@ -338,6 +338,22 @@ export class RouteConnectionHandler { | ||||
|       ); | ||||
|     } | ||||
|      | ||||
|     // Check if this route uses NFTables for forwarding | ||||
|     if (route.action.forwardingEngine === 'nftables') { | ||||
|       // For NFTables routes, we don't need to do anything at the application level | ||||
|       // The packet is forwarded at the kernel level | ||||
|        | ||||
|       // Log the connection | ||||
|       console.log( | ||||
|         `[${connectionId}] Connection forwarded by NFTables: ${record.remoteIP} -> port ${record.localPort}` | ||||
|       ); | ||||
|        | ||||
|       // Just close the socket in our application since it's handled at kernel level | ||||
|       socket.end(); | ||||
|       this.connectionManager.cleanupConnection(record, 'nftables_handled'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Handle the route based on its action type | ||||
|     switch (route.action.type) { | ||||
|       case 'forward': | ||||
| @@ -368,6 +384,45 @@ export class RouteConnectionHandler { | ||||
|     const connectionId = record.id; | ||||
|     const action = route.action; | ||||
|  | ||||
|     // Check if this route uses NFTables for forwarding | ||||
|     if (action.forwardingEngine === 'nftables') { | ||||
|       // Log detailed information about NFTables-handled connection | ||||
|       if (this.settings.enableDetailedLogging) { | ||||
|         console.log( | ||||
|           `[${record.id}] Connection forwarded by NFTables (kernel-level): ` + | ||||
|           `${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` + | ||||
|           ` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})` | ||||
|         ); | ||||
|       } else { | ||||
|         console.log( | ||||
|           `[${record.id}] NFTables forwarding: ${record.remoteIP} -> port ${record.localPort} (Route: "${route.name || 'unnamed'}")` | ||||
|         ); | ||||
|       } | ||||
|        | ||||
|       // Additional NFTables-specific logging if configured | ||||
|       if (action.nftables) { | ||||
|         const nftConfig = action.nftables; | ||||
|         if (this.settings.enableDetailedLogging) { | ||||
|           console.log( | ||||
|             `[${record.id}] NFTables config: ` + | ||||
|             `protocol=${nftConfig.protocol || 'tcp'}, ` + | ||||
|             `preserveSourceIP=${nftConfig.preserveSourceIP || false}, ` + | ||||
|             `priority=${nftConfig.priority || 'default'}, ` + | ||||
|             `maxRate=${nftConfig.maxRate || 'unlimited'}` | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // This connection is handled at the kernel level, no need to process at application level | ||||
|       // Close the socket gracefully in our application layer | ||||
|       socket.end(); | ||||
|        | ||||
|       // Mark the connection as handled by NFTables for proper cleanup | ||||
|       record.nftablesHandled = true; | ||||
|       this.connectionManager.initiateCleanupOnce(record, 'nftables_handled'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // We should have a target configuration for forwarding | ||||
|     if (!action.target) { | ||||
|       console.log(`[${connectionId}] Forward action missing target configuration`); | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import { TimeoutManager } from './timeout-manager.js'; | ||||
| import { PortManager } from './port-manager.js'; | ||||
| import { RouteManager } from './route-manager.js'; | ||||
| import { RouteConnectionHandler } from './route-connection-handler.js'; | ||||
| import { NFTablesManager } from './nftables-manager.js'; | ||||
|  | ||||
| // External dependencies | ||||
| import { Port80Handler } from '../../http/port80/port80-handler.js'; | ||||
| @@ -50,6 +51,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   private timeoutManager: TimeoutManager; | ||||
|   public routeManager: RouteManager; // Made public for route management | ||||
|   private routeConnectionHandler: RouteConnectionHandler; | ||||
|   private nftablesManager: NFTablesManager; | ||||
|    | ||||
|   // Port80Handler for ACME certificate management | ||||
|   private port80Handler: Port80Handler | null = null; | ||||
| @@ -82,7 +84,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|    *   ], | ||||
|    *   defaults: { | ||||
|    *     target: { host: 'localhost', port: 8080 }, | ||||
|    *     security: { allowedIps: ['*'] } | ||||
|    *     security: { ipAllowList: ['*'] } | ||||
|    *   } | ||||
|    * }); | ||||
|    * ``` | ||||
| @@ -167,6 +169,9 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|  | ||||
|     // Initialize port manager | ||||
|     this.portManager = new PortManager(this.settings, this.routeConnectionHandler); | ||||
|      | ||||
|     // Initialize NFTablesManager | ||||
|     this.nftablesManager = new NFTablesManager(this.settings); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
| @@ -270,6 +275,13 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     // Get listening ports from RouteManager | ||||
|     const listeningPorts = this.routeManager.getListeningPorts(); | ||||
|  | ||||
|     // Provision NFTables rules for routes that use NFTables | ||||
|     for (const route of this.settings.routes) { | ||||
|       if (route.action.forwardingEngine === 'nftables') { | ||||
|         await this.nftablesManager.provisionRoute(route); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Start port listeners using the PortManager | ||||
|     await this.portManager.addPorts(listeningPorts); | ||||
|  | ||||
| @@ -364,6 +376,10 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       await this.certProvisioner.stop(); | ||||
|       console.log('CertProvisioner stopped'); | ||||
|     } | ||||
|      | ||||
|     // Stop NFTablesManager | ||||
|     await this.nftablesManager.stop(); | ||||
|     console.log('NFTablesManager stopped'); | ||||
|  | ||||
|     // Stop the Port80Handler if running | ||||
|     if (this.port80Handler) { | ||||
| @@ -432,6 +448,39 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> { | ||||
|     console.log(`Updating routes (${newRoutes.length} routes)`); | ||||
|  | ||||
|     // Get existing routes that use NFTables | ||||
|     const oldNfTablesRoutes = this.settings.routes.filter( | ||||
|       r => r.action.forwardingEngine === 'nftables' | ||||
|     ); | ||||
|      | ||||
|     // Get new routes that use NFTables | ||||
|     const newNfTablesRoutes = newRoutes.filter( | ||||
|       r => r.action.forwardingEngine === 'nftables' | ||||
|     ); | ||||
|      | ||||
|     // Find routes to remove, update, or add | ||||
|     for (const oldRoute of oldNfTablesRoutes) { | ||||
|       const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name); | ||||
|        | ||||
|       if (!newRoute) { | ||||
|         // Route was removed | ||||
|         await this.nftablesManager.deprovisionRoute(oldRoute); | ||||
|       } else { | ||||
|         // Route was updated | ||||
|         await this.nftablesManager.updateRoute(oldRoute, newRoute); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Find new routes to add | ||||
|     for (const newRoute of newNfTablesRoutes) { | ||||
|       const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name); | ||||
|        | ||||
|       if (!oldRoute) { | ||||
|         // New route | ||||
|         await this.nftablesManager.provisionRoute(newRoute); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Update routes in RouteManager | ||||
|     this.routeManager.updateRoutes(newRoutes); | ||||
|  | ||||
| @@ -440,6 +489,9 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|  | ||||
|     // Update port listeners to match the new configuration | ||||
|     await this.portManager.updatePorts(requiredPorts); | ||||
|      | ||||
|     // Update settings with the new routes | ||||
|     this.settings.routes = newRoutes; | ||||
|  | ||||
|     // If NetworkProxy is initialized, resync the configurations | ||||
|     if (this.networkProxyBridge.getNetworkProxy()) { | ||||
| @@ -676,6 +728,13 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     return domains; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get NFTables status | ||||
|    */ | ||||
|   public async getNfTablesStatus(): Promise<Record<string, any>> { | ||||
|     return this.nftablesManager.getStatus(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get status of certificates managed by Port80Handler | ||||
|    */ | ||||
|   | ||||
| @@ -5,7 +5,8 @@ | ||||
|  * including helpers, validators, utilities, and patterns for working with routes. | ||||
|  */ | ||||
|  | ||||
| // Route helpers have been consolidated in route-patterns.js | ||||
| // Export route helpers for creating route configurations | ||||
| export * from './route-helpers.js'; | ||||
|  | ||||
| // Export route validators for validating route configurations | ||||
| export * from './route-validators.js'; | ||||
|   | ||||
| @@ -16,6 +16,7 @@ | ||||
|  * - WebSocket routes (createWebSocketRoute) | ||||
|  * - Port mapping routes (createPortMappingRoute, createOffsetPortMappingRoute) | ||||
|  * - Dynamic routing (createDynamicRoute, createSmartLoadBalancer) | ||||
|  * - NFTables routes (createNfTablesRoute, createNfTablesTerminateRoute) | ||||
|  */ | ||||
|  | ||||
| import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.js'; | ||||
| @@ -618,4 +619,195 @@ export function createSmartLoadBalancer(options: { | ||||
|     priority: options.priority, | ||||
|     ...options | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an NFTables-based route for high-performance packet forwarding | ||||
|  * @param nameOrDomains Name or domain(s) to match | ||||
|  * @param target Target host and port | ||||
|  * @param options Additional route options | ||||
|  * @returns Route configuration object | ||||
|  */ | ||||
| export function createNfTablesRoute( | ||||
|   nameOrDomains: string | string[], | ||||
|   target: { host: string; port: number | 'preserve' }, | ||||
|   options: { | ||||
|     ports?: TPortRange; | ||||
|     protocol?: 'tcp' | 'udp' | 'all'; | ||||
|     preserveSourceIP?: boolean; | ||||
|     ipAllowList?: string[]; | ||||
|     ipBlockList?: string[]; | ||||
|     maxRate?: string; | ||||
|     priority?: number; | ||||
|     useTls?: boolean; | ||||
|     tableName?: string; | ||||
|     useIPSets?: boolean; | ||||
|     useAdvancedNAT?: boolean; | ||||
|   } = {} | ||||
| ): IRouteConfig { | ||||
|   // Determine if this is a name or domain | ||||
|   let name: string; | ||||
|   let domains: string | string[] | undefined; | ||||
|    | ||||
|   if (Array.isArray(nameOrDomains) || (typeof nameOrDomains === 'string' && nameOrDomains.includes('.'))) { | ||||
|     domains = nameOrDomains; | ||||
|     name = Array.isArray(nameOrDomains) ? nameOrDomains[0] : nameOrDomains; | ||||
|   } else { | ||||
|     name = nameOrDomains; | ||||
|     domains = undefined; // No domains | ||||
|   } | ||||
|    | ||||
|   // Create route match | ||||
|   const match: IRouteMatch = { | ||||
|     domains, | ||||
|     ports: options.ports || 80 | ||||
|   }; | ||||
|    | ||||
|   // Create route action | ||||
|   const action: IRouteAction = { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: target.host, | ||||
|       port: target.port | ||||
|     }, | ||||
|     forwardingEngine: 'nftables', | ||||
|     nftables: { | ||||
|       protocol: options.protocol || 'tcp', | ||||
|       preserveSourceIP: options.preserveSourceIP, | ||||
|       maxRate: options.maxRate, | ||||
|       priority: options.priority, | ||||
|       tableName: options.tableName, | ||||
|       useIPSets: options.useIPSets, | ||||
|       useAdvancedNAT: options.useAdvancedNAT | ||||
|     } | ||||
|   }; | ||||
|    | ||||
|   // Add security if allowed or blocked IPs are specified | ||||
|   if (options.ipAllowList?.length || options.ipBlockList?.length) { | ||||
|     action.security = { | ||||
|       ipAllowList: options.ipAllowList, | ||||
|       ipBlockList: options.ipBlockList | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   // Add TLS options if needed | ||||
|   if (options.useTls) { | ||||
|     action.tls = { | ||||
|       mode: 'passthrough' | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   // Create the route config | ||||
|   return { | ||||
|     name, | ||||
|     match, | ||||
|     action | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an NFTables-based TLS termination route | ||||
|  * @param nameOrDomains Name or domain(s) to match | ||||
|  * @param target Target host and port | ||||
|  * @param options Additional route options | ||||
|  * @returns Route configuration object | ||||
|  */ | ||||
| export function createNfTablesTerminateRoute( | ||||
|   nameOrDomains: string | string[], | ||||
|   target: { host: string; port: number | 'preserve' }, | ||||
|   options: { | ||||
|     ports?: TPortRange; | ||||
|     protocol?: 'tcp' | 'udp' | 'all'; | ||||
|     preserveSourceIP?: boolean; | ||||
|     ipAllowList?: string[]; | ||||
|     ipBlockList?: string[]; | ||||
|     maxRate?: string; | ||||
|     priority?: number; | ||||
|     tableName?: string; | ||||
|     useIPSets?: boolean; | ||||
|     useAdvancedNAT?: boolean; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|   } = {} | ||||
| ): IRouteConfig { | ||||
|   // Create basic NFTables route | ||||
|   const route = createNfTablesRoute( | ||||
|     nameOrDomains, | ||||
|     target, | ||||
|     { | ||||
|       ...options, | ||||
|       ports: options.ports || 443, | ||||
|       useTls: false | ||||
|     } | ||||
|   ); | ||||
|    | ||||
|   // Set TLS termination | ||||
|   route.action.tls = { | ||||
|     mode: 'terminate', | ||||
|     certificate: options.certificate || 'auto' | ||||
|   }; | ||||
|    | ||||
|   return route; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a complete NFTables-based HTTPS setup with HTTP redirect | ||||
|  * @param nameOrDomains Name or domain(s) to match | ||||
|  * @param target Target host and port | ||||
|  * @param options Additional route options | ||||
|  * @returns Array of two route configurations (HTTPS and HTTP redirect) | ||||
|  */ | ||||
| export function createCompleteNfTablesHttpsServer( | ||||
|   nameOrDomains: string | string[], | ||||
|   target: { host: string; port: number | 'preserve' }, | ||||
|   options: { | ||||
|     httpPort?: TPortRange; | ||||
|     httpsPort?: TPortRange; | ||||
|     protocol?: 'tcp' | 'udp' | 'all'; | ||||
|     preserveSourceIP?: boolean; | ||||
|     ipAllowList?: string[]; | ||||
|     ipBlockList?: string[]; | ||||
|     maxRate?: string; | ||||
|     priority?: number; | ||||
|     tableName?: string; | ||||
|     useIPSets?: boolean; | ||||
|     useAdvancedNAT?: boolean; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|   } = {} | ||||
| ): IRouteConfig[] { | ||||
|   // Create the HTTPS route using NFTables | ||||
|   const httpsRoute = createNfTablesTerminateRoute( | ||||
|     nameOrDomains, | ||||
|     target, | ||||
|     { | ||||
|       ...options, | ||||
|       ports: options.httpsPort || 443 | ||||
|     } | ||||
|   ); | ||||
|    | ||||
|   // Determine the domain(s) for HTTP redirect | ||||
|   const domains = typeof nameOrDomains === 'string' && !nameOrDomains.includes('.')  | ||||
|     ? undefined  | ||||
|     : nameOrDomains; | ||||
|    | ||||
|   // Extract the HTTPS port for the redirect destination | ||||
|   const httpsPort = typeof options.httpsPort === 'number'  | ||||
|     ? options.httpsPort  | ||||
|     : Array.isArray(options.httpsPort) && typeof options.httpsPort[0] === 'number'  | ||||
|       ? options.httpsPort[0]  | ||||
|       : 443; | ||||
|    | ||||
|   // Create the HTTP redirect route (this uses standard forwarding, not NFTables) | ||||
|   const httpRedirectRoute = createHttpToHttpsRedirect( | ||||
|     domains as any, // Type cast needed since domains can be undefined now | ||||
|     httpsPort, | ||||
|     { | ||||
|       match: { | ||||
|         ports: options.httpPort || 80, | ||||
|         domains: domains as any // Type cast needed since domains can be undefined now | ||||
|       }, | ||||
|       name: `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains || 'all domains'}` | ||||
|     } | ||||
|   ); | ||||
|    | ||||
|   return [httpsRoute, httpRedirectRoute]; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user