diff --git a/changelog.md b/changelog.md index db580c8..fa2cc52 100644 --- a/changelog.md +++ b/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. diff --git a/docs/porthandling.md b/docs/porthandling.md new file mode 100644 index 0000000..e01dbee --- /dev/null +++ b/docs/porthandling.md @@ -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; // 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, /* ... */ }, + // ... + } +]; +``` \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 538fb1b..1a41132 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -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.' } diff --git a/ts/proxies/network-proxy/network-proxy.ts b/ts/proxies/network-proxy/network-proxy.ts index b1178c5..c208ece 100644 --- a/ts/proxies/network-proxy/network-proxy.ts +++ b/ts/proxies/network-proxy/network-proxy.ts @@ -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); diff --git a/ts/proxies/network-proxy/request-handler.ts b/ts/proxies/network-proxy/request-handler.ts index e75cb18..dcb5b36 100644 --- a/ts/proxies/network-proxy/request-handler.ts +++ b/ts/proxies/network-proxy/request-handler.ts @@ -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 diff --git a/ts/proxies/network-proxy/websocket-handler.ts b/ts/proxies/network-proxy/websocket-handler.ts index ab15cf9..077d262 100644 --- a/ts/proxies/network-proxy/websocket-handler.ts +++ b/ts/proxies/network-proxy/websocket-handler.ts @@ -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 diff --git a/ts/proxies/smart-proxy/models/route-types.ts b/ts/proxies/smart-proxy/models/route-types.ts index 495daa6..b32a150 100644 --- a/ts/proxies/smart-proxy/models/route-types.ts +++ b/ts/proxies/smart-proxy/models/route-types.ts @@ -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) } /** diff --git a/ts/proxies/smart-proxy/route-connection-handler.ts b/ts/proxies/smart-proxy/route-connection-handler.ts index c0cd4ee..8157af7 100644 --- a/ts/proxies/smart-proxy/route-connection-handler.ts +++ b/ts/proxies/smart-proxy/route-connection-handler.ts @@ -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;