fix(smartproxy): Update dynamic port mapping to support
This commit is contained in:
		
							
								
								
									
										15
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,20 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-05-14 - 16.0.4 - fix(smartproxy) | ||||
| Update dynamic port mapping to support 'preserve' target port value | ||||
|  | ||||
| - Refactored NetworkProxy to use a default port for 'preserve' values, correctly falling back to the incoming port when target.port is set to 'preserve'. | ||||
| - Updated RequestHandler and WebSocketHandler to check for 'preserve' target port instead of legacy preservePort flag. | ||||
| - Modified IRouteTarget type definitions to allow 'preserve' as a valid target port value. | ||||
|  | ||||
| ## 2025-05-14 - 16.0.4 - fix(smartproxy) | ||||
| Fix dynamic port mapping: update target port resolution to properly handle 'preserve' values across route configurations. Now, when a route's target port is set to 'preserve', the incoming port is used consistently in NetworkProxy, RequestHandler, WebSocketHandler, and RouteConnectionHandler. Also update type definitions in IRouteTarget to support 'preserve'. | ||||
|  | ||||
| - Refactored port resolution in NetworkProxy to use a default port for 'preserve' and then correctly fall back to the incoming port when 'preserve' is specified. | ||||
| - Updated RequestHandler and WebSocketHandler to check if target.port equals 'preserve' instead of using a legacy 'preservePort' flag. | ||||
| - Modified RouteConnectionHandler to correctly resolve dynamic port mappings with 'preserve'. | ||||
| - Updated route type definitions to allow 'preserve' as a valid target port value. | ||||
|  | ||||
| ## 2025-05-14 - 16.0.3 - fix(network-proxy, route-utils, route-manager) | ||||
| Normalize IPv6-mapped IPv4 addresses in IP matching functions and remove deprecated legacy configuration methods in NetworkProxy. Update route-utils and route-manager to compare both canonical and IPv6-mapped IP forms, adjust tests accordingly, and clean up legacy exports. | ||||
|  | ||||
|   | ||||
							
								
								
									
										468
									
								
								docs/porthandling.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										468
									
								
								docs/porthandling.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,468 @@ | ||||
| # SmartProxy Port Handling | ||||
|  | ||||
| This document covers all the port handling capabilities in SmartProxy, including port range specification, dynamic port mapping, and runtime port management. | ||||
|  | ||||
| ## Port Range Syntax | ||||
|  | ||||
| SmartProxy offers flexible port range specification through the `TPortRange` type, which can be defined in three different ways: | ||||
|  | ||||
| ### 1. Single Port | ||||
|  | ||||
| ```typescript | ||||
| // Match a single port | ||||
| { | ||||
|   match: { | ||||
|     ports: 443 | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 2. Array of Specific Ports | ||||
|  | ||||
| ```typescript | ||||
| // Match multiple specific ports | ||||
| { | ||||
|   match: { | ||||
|     ports: [80, 443, 8080] | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 3. Port Range | ||||
|  | ||||
| ```typescript | ||||
| // Match a range of ports | ||||
| { | ||||
|   match: { | ||||
|     ports: [{ from: 8000, to: 8100 }] | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 4. Mixed Port Specifications | ||||
|  | ||||
| You can combine different port specification methods in a single rule: | ||||
|  | ||||
| ```typescript | ||||
| // Match both specific ports and port ranges | ||||
| { | ||||
|   match: { | ||||
|     ports: [80, 443, { from: 8000, to: 8100 }] | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Port Forwarding Options | ||||
|  | ||||
| SmartProxy offers several ways to handle port forwarding from source to target: | ||||
|  | ||||
| ### 1. Static Port Forwarding | ||||
|  | ||||
| Forward to a fixed target port: | ||||
|  | ||||
| ```typescript | ||||
| { | ||||
|   action: { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: 'backend.example.com', | ||||
|       port: 8080 | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 2. Preserve Source Port | ||||
|  | ||||
| Forward to the same port on the target: | ||||
|  | ||||
| ```typescript | ||||
| { | ||||
|   action: { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: 'backend.example.com', | ||||
|       port: 'preserve' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 3. Dynamic Port Mapping | ||||
|  | ||||
| Use a function to determine the target port based on connection context: | ||||
|  | ||||
| ```typescript | ||||
| { | ||||
|   action: { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: 'backend.example.com', | ||||
|       port: (context) => { | ||||
|         // Calculate port based on request details | ||||
|         return 8000 + (context.port % 100); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Port Selection Context | ||||
|  | ||||
| When using dynamic port mapping functions, you have access to a rich context object that provides details about the connection: | ||||
|  | ||||
| ```typescript | ||||
| interface IRouteContext { | ||||
|   // Connection information | ||||
|   port: number;          // The matched incoming port | ||||
|   domain?: string;       // The domain from SNI or Host header | ||||
|   clientIp: string;      // The client's IP address | ||||
|   serverIp: string;      // The server's IP address | ||||
|   path?: string;         // URL path (for HTTP connections) | ||||
|   query?: string;        // Query string (for HTTP connections) | ||||
|   headers?: Record<string, string>; // HTTP headers (for HTTP connections) | ||||
|  | ||||
|   // TLS information | ||||
|   isTls: boolean;        // Whether the connection is TLS | ||||
|   tlsVersion?: string;   // TLS version if applicable | ||||
|  | ||||
|   // Route information | ||||
|   routeName?: string;    // The name of the matched route | ||||
|   routeId?: string;      // The ID of the matched route | ||||
|  | ||||
|   // Additional properties | ||||
|   timestamp: number;     // The request timestamp | ||||
|   connectionId: string;  // Unique connection identifier | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Common Port Mapping Patterns | ||||
|  | ||||
| ### 1. Port Offset Mapping | ||||
|  | ||||
| Forward traffic to target ports with a fixed offset: | ||||
|  | ||||
| ```typescript | ||||
| { | ||||
|   action: { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: 'backend.example.com', | ||||
|       port: (context) => context.port + 1000 | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 2. Domain-Based Port Mapping | ||||
|  | ||||
| Forward to different backend ports based on the domain: | ||||
|  | ||||
| ```typescript | ||||
| { | ||||
|   action: { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: 'backend.example.com', | ||||
|       port: (context) => { | ||||
|         switch (context.domain) { | ||||
|           case 'api.example.com': return 8001; | ||||
|           case 'admin.example.com': return 8002; | ||||
|           case 'staging.example.com': return 8003; | ||||
|           default: return 8000; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 3. Load Balancing with Hash-Based Distribution | ||||
|  | ||||
| Distribute connections across a port range using a deterministic hash function: | ||||
|  | ||||
| ```typescript | ||||
| { | ||||
|   action: { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: 'backend.example.com', | ||||
|       port: (context) => { | ||||
|         // Simple hash function to ensure consistent mapping | ||||
|         const hostname = context.domain || ''; | ||||
|         const hash = hostname.split('').reduce((a, b) => a + b.charCodeAt(0), 0); | ||||
|         return 8000 + (hash % 10); // Map to ports 8000-8009 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## IPv6-Mapped IPv4 Compatibility | ||||
|  | ||||
| SmartProxy automatically handles IPv6-mapped IPv4 addresses for optimal compatibility. When a connection from an IPv4 address (e.g., `192.168.1.1`) arrives as an IPv6-mapped address (`::ffff:192.168.1.1`), the system normalizes these addresses for consistent matching. | ||||
|  | ||||
| This is particularly important when: | ||||
|  | ||||
| 1. Matching client IP restrictions in route configurations | ||||
| 2. Preserving source IP for outgoing connections | ||||
| 3. Tracking connections and rate limits | ||||
|  | ||||
| No special configuration is needed - the system handles this normalization automatically. | ||||
|  | ||||
| ## Dynamic Port Management | ||||
|  | ||||
| SmartProxy allows for runtime port configuration changes without requiring a restart. | ||||
|  | ||||
| ### Adding and Removing Ports | ||||
|  | ||||
| ```typescript | ||||
| // Get the SmartProxy instance | ||||
| const proxy = new SmartProxy({ /* config */ }); | ||||
|  | ||||
| // Add a new listening port | ||||
| await proxy.addListeningPort(8081); | ||||
|  | ||||
| // Remove a listening port | ||||
| await proxy.removeListeningPort(8082); | ||||
| ``` | ||||
|  | ||||
| ### Runtime Route Updates | ||||
|  | ||||
| ```typescript | ||||
| // Get current routes | ||||
| const currentRoutes = proxy.getRoutes(); | ||||
|  | ||||
| // Add new route for the new port | ||||
| const newRoute = { | ||||
|   name: 'New Dynamic Route', | ||||
|   match: { | ||||
|     ports: 8081, | ||||
|     domains: ['dynamic.example.com'] | ||||
|   }, | ||||
|   action: { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: 'backend.example.com', | ||||
|       port: 9000 | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // Update the route configuration | ||||
| await proxy.updateRoutes([...currentRoutes, newRoute]); | ||||
|  | ||||
| // Remove routes for a specific port | ||||
| const routesWithout8082 = currentRoutes.filter(route => { | ||||
|   const ports = proxy.routeManager.expandPortRange(route.match.ports); | ||||
|   return !ports.includes(8082); | ||||
| }); | ||||
| await proxy.updateRoutes(routesWithout8082); | ||||
| ``` | ||||
|  | ||||
| ## Performance Considerations | ||||
|  | ||||
| ### Port Range Expansion | ||||
|  | ||||
| When using large port ranges, SmartProxy uses internal caching to optimize performance. For example, a range like `{ from: 1000, to: 2000 }` is expanded only once and then cached for future use. | ||||
|  | ||||
| ### Port Range Validation | ||||
|  | ||||
| The system automatically validates port ranges to ensure: | ||||
|  | ||||
| 1. Port numbers are within the valid range (1-65535) | ||||
| 2. The "from" value is not greater than the "to" value in range specifications | ||||
| 3. Port ranges do not contain duplicate entries | ||||
|  | ||||
| Invalid port ranges will be logged as warnings and skipped during configuration. | ||||
|  | ||||
| ## Configuration Recipes | ||||
|  | ||||
| ### Global Port Range | ||||
|  | ||||
| Listen on a large range of ports and forward to the same ports on a backend: | ||||
|  | ||||
| ```typescript | ||||
| { | ||||
|   name: 'Global port range forwarding', | ||||
|   match: { | ||||
|     ports: [{ from: 8000, to: 9000 }] | ||||
|   }, | ||||
|   action: { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: 'backend.example.com', | ||||
|       port: 'preserve' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Domain-Specific Port Ranges | ||||
|  | ||||
| Different port ranges for different domain groups: | ||||
|  | ||||
| ```typescript | ||||
| [ | ||||
|   { | ||||
|     name: 'API port range', | ||||
|     match: { | ||||
|       ports: [{ from: 8000, to: 8099 }] | ||||
|     }, | ||||
|     action: { | ||||
|       type: 'forward', | ||||
|       target: { | ||||
|         host: 'api.backend.example.com', | ||||
|         port: 'preserve' | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     name: 'Admin port range', | ||||
|     match: { | ||||
|       ports: [{ from: 9000, to: 9099 }] | ||||
|     }, | ||||
|     action: { | ||||
|       type: 'forward', | ||||
|       target: { | ||||
|         host: 'admin.backend.example.com', | ||||
|         port: 'preserve' | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| ] | ||||
| ``` | ||||
|  | ||||
| ### Mixed Internal/External Port Forwarding | ||||
|  | ||||
| Forward specific high-numbered ports to standard ports on internal servers: | ||||
|  | ||||
| ```typescript | ||||
| [ | ||||
|   { | ||||
|     name: 'Web server forwarding', | ||||
|     match: { | ||||
|       ports: [8080, 8443] | ||||
|     }, | ||||
|     action: { | ||||
|       type: 'forward', | ||||
|       target: { | ||||
|         host: 'web.internal', | ||||
|         port: (context) => context.port === 8080 ? 80 : 443 | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     name: 'Database forwarding', | ||||
|     match: { | ||||
|       ports: [15432] | ||||
|     }, | ||||
|     action: { | ||||
|       type: 'forward', | ||||
|       target: { | ||||
|         host: 'db.internal', | ||||
|         port: 5432 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| ] | ||||
| ``` | ||||
|  | ||||
| ## Debugging Port Configurations | ||||
|  | ||||
| When troubleshooting port forwarding issues, enable detailed logging: | ||||
|  | ||||
| ```typescript | ||||
| const proxy = new SmartProxy({ | ||||
|   routes: [ /* your routes */ ], | ||||
|   enableDetailedLogging: true | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| This will log: | ||||
| - Port configuration during startup | ||||
| - Port matching decisions during routing | ||||
| - Dynamic port function results | ||||
| - Connection details including source and target ports | ||||
|  | ||||
| ## Port Security Considerations | ||||
|  | ||||
| ### Restricting Ports | ||||
|  | ||||
| For security, you may want to restrict which ports can be accessed by specific clients: | ||||
|  | ||||
| ```typescript | ||||
| { | ||||
|   name: 'Restricted port range', | ||||
|   match: { | ||||
|     ports: [{ from: 8000, to: 9000 }], | ||||
|     clientIp: ['10.0.0.0/8'] // Only internal network can access these ports | ||||
|   }, | ||||
|   action: { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: 'internal.example.com', | ||||
|       port: 'preserve' | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Rate Limiting by Port | ||||
|  | ||||
| Apply different rate limits for different port ranges: | ||||
|  | ||||
| ```typescript | ||||
| { | ||||
|   name: 'API ports with rate limiting', | ||||
|   match: { | ||||
|     ports: [{ from: 8000, to: 8100 }] | ||||
|   }, | ||||
|   action: { | ||||
|     type: 'forward', | ||||
|     target: { | ||||
|       host: 'api.example.com', | ||||
|       port: 'preserve' | ||||
|     }, | ||||
|     security: { | ||||
|       rateLimit: { | ||||
|         enabled: true, | ||||
|         maxRequests: 100, | ||||
|         window: 60 // 60 seconds | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Best Practices | ||||
|  | ||||
| 1. **Use Specific Port Ranges**: Instead of large ranges (e.g., 1-65535), use specific ranges for specific purposes | ||||
|  | ||||
| 2. **Prioritize Routes**: When multiple routes could match, use the `priority` field to ensure the most specific route is matched first | ||||
|  | ||||
| 3. **Name Your Routes**: Use descriptive names to make debugging easier, especially when using port ranges | ||||
|  | ||||
| 4. **Use Preserve Port Where Possible**: Using `port: 'preserve'` is more efficient and easier to maintain than creating multiple specific mappings | ||||
|  | ||||
| 5. **Limit Dynamic Port Functions**: While powerful, complex port functions can be harder to debug; prefer simple map or math-based functions | ||||
|  | ||||
| 6. **Use Port Variables**: For complex setups, define your port ranges as variables for easier maintenance: | ||||
|  | ||||
| ```typescript | ||||
| const API_PORTS = [{ from: 8000, to: 8099 }]; | ||||
| const ADMIN_PORTS = [{ from: 9000, to: 9099 }]; | ||||
|  | ||||
| const routes = [ | ||||
|   { | ||||
|     name: 'API Routes', | ||||
|     match: { ports: API_PORTS, /* ... */ }, | ||||
|     // ... | ||||
|   }, | ||||
|   { | ||||
|     name: 'Admin Routes', | ||||
|     match: { ports: ADMIN_PORTS, /* ... */ }, | ||||
|     // ... | ||||
|   } | ||||
| ]; | ||||
| ``` | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '16.0.3', | ||||
|   version: '16.0.4', | ||||
|   description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' | ||||
| } | ||||
|   | ||||
| @@ -447,6 +447,8 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|  | ||||
|     // Create legacy proxy configs for the router | ||||
|     // This is only needed for backward compatibility with ProxyRouter | ||||
|      | ||||
|     const defaultPort = 443; // Default port for HTTPS when using 'preserve' | ||||
|     // and will be removed in the future | ||||
|     const legacyConfigs: IReverseProxyConfig[] = []; | ||||
|  | ||||
| @@ -472,7 +474,8 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|         ? route.action.target.host | ||||
|         : [route.action.target.host]; | ||||
|  | ||||
|       const targetPort = route.action.target.port; | ||||
|       // Handle 'preserve' port value | ||||
|       const targetPort = route.action.target.port === 'preserve' ? defaultPort : route.action.target.port; | ||||
|  | ||||
|       // Get certificate information | ||||
|       const certData = certificateUpdates.get(domain); | ||||
|   | ||||
| @@ -540,7 +540,7 @@ export class RequestHandler { | ||||
|             this.logger.debug(`Resolved function-based port to: ${resolvedPort}`); | ||||
|           } | ||||
|         } else { | ||||
|           targetPort = matchingRoute.action.target.port; | ||||
|           targetPort = matchingRoute.action.target.port === 'preserve' ? routeContext.port : matchingRoute.action.target.port as number; | ||||
|         } | ||||
|  | ||||
|         // Select a single host if an array was provided | ||||
| @@ -760,7 +760,7 @@ export class RequestHandler { | ||||
|             this.logger.debug(`Resolved HTTP/2 function-based port to: ${resolvedPort}`); | ||||
|           } | ||||
|         } else { | ||||
|           targetPort = matchingRoute.action.target.port; | ||||
|           targetPort = matchingRoute.action.target.port === 'preserve' ? routeContext.port : matchingRoute.action.target.port as number; | ||||
|         } | ||||
|  | ||||
|         // Select a single host if an array was provided | ||||
|   | ||||
| @@ -204,7 +204,7 @@ export class WebSocketHandler { | ||||
|             targetPort = route.action.target.port(toBaseContext(routeContext)); | ||||
|             this.logger.debug(`Resolved function-based port for WebSocket: ${targetPort}`); | ||||
|           } else { | ||||
|             targetPort = route.action.target.port; | ||||
|             targetPort = route.action.target.port === 'preserve' ? routeContext.port : route.action.target.port as number; | ||||
|           } | ||||
|  | ||||
|           // Select a single host if an array was provided | ||||
|   | ||||
| @@ -69,8 +69,7 @@ export interface IRouteContext { | ||||
|  */ | ||||
| export interface IRouteTarget { | ||||
|   host: string | string[] | ((context: IRouteContext) => string | string[]);  // Host or hosts with optional function for dynamic resolution | ||||
|   port: number | ((context: IRouteContext) => number);  // Port with optional function for dynamic mapping | ||||
|   preservePort?: boolean;   // Use incoming port as target port (ignored if port is a function) | ||||
|   port: number | 'preserve' | ((context: IRouteContext) => number);  // Port with optional function for dynamic mapping (use 'preserve' to keep the incoming port) | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -434,8 +434,8 @@ export class RouteConnectionHandler { | ||||
|         this.connectionManager.cleanupConnection(record, 'port_mapping_error'); | ||||
|         return; | ||||
|       } | ||||
|     } else if (action.target.preservePort) { | ||||
|       // Use incoming port if preservePort is true | ||||
|     } else if (action.target.port === 'preserve') { | ||||
|       // Use incoming port if port is 'preserve' | ||||
|       targetPort = record.localPort; | ||||
|     } else { | ||||
|       // Use static port from configuration | ||||
| @@ -525,7 +525,7 @@ export class RouteConnectionHandler { | ||||
|       let targetPort: number; | ||||
|       if (typeof action.target.port === 'function') { | ||||
|         targetPort = action.target.port(routeContext); | ||||
|       } else if (action.target.preservePort) { | ||||
|       } else if (action.target.port === 'preserve') { | ||||
|         targetPort = record.localPort; | ||||
|       } else { | ||||
|         targetPort = action.target.port; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user