diff --git a/package.json b/package.json
index 0aa9bd2..34884e2 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
{
"name": "@push.rocks/smartproxy",
- "version": "13.1.3",
+ "version": "14.0.0",
"private": false,
- "description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
+ "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.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
diff --git a/readme.md b/readme.md
index 4aca6fd..d76b808 100644
--- a/readme.md
+++ b/readme.md
@@ -2,16 +2,16 @@
A unified high-performance proxy toolkit for Node.js, with **SmartProxy** as the central API to handle all your proxy needs:
-- **Unified Configuration API**: One consistent way to configure various proxy types
+- **Unified Route-Based Configuration**: Match/action pattern for clean, consistent traffic routing
- **SSL/TLS Support**: Automatic HTTPS with Let's Encrypt certificate provisioning
-- **Simplified Domain Management**: Easy routing based on domain names with wildcard support
+- **Flexible Matching Patterns**: Route by port, domain, path, client IP, and TLS version
- **Advanced SNI Handling**: Smart TCP/SNI-based forwarding with IP filtering
-- **Multiple Forwarding Types**: HTTP-only, HTTPS passthrough, TLS termination options
+- **Multiple Action Types**: Forward (with TLS modes), redirect, or block traffic
- **Security Features**: IP allowlists, connection limits, timeouts, and more
## Project Architecture Overview
-SmartProxy has been restructured using a modern, modular architecture to improve maintainability and clarity:
+SmartProxy has been restructured using a modern, modular architecture with a unified route-based configuration system in v14.0.0:
```
/ts
@@ -29,19 +29,17 @@ SmartProxy has been restructured using a modern, modular architecture to improve
│ │ ├── http-handler.ts # HTTP-only handler
│ │ └── ... # Other handlers
│ ├── /config # Configuration models
-│ │ ├── forwarding-types.ts # Type definitions
-│ │ ├── domain-config.ts # Domain config utilities
-│ │ └── domain-manager.ts # Domain routing manager
│ └── /factory # Factory for creating handlers
├── /proxies # Different proxy implementations
│ ├── /smart-proxy # SmartProxy implementation
│ │ ├── /models # SmartProxy-specific interfaces
+│ │ │ ├── route-types.ts # Route-based configuration types
+│ │ │ └── interfaces.ts # SmartProxy interfaces
+│ │ ├── route-helpers.ts # Helper functions for creating routes
+│ │ ├── route-manager.ts # Route management system
│ │ ├── smart-proxy.ts # Main SmartProxy class
│ │ └── ... # Supporting classes
│ ├── /network-proxy # NetworkProxy implementation
-│ │ ├── /models # NetworkProxy-specific interfaces
-│ │ ├── network-proxy.ts # Main NetworkProxy class
-│ │ └── ... # Supporting classes
│ └── /nftables-proxy # NfTablesProxy implementation
├── /tls # TLS-specific functionality
│ ├── /sni # SNI handling components
@@ -58,18 +56,20 @@ SmartProxy has been restructured using a modern, modular architecture to improve
- **SmartProxy** (`ts/proxies/smart-proxy/smart-proxy.ts`)
The central unified API for all proxy needs, featuring:
- - Domain-based routing with SNI inspection
+ - Route-based configuration with match/action pattern
+ - Flexible matching criteria (ports, domains, paths, client IPs)
+ - Multiple action types (forward, redirect, block)
- Automatic certificate management
- - Multiple forwarding types in one configuration
- Advanced security controls
- - Flexible backend targeting options
### Helper Functions
-- **createDomainConfig**
- Create domain configuration with clean syntax
-- **httpOnly**, **httpsPassthrough**, **tlsTerminateToHttp**, **tlsTerminateToHttps**
- Helper functions to create different forwarding configurations
+- **createRoute**, **createHttpRoute**, **createHttpsRoute**, **createPassthroughRoute**
+ Helper functions to create different route configurations with clean syntax
+- **createRedirectRoute**, **createHttpToHttpsRedirect**, **createBlockRoute**
+ Helper functions for common redirect and security configurations
+- **createLoadBalancerRoute**, **createHttpsServer**
+ Helper functions for complex configurations
### Specialized Components
@@ -93,8 +93,8 @@ SmartProxy has been restructured using a modern, modular architecture to improve
### Interfaces and Types
-- `ISmartProxyOptions`, `IDomainConfig` (`ts/proxies/smart-proxy/models/interfaces.ts`)
-- `IForwardConfig`, `TForwardingType` (`ts/forwarding/config/forwarding-types.ts`)
+- `IRouteConfig`, `IRouteMatch`, `IRouteAction` (`ts/proxies/smart-proxy/models/route-types.ts`)
+- `IRoutedSmartProxyOptions` (`ts/proxies/smart-proxy/models/route-types.ts`)
- `INetworkProxyOptions` (`ts/proxies/network-proxy/models/types.ts`)
- `IAcmeOptions`, `IDomainOptions` (`ts/certificate/models/certificate-types.ts`)
- `INfTableProxySettings` (`ts/proxies/nftables-proxy/models/interfaces.ts`)
@@ -105,51 +105,71 @@ Install via npm:
npm install @push.rocks/smartproxy
```
-## Quick Start with SmartProxy
+## Quick Start with SmartProxy v14.0.0
-SmartProxy is the recommended way to use this library, providing a unified API for all proxy scenarios.
+SmartProxy v14.0.0 introduces a new unified route-based configuration system that makes configuring proxies more flexible and intuitive.
```typescript
-import { SmartProxy, createDomainConfig, httpOnly, tlsTerminateToHttp, httpsPassthrough } from '@push.rocks/smartproxy';
+import {
+ SmartProxy,
+ createHttpRoute,
+ createHttpsRoute,
+ createPassthroughRoute,
+ createHttpToHttpsRedirect
+} from '@push.rocks/smartproxy';
-// Create a new SmartProxy instance with all your domain configurations in one place
+// Create a new SmartProxy instance with route-based configuration
const proxy = new SmartProxy({
- // Listen on port 443 for incoming connections
- fromPort: 443,
-
- // Configure domains and their forwarding rules
- domainConfigs: [
- // Basic HTTP forwarding for api.example.com
- createDomainConfig('api.example.com', httpOnly({
+ // Define all your routing rules in one array
+ routes: [
+ // Basic HTTP route - forward traffic from port 80 to internal service
+ createHttpRoute({
+ ports: 80,
+ domains: 'api.example.com',
target: { host: 'localhost', port: 3000 }
- })),
+ }),
- // HTTPS termination with automatic Let's Encrypt certificates
- createDomainConfig('secure.example.com', tlsTerminateToHttp({
+ // HTTPS route with TLS termination and automatic certificates
+ createHttpsRoute({
+ ports: 443,
+ domains: 'secure.example.com',
target: { host: 'localhost', port: 8080 },
- acme: {
- enabled: true,
- production: true
- }
- })),
+ certificate: 'auto' // Use Let's Encrypt
+ }),
- // Multiple domains with wildcard support
- createDomainConfig(['example.com', '*.example.com'], httpsPassthrough({
- target: {
- // Load balancing across multiple backend servers
- host: ['192.168.1.10', '192.168.1.11'],
- port: 443
- },
+ // HTTPS passthrough for legacy systems
+ createPassthroughRoute({
+ ports: 443,
+ domains: 'legacy.example.com',
+ target: { host: '192.168.1.10', port: 443 }
+ }),
+
+ // Redirect HTTP to HTTPS
+ createHttpToHttpsRedirect({
+ domains: ['example.com', '*.example.com']
+ }),
+
+ // Complex load balancer setup with security controls
+ createLoadBalancerRoute({
+ domains: ['app.example.com'],
+ targets: ['192.168.1.10', '192.168.1.11', '192.168.1.12'],
+ targetPort: 8080,
+ tlsMode: 'terminate',
+ certificate: 'auto',
security: {
- // IP filtering for enhanced security
allowedIps: ['10.0.0.*', '192.168.1.*'],
- blockedIps: ['1.2.3.4']
+ blockedIps: ['1.2.3.4'],
+ maxConnections: 1000
}
- }))
+ })
],
- // Enable SNI-based routing
- sniEnabled: true,
+ // Global settings that apply to all routes
+ defaults: {
+ security: {
+ maxConnections: 500
+ }
+ },
// Automatic Let's Encrypt integration
acme: {
@@ -167,65 +187,189 @@ proxy.on('certificate', evt => {
// Start the proxy
await proxy.start();
-// Dynamically add or update domain configurations later
-await proxy.updateDomainConfigs([
- createDomainConfig('new-domain.com', tlsTerminateToHttp({
- target: { host: 'localhost', port: 9000 }
- }))
+// Dynamically add new routes later
+await proxy.addRoutes([
+ createHttpsRoute({
+ domains: 'new-domain.com',
+ target: { host: 'localhost', port: 9000 },
+ certificate: 'auto'
+ })
]);
// Later, gracefully shut down
await proxy.stop();
```
-### What You Can Do with SmartProxy
+## Route-Based Configuration System
-1. **Domain-Based Routing**
+The new route-based configuration in v14.0.0 follows a match/action pattern, making it more powerful and flexible:
+
+```typescript
+// Basic structure of a route configuration
+{
+ // What traffic to match
+ match: {
+ ports: 443, // Required: port(s) to listen on
+ domains: 'example.com', // Optional: domain(s) to match
+ path: '/api', // Optional: URL path pattern
+ clientIp: ['10.0.0.*'], // Optional: client IP patterns to match
+ tlsVersion: ['TLSv1.2', 'TLSv1.3'] // Optional: TLS versions to match
+ },
+
+ // What to do with matched traffic
+ action: {
+ type: 'forward', // 'forward', 'redirect', or 'block'
+
+ // For 'forward' actions
+ target: {
+ host: 'localhost', // Target host(s) for forwarding
+ port: 8080 // Target port
+ },
+
+ // TLS handling (for 'forward' actions)
+ tls: {
+ mode: 'terminate', // 'passthrough', 'terminate', 'terminate-and-reencrypt'
+ certificate: 'auto' // 'auto' for Let's Encrypt or {key, cert}
+ },
+
+ // For 'redirect' actions
+ redirect: {
+ to: 'https://{domain}{path}', // URL pattern with variables
+ status: 301 // HTTP status code
+ },
+
+ // Security controls for any action
+ security: {
+ allowedIps: ['10.0.0.*'], // IP allowlist
+ blockedIps: ['1.2.3.4'], // IP blocklist
+ maxConnections: 100 // Connection limits
+ },
+
+ // Advanced options
+ advanced: {
+ timeout: 30000, // Connection timeout
+ headers: { // Custom headers
+ 'X-Forwarded-For': '{clientIp}'
+ },
+ keepAlive: true // Connection pooling
+ }
+ },
+
+ // Metadata (optional)
+ name: 'API Server', // Human-readable name
+ description: 'Main API endpoints', // Description
+ priority: 100, // Matching priority (higher = matched first)
+ tags: ['api', 'internal'] // Arbitrary tags
+}
+```
+
+### Using Helper Functions
+
+While you can create route configurations manually, SmartProxy provides helper functions to make it easier:
+
+```typescript
+// Instead of building the full object:
+const route = {
+ match: { ports: 80, domains: 'example.com' },
+ action: { type: 'forward', target: { host: 'localhost', port: 8080 } },
+ name: 'Web Server'
+};
+
+// Use the helper function:
+const route = createHttpRoute({
+ domains: 'example.com',
+ target: { host: 'localhost', port: 8080 },
+ name: 'Web Server'
+});
+```
+
+Available helper functions:
+- `createRoute()` - Basic function to create any route configuration
+- `createHttpRoute()` - Create an HTTP forwarding route
+- `createHttpsRoute()` - Create an HTTPS route with TLS termination
+- `createPassthroughRoute()` - Create an HTTPS passthrough route
+- `createRedirectRoute()` - Create a generic redirect route
+- `createHttpToHttpsRedirect()` - Create an HTTP to HTTPS redirect
+- `createBlockRoute()` - Create a route to block specific traffic
+- `createLoadBalancerRoute()` - Create a route for load balancing
+- `createHttpsServer()` - Create a complete HTTPS server setup with HTTP redirect
+
+## What You Can Do with SmartProxy
+
+1. **Route-Based Traffic Management**
```typescript
// Route requests for different domains to different backend servers
- createDomainConfig('api.example.com', httpOnly({
- target: { host: 'api-server', port: 3000 }
- }))
+ createHttpsRoute({
+ domains: 'api.example.com',
+ target: { host: 'api-server', port: 3000 },
+ certificate: 'auto'
+ })
```
2. **Automatic SSL with Let's Encrypt**
```typescript
// Get and automatically renew certificates
- createDomainConfig('secure.example.com', tlsTerminateToHttp({
+ createHttpsRoute({
+ domains: 'secure.example.com',
target: { host: 'localhost', port: 8080 },
- acme: { enabled: true, production: true }
- }))
+ certificate: 'auto'
+ })
```
3. **Load Balancing**
```typescript
// Distribute traffic across multiple backend servers
- createDomainConfig('app.example.com', httpOnly({
- target: {
- host: ['10.0.0.1', '10.0.0.2', '10.0.0.3'],
- port: 8080
- }
- }))
+ createLoadBalancerRoute({
+ domains: 'app.example.com',
+ targets: ['10.0.0.1', '10.0.0.2', '10.0.0.3'],
+ targetPort: 8080,
+ tlsMode: 'terminate',
+ certificate: 'auto'
+ })
```
4. **Security Controls**
```typescript
// Restrict access based on IP addresses
- createDomainConfig('admin.example.com', httpOnly({
+ createHttpsRoute({
+ domains: 'admin.example.com',
target: { host: 'localhost', port: 8080 },
+ certificate: 'auto',
security: {
allowedIps: ['10.0.0.*', '192.168.1.*'],
maxConnections: 100
}
- }))
+ })
```
5. **Wildcard Domains**
```typescript
// Handle all subdomains with one config
- createDomainConfig(['example.com', '*.example.com'], httpsPassthrough({
+ createPassthroughRoute({
+ domains: ['example.com', '*.example.com'],
target: { host: 'backend-server', port: 443 }
- }))
+ })
+ ```
+
+6. **Path-Based Routing**
+ ```typescript
+ // Route based on URL path
+ createHttpsRoute({
+ domains: 'example.com',
+ path: '/api/*',
+ target: { host: 'api-server', port: 3000 },
+ certificate: 'auto'
+ })
+ ```
+
+7. **Block Malicious Traffic**
+ ```typescript
+ // Block traffic from specific IPs
+ createBlockRoute({
+ ports: [80, 443],
+ clientIp: ['1.2.3.*', '5.6.7.*'],
+ priority: 1000 // High priority to ensure blocking
+ })
```
## Other Components
@@ -293,13 +437,57 @@ const redirect = new SslRedirect(80);
await redirect.start();
```
-## API Reference
-For full configuration options and type definitions, see the TypeScript interfaces:
-- `INetworkProxyOptions` (`ts/proxies/network-proxy/models/types.ts`)
-- `IAcmeOptions`, `IDomainOptions` (`ts/certificate/models/certificate-types.ts`)
-- `IForwardConfig` (`ts/forwarding/config/forwarding-types.ts`)
-- `INfTableProxySettings` (`ts/proxies/nftables-proxy/models/interfaces.ts`)
-- `ISmartProxyOptions`, `IDomainConfig` (`ts/proxies/smart-proxy/models/interfaces.ts`)
+## Migration from v13.x to v14.0.0
+
+Version 14.0.0 introduces a breaking change with the new route-based configuration system:
+
+### Key Changes
+
+1. **Configuration Structure**: The configuration now uses the match/action pattern instead of the old domain-based and port-based approach
+2. **SmartProxy Options**: Now takes an array of route configurations instead of `domainConfigs` and port ranges
+3. **Helper Functions**: New helper functions have been introduced to simplify configuration
+
+### Migration Example
+
+**v13.x Configuration**:
+```typescript
+import { SmartProxy, createDomainConfig, httpOnly, tlsTerminateToHttp } from '@push.rocks/smartproxy';
+
+const proxy = new SmartProxy({
+ fromPort: 443,
+ domainConfigs: [
+ createDomainConfig('example.com', tlsTerminateToHttp({
+ target: { host: 'localhost', port: 8080 },
+ acme: { enabled: true, production: true }
+ }))
+ ],
+ sniEnabled: true
+});
+```
+
+**v14.0.0 Configuration**:
+```typescript
+import { SmartProxy, createHttpsRoute } from '@push.rocks/smartproxy';
+
+const proxy = new SmartProxy({
+ routes: [
+ createHttpsRoute({
+ ports: 443,
+ domains: 'example.com',
+ target: { host: 'localhost', port: 8080 },
+ certificate: 'auto'
+ })
+ ]
+});
+```
+
+### Migration Steps
+
+1. Replace `domainConfigs` with an array of route configurations using `routes`
+2. Convert each domain configuration to use the new helper functions
+3. Update any code that uses `updateDomainConfigs()` to use `addRoutes()` or `updateRoutes()`
+4. For port-only configurations, create route configurations with port matching only
+5. For SNI-based routing, SNI is now automatically enabled when needed
## Architecture & Flow Diagrams
@@ -309,11 +497,10 @@ flowchart TB
subgraph "SmartProxy Components"
direction TB
- HTTP80["HTTP Port 80
Redirect / SslRedirect"]
+ RouteConfig["Route Configuration
(Match/Action)"]
+ RouteManager["Route Manager"]
HTTPS443["HTTPS Port 443
NetworkProxy"]
SmartProxy["SmartProxy
(TCP/SNI Proxy)"]
- NfTables[NfTablesProxy]
- Router[ProxyRouter]
ACME["Port80Handler
(ACME HTTP-01)"]
Certs[(SSL Certificates)]
end
@@ -324,188 +511,110 @@ flowchart TB
Service3[Service 3]
end
- Client -->|HTTP Request| HTTP80
- HTTP80 -->|Redirect| Client
- Client -->|HTTPS Request| HTTPS443
- Client -->|TLS/TCP| SmartProxy
+ Client -->|HTTP/HTTPS Request| SmartProxy
- HTTPS443 -->|Route Request| Router
- Router -->|Proxy Request| Service1
- Router -->|Proxy Request| Service2
+ SmartProxy -->|Route Matching| RouteManager
+ RouteManager -->|Use| RouteConfig
+ RouteManager -->|Execute Action| SmartProxy
- SmartProxy -->|Direct TCP| Service2
- SmartProxy -->|Direct TCP| Service3
+ SmartProxy -->|Forward| Service1
+ SmartProxy -->|Redirect| Client
+ SmartProxy -->|Forward| Service2
+ SmartProxy -->|Forward| Service3
- NfTables -.->|Low-level forwarding| SmartProxy
-
- HTTP80 -.->|Challenge Response| ACME
ACME -.->|Generate/Manage| Certs
- Certs -.->|Provide TLS Certs| HTTPS443
+ Certs -.->|Provide TLS Certs| SmartProxy
classDef component fill:#f9f,stroke:#333,stroke-width:2px;
classDef backend fill:#bbf,stroke:#333,stroke-width:1px;
classDef client fill:#dfd,stroke:#333,stroke-width:2px;
class Client client;
- class HTTP80,HTTPS443,SmartProxy,NfTables,Router,ACME component;
+ class RouteConfig,RouteManager,HTTPS443,SmartProxy,ACME component;
class Service1,Service2,Service3 backend;
```
-### HTTPS Reverse Proxy Flow
-This diagram shows how HTTPS requests are handled and proxied to backend services:
-
-```mermaid
-sequenceDiagram
- participant Client
- participant NetworkProxy
- participant ProxyRouter
- participant Backend
-
- Client->>NetworkProxy: HTTPS Request
-
- Note over NetworkProxy: TLS Termination
-
- NetworkProxy->>ProxyRouter: Route Request
- ProxyRouter->>ProxyRouter: Match hostname to config
-
- alt Authentication Required
- NetworkProxy->>Client: Request Authentication
- Client->>NetworkProxy: Send Credentials
- NetworkProxy->>NetworkProxy: Validate Credentials
- end
-
- NetworkProxy->>Backend: Forward Request
- Backend->>NetworkProxy: Response
-
- Note over NetworkProxy: Add Default Headers
-
- NetworkProxy->>Client: Forward Response
-
- alt WebSocket Request
- Client->>NetworkProxy: Upgrade to WebSocket
- NetworkProxy->>Backend: Upgrade to WebSocket
- loop WebSocket Active
- Client->>NetworkProxy: WebSocket Message
- NetworkProxy->>Backend: Forward Message
- Backend->>NetworkProxy: WebSocket Message
- NetworkProxy->>Client: Forward Message
- NetworkProxy-->>NetworkProxy: Heartbeat Check
- end
- end
-```
-
-### SNI-based Connection Handling
-This diagram illustrates how TCP connections with SNI (Server Name Indication) are processed and forwarded:
+### Route-Based Connection Handling
+This diagram illustrates how requests are matched and processed using the route-based configuration:
```mermaid
sequenceDiagram
participant Client
participant SmartProxy
+ participant RouteManager
participant Backend
- Client->>SmartProxy: TLS Connection
+ Client->>SmartProxy: Connection (TCP/HTTP/HTTPS)
- alt SNI Enabled
- SmartProxy->>Client: Accept Connection
- Client->>SmartProxy: TLS ClientHello with SNI
- SmartProxy->>SmartProxy: Extract SNI Hostname
- SmartProxy->>SmartProxy: Match Domain Config
- SmartProxy->>SmartProxy: Validate Client IP
+ SmartProxy->>RouteManager: Match connection against routes
+
+ RouteManager->>RouteManager: Check port match
+ RouteManager->>RouteManager: Check domain match (if SNI)
+ RouteManager->>RouteManager: Check path match (if HTTP)
+ RouteManager->>RouteManager: Check client IP match
+ RouteManager->>RouteManager: Check TLS version match
+
+ RouteManager->>RouteManager: Determine highest priority matching route
+
+ alt Forward Action
+ RouteManager->>SmartProxy: Use forward action
- alt IP Allowed
- SmartProxy->>Backend: Forward Connection
- Note over SmartProxy,Backend: Bidirectional Data Flow
- else IP Rejected
- SmartProxy->>Client: Close Connection
+ alt TLS Termination
+ SmartProxy->>SmartProxy: Terminate TLS
+ SmartProxy->>Backend: Forward as HTTP/HTTPS
+ else TLS Passthrough
+ SmartProxy->>Backend: Forward raw TCP
end
- else Port-based Routing
- SmartProxy->>SmartProxy: Match Port Range
- SmartProxy->>SmartProxy: Find Domain Config
- SmartProxy->>SmartProxy: Validate Client IP
- alt IP Allowed
- SmartProxy->>Backend: Forward Connection
- Note over SmartProxy,Backend: Bidirectional Data Flow
- else IP Rejected
- SmartProxy->>Client: Close Connection
- end
+ else Redirect Action
+ RouteManager->>SmartProxy: Use redirect action
+ SmartProxy->>Client: Send redirect response
+
+ else Block Action
+ RouteManager->>SmartProxy: Use block action
+ SmartProxy->>Client: Close connection
end
loop Connection Active
SmartProxy-->>SmartProxy: Monitor Activity
- SmartProxy-->>SmartProxy: Check Max Lifetime
- alt Inactivity or Max Lifetime Exceeded
+ SmartProxy-->>SmartProxy: Check Security Rules
+ alt Security Violation or Timeout
SmartProxy->>Client: Close Connection
SmartProxy->>Backend: Close Connection
end
end
```
-### Let's Encrypt Certificate Acquisition
-This diagram shows how certificates are automatically acquired through the ACME protocol:
-
-```mermaid
-sequenceDiagram
- participant Client
- participant Port80Handler
- participant ACME as Let's Encrypt ACME
- participant NetworkProxy
-
- Client->>Port80Handler: HTTP Request for domain
-
- alt Certificate Exists
- Port80Handler->>Client: Redirect to HTTPS
- else No Certificate
- Port80Handler->>Port80Handler: Mark domain as obtaining cert
- Port80Handler->>ACME: Create account & new order
- ACME->>Port80Handler: Challenge information
-
- Port80Handler->>Port80Handler: Store challenge token & key authorization
-
- ACME->>Port80Handler: HTTP-01 Challenge Request
- Port80Handler->>ACME: Challenge Response
-
- ACME->>ACME: Validate domain ownership
- ACME->>Port80Handler: Challenge validated
-
- Port80Handler->>Port80Handler: Generate CSR
- Port80Handler->>ACME: Submit CSR
- ACME->>Port80Handler: Issue Certificate
-
- Port80Handler->>Port80Handler: Store certificate & private key
- Port80Handler->>Port80Handler: Mark certificate as obtained
-
- Note over Port80Handler,NetworkProxy: Certificate available for use
-
- Client->>Port80Handler: Another HTTP Request
- Port80Handler->>Client: Redirect to HTTPS
- Client->>NetworkProxy: HTTPS Request
- Note over NetworkProxy: Uses new certificate
- end
-```
-
## Features
-- HTTP/HTTPS Reverse Proxy (NetworkProxy)
- • TLS termination, virtual-host routing, HTTP/2 & WebSocket support, pooling & metrics
+- **Route-Based Traffic Management**
+ • Match/action pattern for flexible routing
+ • Port, domain, path, client IP, and TLS version matching
+ • Multiple action types (forward, redirect, block)
-- Automatic ACME Certificates (Port80Handler)
- • HTTP-01 challenge handling, certificate issuance/renewal, pluggable storage
+- **TLS Handling Options**
+ • TLS passthrough for end-to-end encryption
+ • TLS termination for content inspection
+ • TLS termination with re-encryption for gateway scenarios
-- Low-Level Port Forwarding (NfTablesProxy)
- • nftables NAT rules for ports/ranges, IPv4/IPv6, IP filtering, QoS & ipset support
+- **Automatic ACME Certificates**
+ • HTTP-01 challenge handling
+ • Certificate issuance/renewal
+ • Pluggable storage
-- Custom Redirects (Redirect / SslRedirect)
- • URL redirects with wildcard host/path, template variables & status codes
+- **Security Controls**
+ • IP allow/block lists with glob pattern support
+ • Connection limits and rate limiting
+ • Timeout controls and connection monitoring
-- TCP/SNI Proxy (SmartProxy)
- • SNI-based routing, IP allow/block lists, port ranges, timeouts & graceful shutdown
+- **Load Balancing**
+ • Round-robin distribution across multiple backends
+ • Health checks and failure handling
-- SNI Utilities (SniHandler)
- • Robust ClientHello parsing, fragmentation & session resumption support
-
-- Core Utilities
- • ValidationUtils and IpUtils for configuration validation and IP management
+- **Advanced Features**
+ • Custom header manipulation
+ • Template variables for dynamic values
+ • Priority-based route matching
## Certificate Hooks & Events
@@ -520,176 +629,171 @@ Provide a `certProvisionFunction(domain)` in SmartProxy settings to supply stati
## SmartProxy: Common Use Cases
-The SmartProxy component offers a clean, unified approach to handle virtually any proxy scenario.
+The SmartProxy component with route-based configuration offers a clean, unified approach to handle virtually any proxy scenario.
### 1. API Gateway / Backend Routing
-Create a flexible API gateway to route traffic to different microservices based on domain:
+Create a flexible API gateway to route traffic to different microservices based on domain and path:
```typescript
-import { SmartProxy, createDomainConfig, httpOnly, tlsTerminateToHttp } from '@push.rocks/smartproxy';
+import { SmartProxy, createHttpsRoute } from '@push.rocks/smartproxy';
const apiGateway = new SmartProxy({
- fromPort: 443,
- domainConfigs: [
+ routes: [
// Users API
- createDomainConfig('users.api.example.com', tlsTerminateToHttp({
+ createHttpsRoute({
+ ports: 443,
+ domains: 'api.example.com',
+ path: '/users/*',
target: { host: 'users-service', port: 3000 },
- acme: { enabled: true, production: true }
- })),
+ certificate: 'auto'
+ }),
// Products API
- createDomainConfig('products.api.example.com', tlsTerminateToHttp({
+ createHttpsRoute({
+ ports: 443,
+ domains: 'api.example.com',
+ path: '/products/*',
target: { host: 'products-service', port: 3001 },
- acme: { enabled: true, production: true }
- })),
+ certificate: 'auto'
+ }),
- // Admin dashboard gets extra security
- createDomainConfig('admin.example.com', tlsTerminateToHttp({
+ // Admin dashboard with extra security
+ createHttpsRoute({
+ ports: 443,
+ domains: 'admin.example.com',
target: { host: 'admin-dashboard', port: 8080 },
+ certificate: 'auto',
security: {
allowedIps: ['10.0.0.*', '192.168.1.*'] // Only allow internal network
}
- }))
- ],
- sniEnabled: true
+ })
+ ]
});
await apiGateway.start();
```
-### 2. Automatic HTTPS for Development
+### 2. Complete HTTPS Server with HTTP Redirect
-Easily add HTTPS to your local development environment with automatic certificates:
+Easily set up a secure HTTPS server with automatic redirection from HTTP:
```typescript
-import { SmartProxy, createDomainConfig, tlsTerminateToHttp } from '@push.rocks/smartproxy';
+import { SmartProxy, createHttpsServer } from '@push.rocks/smartproxy';
-const devProxy = new SmartProxy({
- fromPort: 443,
- domainConfigs: [
- createDomainConfig('dev.local', tlsTerminateToHttp({
- target: { host: 'localhost', port: 3000 },
- // For development, use self-signed or existing certificates
- https: {
- customCert: {
- key: fs.readFileSync('dev-cert.key', 'utf8'),
- cert: fs.readFileSync('dev-cert.pem', 'utf8')
- }
- },
- // Auto-redirect HTTP to HTTPS
- http: {
- enabled: true,
- redirectToHttps: true
- }
- }))
+const webServer = new SmartProxy({
+ routes: [
+ // createHttpsServer creates both the HTTPS route and HTTP redirect
+ ...createHttpsServer({
+ domains: 'example.com',
+ target: { host: 'localhost', port: 8080 },
+ certificate: 'auto',
+ addHttpRedirect: true
+ })
]
});
-await devProxy.start();
+await webServer.start();
```
-### 3. Load Balancing Multiple Servers
+### 3. Multi-Tenant Application with Wildcard Domains
-Distribute traffic across multiple backend servers with round-robin load balancing:
+Support dynamically created tenants with wildcard domain matching:
```typescript
-import { SmartProxy, createDomainConfig, tlsTerminateToHttp } from '@push.rocks/smartproxy';
+import { SmartProxy, createHttpsRoute, createHttpToHttpsRedirect } from '@push.rocks/smartproxy';
-const loadBalancer = new SmartProxy({
- fromPort: 443,
- domainConfigs: [
- createDomainConfig('app.example.com', tlsTerminateToHttp({
- target: {
- // Round-robin across multiple servers
- host: [
- '10.0.0.10',
- '10.0.0.11',
- '10.0.0.12'
- ],
- port: 8080
- },
- acme: { enabled: true, production: true }
- }))
- ]
-});
-
-await loadBalancer.start();
-```
-
-### 4. Wildcard Subdomain Handling
-
-Support multiple or dynamically created subdomains with one configuration:
-
-```typescript
-import { SmartProxy, createDomainConfig, tlsTerminateToHttp } from '@push.rocks/smartproxy';
-
-const multiTenantProxy = new SmartProxy({
- fromPort: 443,
- domainConfigs: [
- // Handle all customer subdomains with one config
- createDomainConfig('*.example.com', tlsTerminateToHttp({
+const multiTenantApp = new SmartProxy({
+ routes: [
+ // Handle all tenant subdomains with one route
+ createHttpsRoute({
+ ports: 443,
+ domains: '*.example.com',
target: { host: 'tenant-router', port: 8080 },
- acme: { enabled: true, production: true },
+ certificate: 'auto',
// Pass original hostname to backend for tenant identification
advanced: {
headers: {
'X-Original-Host': '{sni}'
}
}
- }))
- ],
- sniEnabled: true
+ }),
+
+ // Redirect HTTP to HTTPS for all subdomains
+ createHttpToHttpsRedirect({
+ domains: ['*.example.com']
+ })
+ ]
});
-await multiTenantProxy.start();
+await multiTenantApp.start();
```
-### 5. Comprehensive Proxy Server
+### 4. Complex Multi-Service Infrastructure
-Create a complete proxy solution with multiple services on a single server:
+Create a comprehensive proxy solution with multiple services and security controls:
```typescript
-import { SmartProxy, createDomainConfig, httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, httpsPassthrough } from '@push.rocks/smartproxy';
+import {
+ SmartProxy,
+ createHttpsRoute,
+ createPassthroughRoute,
+ createBlockRoute,
+ createHttpToHttpsRedirect
+} from '@push.rocks/smartproxy';
const enterpriseProxy = new SmartProxy({
- fromPort: 443,
- domainConfigs: [
+ routes: [
// Web application with automatic HTTPS
- createDomainConfig('app.example.com', tlsTerminateToHttp({
+ createHttpsRoute({
+ ports: 443,
+ domains: 'app.example.com',
target: { host: 'web-app', port: 8080 },
- acme: { enabled: true, production: true },
- http: { enabled: true, redirectToHttps: true }
- })),
+ certificate: 'auto'
+ }),
// Legacy system that needs HTTPS passthrough
- createDomainConfig('legacy.example.com', httpsPassthrough({
+ createPassthroughRoute({
+ ports: 443,
+ domains: 'legacy.example.com',
target: { host: 'legacy-server', port: 443 }
- })),
+ }),
// Internal APIs with IP restrictions
- createDomainConfig('api.internal.example.com', tlsTerminateToHttp({
+ createHttpsRoute({
+ ports: 443,
+ domains: 'api.internal.example.com',
target: { host: 'api-gateway', port: 3000 },
+ certificate: 'auto',
security: {
allowedIps: ['10.0.0.0/16', '192.168.0.0/16'],
maxConnections: 500
}
- })),
+ }),
- // External services with customer certificate
- createDomainConfig('external.example.com', tlsTerminateToHttps({
- target: { host: 'external-service', port: 8443 },
- https: {
- customCert: {
- key: fs.readFileSync('external-key.pem', 'utf8'),
- cert: fs.readFileSync('external-cert.pem', 'utf8')
- }
- }
- }))
+ // Block known malicious IPs
+ createBlockRoute({
+ ports: [80, 443],
+ clientIp: ['1.2.3.*', '5.6.7.*'],
+ priority: 1000
+ }),
+
+ // Redirect all HTTP to HTTPS
+ createHttpToHttpsRedirect({
+ domains: ['*.example.com', 'example.com']
+ })
],
- sniEnabled: true,
+
+ // Global settings that apply to all routes
+ defaults: {
+ security: {
+ maxConnections: 1000
+ }
+ },
+
// Enable connection timeouts for security
inactivityTimeout: 30000,
+
// Using global certificate management
acme: {
enabled: true,
@@ -702,116 +806,86 @@ const enterpriseProxy = new SmartProxy({
await enterpriseProxy.start();
```
-## Unified Forwarding System Details
+## Route-Based Configuration Details
-SmartProxy's unified forwarding system supports four primary forwarding types:
+### Match Criteria Options
-1. **HTTP-only (`http-only`)**: Forwards HTTP traffic to a backend server.
-2. **HTTPS Passthrough (`https-passthrough`)**: Passes through raw TLS traffic without termination (SNI forwarding).
-3. **HTTPS Termination to HTTP (`https-terminate-to-http`)**: Terminates TLS and forwards the decrypted traffic to an HTTP backend.
-4. **HTTPS Termination to HTTPS (`https-terminate-to-https`)**: Terminates TLS and creates a new TLS connection to an HTTPS backend.
+- **ports**: `number | number[] | Array<{ from: number; to: number }>` (required)
+ Listen on specific ports or port ranges
-### Configuration Format
+- **domains**: `string | string[]` (optional)
+ Match specific domain names, supports wildcards (e.g., `*.example.com`)
-Each domain is configured with a forwarding type and target:
+- **path**: `string` (optional)
+ Match specific URL paths, supports glob patterns
+- **clientIp**: `string[]` (optional)
+ Match client IP addresses, supports glob patterns
+
+- **tlsVersion**: `string[]` (optional)
+ Match specific TLS versions (e.g., `TLSv1.2`, `TLSv1.3`)
+
+### Action Types
+
+1. **Forward**:
+ ```typescript
+ {
+ type: 'forward',
+ target: { host: 'localhost', port: 8080 },
+ tls: { mode: 'terminate', certificate: 'auto' }
+ }
+ ```
+
+2. **Redirect**:
+ ```typescript
+ {
+ type: 'redirect',
+ redirect: { to: 'https://{domain}{path}', status: 301 }
+ }
+ ```
+
+3. **Block**:
+ ```typescript
+ {
+ type: 'block'
+ }
+ ```
+
+### TLS Modes
+
+- **passthrough**: Forward raw TLS traffic without decryption
+- **terminate**: Terminate TLS and forward as HTTP
+- **terminate-and-reencrypt**: Terminate TLS and create a new TLS connection to the backend
+
+### Template Variables
+
+Template variables can be used in string values:
+
+- `{domain}`: The requested domain name
+- `{port}`: The incoming port number
+- `{path}`: The requested URL path
+- `{query}`: The query string
+- `{clientIp}`: The client's IP address
+- `{sni}`: The SNI hostname
+
+Example:
```typescript
-{
- domains: ['example.com'], // Single domain or array of domains (with wildcard support)
- forwarding: {
- type: 'http-only', // One of the four forwarding types
- target: {
- host: 'localhost', // Backend server (string or array for load balancing)
- port: 3000 // Backend port
- }
- // Additional options as needed
- }
-}
+createRedirectRoute({
+ domains: 'old.example.com',
+ redirectTo: 'https://new.example.com{path}?source=redirect'
+})
```
-### Helper Functions
-
-Helper functions provide a cleaner syntax for creating configurations:
-
-```typescript
-// Instead of manually specifying the type and format
-const config = createDomainConfig('example.com', httpOnly({
- target: { host: 'localhost', port: 3000 }
-}));
-
-// Available helper functions:
-// - httpOnly() - For HTTP-only traffic
-// - httpsPassthrough() - For SNI-based passthrough
-// - tlsTerminateToHttp() - For HTTPS termination to HTTP
-// - tlsTerminateToHttps() - For HTTPS termination to HTTPS
-```
-
-### Advanced Configuration Options
-
-For more complex scenarios, additional options can be specified:
-
-```typescript
-createDomainConfig('api.example.com', tlsTerminateToHttps({
- // Target configuration with load balancing
- target: {
- host: ['10.0.0.10', '10.0.0.11'], // Round-robin load balancing
- port: 8443
- },
-
- // HTTP options
- http: {
- enabled: true, // Listen on HTTP port
- redirectToHttps: true // Automatically redirect to HTTPS
- },
-
- // HTTPS/TLS options
- https: {
- customCert: { // Provide your own certificate
- key: '-----BEGIN PRIVATE KEY-----\n...',
- cert: '-----BEGIN CERTIFICATE-----\n...'
- },
- forwardSni: true // Forward original SNI to backend
- },
-
- // Let's Encrypt ACME integration
- acme: {
- enabled: true, // Enable automatic certificates
- production: true, // Use production Let's Encrypt
- maintenance: true // Auto-renew certificates
- },
-
- // Security settings
- security: {
- allowedIps: ['10.0.0.*'], // IP allowlist (glob patterns)
- blockedIps: ['1.2.3.4'], // IP blocklist
- maxConnections: 100 // Connection limits
- },
-
- // Advanced settings
- advanced: {
- timeout: 30000, // Connection timeout in ms
- headers: { // Custom headers to backend
- 'X-Forwarded-For': '{clientIp}',
- 'X-Original-Host': '{sni}' // Template variables available
- },
- keepAlive: true // Keep connections alive
- }
-}))
-```
-
-### Extended Configuration Options
-
-#### IForwardConfig
-- `type`: 'http-only' | 'https-passthrough' | 'https-terminate-to-http' | 'https-terminate-to-https'
-- `target`: { host: string | string[], port: number }
-- `http?`: { enabled?: boolean, redirectToHttps?: boolean, headers?: Record }
-- `https?`: { customCert?: { key: string, cert: string }, forwardSni?: boolean }
-- `acme?`: { enabled?: boolean, maintenance?: boolean, production?: boolean, forwardChallenges?: { host: string, port: number, useTls?: boolean } }
-- `security?`: { allowedIps?: string[], blockedIps?: string[], maxConnections?: number }
-- `advanced?`: { portRanges?: Array<{ from: number, to: number }>, networkProxyPort?: number, keepAlive?: boolean, timeout?: number, headers?: Record }
-
## Configuration Options
+### SmartProxy (IRoutedSmartProxyOptions)
+- `routes` (IRouteConfig[], required) - Array of route configurations
+- `defaults` (object) - Default settings for all routes
+- `acme` (IAcmeOptions) - ACME certificate options
+- Connection timeouts: `initialDataTimeout`, `socketTimeout`, `inactivityTimeout`, etc.
+- Socket opts: `noDelay`, `keepAlive`, `enableKeepAliveProbes`
+- `certProvisionFunction` (callback) - Custom certificate provisioning
+
### NetworkProxy (INetworkProxyOptions)
- `port` (number, required)
- `backendProtocol` ('http1'|'http2', default 'http1')
@@ -844,25 +918,22 @@ createDomainConfig('api.example.com', tlsTerminateToHttps({
- `useIPSets` (boolean, default true)
- `qos`, `netProxyIntegration` (objects)
-### Redirect / SslRedirect
-- Constructor options: `httpPort`, `httpsPort`, `sslOptions`, `rules` (IRedirectRule[])
-
-### SmartProxy (ISmartProxyOptions)
-- `fromPort`, `toPort` (number)
-- `domainConfigs` (IDomainConfig[]) - Using unified forwarding configuration
-- `sniEnabled`, `preserveSourceIP` (booleans)
-- `defaultAllowedIPs`, `defaultBlockedIPs` (string[]) - Default IP allowlists/blocklists
-- Timeouts: `initialDataTimeout`, `socketTimeout`, `inactivityTimeout`, etc.
-- Socket opts: `noDelay`, `keepAlive`, `enableKeepAliveProbes`
-- `acme` (IAcmeOptions), `certProvisionFunction` (callback)
-- `useNetworkProxy` (number[]), `networkProxyPort` (number)
-- `globalPortRanges` (Array<{ from: number; to: number }>)
-
## Troubleshooting
+### SmartProxy
+- If routes aren't matching as expected, check their priorities
+- For domain matching issues, verify SNI extraction is working
+- Use higher priority for block routes to ensure they take precedence
+- Enable `enableDetailedLogging` or `enableTlsDebugLogging` for debugging
+
+### TLS/Certificates
+- For certificate issues, check the ACME settings and domain validation
+- Ensure domains are publicly accessible for Let's Encrypt validation
+- For TLS handshake issues, increase `initialDataTimeout` and `maxPendingDataSize`
+
### NetworkProxy
- Verify ports, certificates and `rejectUnauthorized` for TLS errors
-- Configure CORS or use `addDefaultHeaders` for preflight issues
+- Configure CORS for preflight issues
- Increase `maxConnections` or `connectionPoolSize` under load
### Port80Handler
@@ -873,18 +944,6 @@ createDomainConfig('api.example.com', tlsTerminateToHttps({
- Ensure `nft` is installed and run with sufficient privileges
- Use `forceCleanSlate:true` to clear conflicting rules
-### Redirect / SslRedirect
-- Check `fromHost`/`fromPath` patterns and Host headers
-- Validate `sslOptions` key/cert correctness
-
-### SmartProxy & SniHandler
-- Increase `initialDataTimeout`/`maxPendingDataSize` for large ClientHello
-- Enable `enableTlsDebugLogging` to trace handshake
-- Ensure `allowSessionTicket` and fragmentation support for resumption
-- Double-check forwarding configuration to ensure correct `type` for your use case
-- Use helper functions like `httpOnly()`, `httpsPassthrough()`, etc. to create correct configurations
-- For IP filtering issues, check the `security.allowedIps` and `security.blockedIps` settings
-
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
diff --git a/readme.plan.md b/readme.plan.md
index 7f6bde3..3a2e584 100644
--- a/readme.plan.md
+++ b/readme.plan.md
@@ -1,4 +1,4 @@
-# SmartProxy Fully Unified Configuration Plan
+# SmartProxy Fully Unified Configuration Plan (Updated)
## Project Goal
Redesign SmartProxy's configuration for a more elegant, unified, and comprehensible approach by:
@@ -6,6 +6,7 @@ Redesign SmartProxy's configuration for a more elegant, unified, and comprehensi
2. Eliminating the confusion between domain configs and port forwarding
3. Providing a clear, declarative API that makes the intent obvious
4. Enhancing maintainability and extensibility for future features
+5. Completely removing legacy code to eliminate technical debt
## Current Issues
@@ -132,177 +133,14 @@ interface ISmartProxyOptions {
}
```
-### Example Configuration
-
-```typescript
-const proxy = new SmartProxy({
- // All routing is configured in a single array
- routes: [
- // Basic HTTPS server with automatic certificates
- {
- match: {
- ports: 443,
- domains: ['example.com', '*.example.com']
- },
- action: {
- type: 'forward',
- target: {
- host: 'localhost',
- port: 8080
- },
- tls: {
- mode: 'terminate',
- certificate: 'auto' // Use ACME
- }
- },
- name: 'Main HTTPS Server'
- },
-
- // HTTP to HTTPS redirect
- {
- match: {
- ports: 80,
- domains: ['example.com', '*.example.com']
- },
- action: {
- type: 'redirect',
- redirect: {
- to: 'https://{domain}{path}',
- status: 301
- }
- },
- name: 'HTTP to HTTPS Redirect'
- },
-
- // Admin portal with IP restriction
- {
- match: {
- ports: 8443,
- domains: 'admin.example.com'
- },
- action: {
- type: 'forward',
- target: {
- host: 'admin-backend',
- port: 3000
- },
- tls: {
- mode: 'terminate',
- certificate: 'auto'
- },
- security: {
- allowedIps: ['192.168.1.*', '10.0.0.*'],
- maxConnections: 10
- }
- },
- priority: 100, // Higher priority than default rules
- name: 'Admin Portal'
- },
-
- // Port range for direct forwarding
- {
- match: {
- ports: [{ from: 10000, to: 10010 }],
- // No domains = all domains or direct IP
- },
- action: {
- type: 'forward',
- target: {
- host: 'backend-server',
- port: 10000,
- preservePort: true // Use same port number as incoming
- },
- tls: {
- mode: 'passthrough' // Direct TCP forwarding
- }
- },
- name: 'Dynamic Port Range'
- },
-
- // Path-based routing
- {
- match: {
- ports: 443,
- domains: 'api.example.com',
- path: '/v1/*'
- },
- action: {
- type: 'forward',
- target: {
- host: 'api-v1-service',
- port: 8001
- },
- tls: {
- mode: 'terminate'
- }
- },
- name: 'API v1 Endpoints'
- },
-
- // Load balanced backend
- {
- match: {
- ports: 443,
- domains: 'app.example.com'
- },
- action: {
- type: 'forward',
- target: {
- // Round-robin load balancing
- host: [
- 'app-server-1',
- 'app-server-2',
- 'app-server-3'
- ],
- port: 8080
- },
- tls: {
- mode: 'terminate'
- },
- advanced: {
- headers: {
- 'X-Served-By': '{server}',
- 'X-Client-IP': '{clientIp}'
- }
- }
- },
- name: 'Load Balanced App Servers'
- }
- ],
-
- // Global defaults
- defaults: {
- target: {
- host: 'localhost',
- port: 8080
- },
- security: {
- maxConnections: 1000,
- // Global security defaults
- }
- },
-
- // ACME configuration for auto certificates
- acme: {
- enabled: true,
- email: 'admin@example.com',
- production: true,
- renewThresholdDays: 30
- },
-
- // Other global settings
- // ...
-});
-```
-
-## Implementation Plan
+## Revised Implementation Plan
### Phase 1: Core Design & Interface Definition
1. **Define New Core Interfaces**:
- Create `IRouteConfig` interface with `match` and `action` branches
- Define all sub-interfaces for matching and actions
- - Update `ISmartProxyOptions` to use `routes` array
+ - Create new `ISmartProxyOptions` to use `routes` array exclusively
- Define template variable system for dynamic values
2. **Create Helper Functions**:
@@ -319,78 +157,56 @@ const proxy = new SmartProxy({
### Phase 2: Core Implementation
1. **Create RouteManager**:
- - Replaces both PortRangeManager and DomainConfigManager
- - Handles all routing decisions in one place
- - Provides fast lookup from port+domain+path to action
- - Manages server instances for different ports
+ - Build a new RouteManager to replace both PortRangeManager and DomainConfigManager
+ - Implement port and domain matching in one unified system
+ - Create efficient route lookup algorithms
-2. **Update ConnectionHandler**:
- - Simplify to work with the unified route system
- - Implement templating system for dynamic values
- - Support more sophisticated routing rules
+2. **Implement New ConnectionHandler**:
+ - Create a new ConnectionHandler built from scratch for routes
+ - Implement the routing logic with the new match/action pattern
+ - Support template processing for headers and other dynamic values
3. **Implement New SmartProxy Core**:
- - Rewrite initialization to use route-based configuration
- - Create network servers based on port specifications
+ - Create new SmartProxy implementation using routes exclusively
+ - Build network servers based on port specifications
- Manage TLS contexts and certificates
-### Phase 3: Feature Implementation
+### Phase 3: Legacy Code Removal
-1. **Path-Based Routing**:
- - Implement HTTP path matching
- - Add wildcard and regex support for paths
- - Create route differentiation based on HTTP method
+1. **Identify Legacy Components**:
+ - Create an inventory of all files and components to be removed
+ - Document dependencies between legacy components
+ - Create a removal plan that minimizes disruption
-2. **Enhanced Security**:
- - Implement per-route security rules
- - Add authentication methods (basic, digest, etc.)
- - Support for IP-based access control
+2. **Remove Legacy Components**:
+ - Remove PortRangeManager and related code
+ - Remove DomainConfigManager and related code
+ - Remove old ConnectionHandler implementation
+ - Remove other legacy components
-3. **TLS Management**:
- - Support multiple certificate types (auto, manual, wildcard)
- - Implement certificate selection based on SNI
- - Support different TLS modes per route
+3. **Clean Interface Adaptations**:
+ - Remove all legacy interfaces and types
+ - Update type exports to only expose route-based interfaces
+ - Remove any adapter or backward compatibility code
-4. **Metrics & Monitoring**:
- - Per-route statistics
- - Named route tracking for better visibility
- - Tag-based grouping of metrics
-
-### Phase 4: Backward Compatibility
-
-1. **Legacy Adapter Layer**:
- - Convert old configuration format to route-based format
- - Map fromPort/toPort/domainConfigs to unified routes
- - Preserve all functionality during migration
-
-2. **API Compatibility**:
- - Support both old and new configuration methods
- - Add deprecation warnings when using legacy properties
- - Provide migration utilities
-
-3. **Documentation**:
- - Clear migration guide for existing users
- - Examples mapping old config to new config
- - Timeline for deprecation
-
-### Phase 5: Documentation & Examples
+### Phase 4: Updated Documentation & Examples
1. **Update Core Documentation**:
- - Rewrite README.md with a focus on route-based configuration
+ - Rewrite README.md with a focus on route-based configuration exclusively
- Create interface reference documentation
- Document all template variables
2. **Create Example Library**:
- - Common configuration patterns
+ - Common configuration patterns using the new API
- Complex use cases for advanced features
- Infrastructure-as-code examples
-3. **Add Validation Tooling**:
+3. **Add Validation Tools**:
- Configuration validator to check for issues
- Schema definitions for IDE autocomplete
- Runtime validation helpers
-### Phase 6: Testing
+### Phase 5: Testing
1. **Unit Tests**:
- Test route matching logic
@@ -398,378 +214,103 @@ const proxy = new SmartProxy({
- Test template processing
2. **Integration Tests**:
- - Verify full proxy flows
+ - Verify full proxy flows with the new system
- Test complex routing scenarios
- - Check backward compatibility
+ - Ensure all features work as expected
3. **Performance Testing**:
- Benchmark routing performance
- Evaluate memory usage
- Test with large numbers of routes
-## Benefits of the New Approach
-
-1. **Truly Unified Configuration**:
- - One "source of truth" for all routing
- - Entire routing flow visible in a single configuration
- - No overlapping or conflicting configuration systems
-
-2. **Declarative Intent**:
- - Configuration clearly states what to match and what action to take
- - Metadata provides context and documentation inline
- - Easy to understand the purpose of each route
-
-3. **Advanced Routing Capabilities**:
- - Path-based routing with pattern matching
- - Client IP-based conditional routing
- - Fine-grained security controls
-
-4. **Composable and Extensible**:
- - Each route is a self-contained unit of configuration
- - Routes can be grouped by tags or priority
- - New match criteria or actions can be added without breaking changes
-
-5. **Better Developer Experience**:
- - Clear, consistent configuration pattern
- - Helper functions for common scenarios
- - Better error messages and validation
-
-## Example Use Cases
-
-### 1. Complete Reverse Proxy with Auto SSL
-
-```typescript
-const proxy = new SmartProxy({
- routes: [
- // HTTPS server for all domains
- {
- match: { ports: 443 },
- action: {
- type: 'forward',
- target: { host: 'localhost', port: 8080 },
- tls: { mode: 'terminate', certificate: 'auto' }
- }
- },
- // HTTP to HTTPS redirect
- {
- match: { ports: 80 },
- action: {
- type: 'redirect',
- redirect: { to: 'https://{domain}{path}', status: 301 }
- }
- }
- ],
- acme: {
- enabled: true,
- email: 'admin@example.com',
- production: true
- }
-});
-```
-
-### 2. Microservices API Gateway
-
-```typescript
-const apiGateway = new SmartProxy({
- routes: [
- // Users API
- {
- match: {
- ports: 443,
- domains: 'api.example.com',
- path: '/users/*'
- },
- action: {
- type: 'forward',
- target: { host: 'users-service', port: 8001 },
- tls: { mode: 'terminate', certificate: 'auto' }
- },
- name: 'Users Service'
- },
- // Products API
- {
- match: {
- ports: 443,
- domains: 'api.example.com',
- path: '/products/*'
- },
- action: {
- type: 'forward',
- target: { host: 'products-service', port: 8002 },
- tls: { mode: 'terminate', certificate: 'auto' }
- },
- name: 'Products Service'
- },
- // Orders API with authentication
- {
- match: {
- ports: 443,
- domains: 'api.example.com',
- path: '/orders/*'
- },
- action: {
- type: 'forward',
- target: { host: 'orders-service', port: 8003 },
- tls: { mode: 'terminate', certificate: 'auto' },
- security: {
- authentication: {
- type: 'basic',
- users: { 'admin': 'password' }
- }
- }
- },
- name: 'Orders Service (Protected)'
- }
- ],
- acme: {
- enabled: true,
- email: 'admin@example.com'
- }
-});
-```
-
-### 3. Multi-Environment Setup
-
-```typescript
-const environments = {
- production: {
- target: { host: 'prod-backend', port: 8080 },
- security: { maxConnections: 1000 }
- },
- staging: {
- target: { host: 'staging-backend', port: 8080 },
- security: {
- allowedIps: ['10.0.0.*', '192.168.1.*'],
- maxConnections: 100
- }
- },
- development: {
- target: { host: 'localhost', port: 3000 },
- security: {
- allowedIps: ['127.0.0.1'],
- maxConnections: 10
- }
- }
-};
-
-// Select environment based on hostname
-const proxy = new SmartProxy({
- routes: [
- // Production environment
- {
- match: {
- ports: [80, 443],
- domains: ['app.example.com', 'www.example.com']
- },
- action: {
- type: 'forward',
- target: environments.production.target,
- tls: { mode: 'terminate', certificate: 'auto' },
- security: environments.production.security
- },
- name: 'Production Environment'
- },
- // Staging environment
- {
- match: {
- ports: [80, 443],
- domains: 'staging.example.com'
- },
- action: {
- type: 'forward',
- target: environments.staging.target,
- tls: { mode: 'terminate', certificate: 'auto' },
- security: environments.staging.security
- },
- name: 'Staging Environment'
- },
- // Development environment
- {
- match: {
- ports: [80, 443],
- domains: 'dev.example.com'
- },
- action: {
- type: 'forward',
- target: environments.development.target,
- tls: { mode: 'terminate', certificate: 'auto' },
- security: environments.development.security
- },
- name: 'Development Environment'
- }
- ],
- acme: { enabled: true, email: 'admin@example.com' }
-});
-```
-
## Implementation Strategy
### Code Organization
1. **New Files**:
- - `route-manager.ts` (core routing engine)
- - `route-types.ts` (interface definitions)
- - `route-helpers.ts` (helper functions)
- - `route-matcher.ts` (matching logic)
- - `template-engine.ts` (for variable substitution)
+ - `route-config.ts` - Core route interfaces
+ - `route-manager.ts` - Route matching and management
+ - `route-connection-handler.ts` - Connection handling with routes
+ - `route-smart-proxy.ts` - Main SmartProxy implementation
+ - `template-engine.ts` - For variable substitution
-2. **Modified Files**:
- - `smart-proxy.ts` (update to use route-based configuration)
- - `connection-handler.ts` (simplify using route-based approach)
- - Replace `port-range-manager.ts` and `domain-config-manager.ts`
+2. **File Removal**:
+ - Remove `port-range-manager.ts`
+ - Remove `domain-config-manager.ts`
+ - Remove legacy interfaces and adapter code
+ - Remove backward compatibility shims
-### Backward Compatibility
+### Transition Strategy
-The backward compatibility layer will convert the legacy configuration to the new format:
+1. **Breaking Change Approach**:
+ - This will be a major version update with breaking changes
+ - No backward compatibility will be maintained
+ - Clear migration documentation will guide users to the new API
-```typescript
-function convertLegacyConfig(legacy: ILegacySmartProxyOptions): ISmartProxyOptions {
- const routes: IRouteConfig[] = [];
-
- // Convert main port configuration
- if (legacy.fromPort) {
- // Add main listener for fromPort
- routes.push({
- match: { ports: legacy.fromPort },
- action: {
- type: 'forward',
- target: { host: legacy.targetIP || 'localhost', port: legacy.toPort },
- tls: { mode: legacy.sniEnabled ? 'passthrough' : 'terminate' }
- },
- name: 'Main Listener (Legacy)'
- });
-
- // If ACME is enabled, add HTTP listener for challenges
- if (legacy.acme?.enabled) {
- routes.push({
- match: { ports: 80 },
- action: {
- type: 'forward',
- target: { host: 'localhost', port: 80 },
- // Special flag for ACME handler
- acmeEnabled: true
- },
- name: 'ACME Challenge Handler (Legacy)'
- });
- }
- }
-
- // Convert domain configs
- if (legacy.domainConfigs) {
- for (const domainConfig of legacy.domainConfigs) {
- const { domains, forwarding } = domainConfig;
-
- // Determine action based on forwarding type
- let action: Partial = {
- type: 'forward',
- target: {
- host: forwarding.target.host,
- port: forwarding.target.port
- }
- };
-
- // Set TLS mode based on forwarding type
- switch (forwarding.type) {
- case 'http-only':
- // No TLS
- break;
- case 'https-passthrough':
- action.tls = { mode: 'passthrough' };
- break;
- case 'https-terminate-to-http':
- action.tls = {
- mode: 'terminate',
- certificate: forwarding.https?.customCert || 'auto'
- };
- break;
- case 'https-terminate-to-https':
- action.tls = {
- mode: 'terminate-and-reencrypt',
- certificate: forwarding.https?.customCert || 'auto'
- };
- break;
- }
-
- // Security settings
- if (forwarding.security) {
- action.security = forwarding.security;
- }
-
- // Add HTTP redirect if needed
- if (forwarding.http?.redirectToHttps) {
- routes.push({
- match: { ports: 80, domains },
- action: {
- type: 'redirect',
- redirect: { to: 'https://{domain}{path}', status: 301 }
- },
- name: `HTTP Redirect for ${domains.join(', ')} (Legacy)`
- });
- }
-
- // Add main route
- routes.push({
- match: {
- ports: forwarding.type.startsWith('https') ? 443 : 80,
- domains
- },
- action: action as IRouteAction,
- name: `Route for ${domains.join(', ')} (Legacy)`
- });
-
- // Add port ranges if specified
- if (forwarding.advanced?.portRanges) {
- for (const range of forwarding.advanced.portRanges) {
- routes.push({
- match: {
- ports: { from: range.from, to: range.to },
- domains
- },
- action: action as IRouteAction,
- name: `Port Range ${range.from}-${range.to} for ${domains.join(', ')} (Legacy)`
- });
- }
- }
- }
- }
-
- // Global port ranges
- if (legacy.globalPortRanges) {
- for (const range of legacy.globalPortRanges) {
- routes.push({
- match: { ports: { from: range.from, to: range.to } },
- action: {
- type: 'forward',
- target: {
- host: legacy.targetIP || 'localhost',
- port: legacy.forwardAllGlobalRanges ? 0 : legacy.toPort,
- preservePort: !!legacy.forwardAllGlobalRanges
- },
- tls: { mode: 'passthrough' }
- },
- name: `Global Port Range ${range.from}-${range.to} (Legacy)`
- });
- }
- }
-
- return {
- routes,
- defaults: {
- target: {
- host: legacy.targetIP || 'localhost',
- port: legacy.toPort
- }
- },
- acme: legacy.acme
- };
-}
-```
+2. **Package Structure**:
+ - `@push.rocks/smartproxy` package will be updated to v14.0.0
+ - Legacy code fully removed, only route-based API exposed
+ - Support documentation provided for migration
-## Success Criteria
+3. **Migration Documentation**:
+ - Provide a migration guide with examples
+ - Show equivalent route configurations for common legacy patterns
+ - Offer code transformation helpers for complex setups
-- All existing functionality works with the new route-based configuration
-- Performance is equal or better than the current implementation
-- Configuration is more intuitive and easier to understand
-- New features can be added without breaking existing code
-- Code is more maintainable with clear separation of concerns
-- Migration from old configuration to new is straightforward
\ No newline at end of file
+## Benefits of the Clean Approach
+
+1. **Reduced Complexity**:
+ - No overlapping or conflicting configuration systems
+ - No dual maintenance of backward compatibility code
+ - Simplified internal architecture
+
+2. **Cleaner Code Base**:
+ - Removal of technical debt
+ - Better separation of concerns
+ - More maintainable codebase
+
+3. **Better User Experience**:
+ - Consistent, predictable API
+ - No confusing overlapping options
+ - Clear documentation of one approach, not two
+
+4. **Future-Proof Design**:
+ - Easier to extend with new features
+ - Better performance without legacy overhead
+ - Cleaner foundation for future enhancements
+
+## Migration Support
+
+While we're removing backward compatibility from the codebase, we'll provide extensive migration support:
+
+1. **Migration Guide**:
+ - Detailed documentation on moving from legacy to route-based config
+ - Pattern-matching examples for all common use cases
+ - Troubleshooting guide for common migration issues
+
+2. **Conversion Tool**:
+ - Provide a standalone one-time conversion tool
+ - Takes legacy configuration and outputs route-based equivalents
+ - Will not be included in the main package to avoid bloat
+
+3. **Version Policy**:
+ - Maintain the legacy version (13.x) for security updates
+ - Make the route-based version a clear major version change (14.0.0)
+ - Clearly communicate the breaking changes
+
+## Timeline and Versioning
+
+1. **Development**:
+ - Develop route-based implementation in a separate branch
+ - Complete full test coverage of new implementation
+ - Ensure documentation is complete
+
+2. **Release**:
+ - Release as version 14.0.0
+ - Clearly mark as breaking change
+ - Provide migration guide at release time
+
+3. **Support**:
+ - Offer extended support for migration questions
+ - Consider maintaining security updates for v13.x for 6 months
+ - Focus active development on route-based version only
\ No newline at end of file
diff --git a/test/test.route-config.ts b/test/test.route-config.ts
new file mode 100644
index 0000000..133001f
--- /dev/null
+++ b/test/test.route-config.ts
@@ -0,0 +1,181 @@
+/**
+ * Tests for the new route-based configuration system
+ */
+import { expect, tap } from '@push.rocks/tapbundle';
+
+// Import from core modules
+import {
+ SmartProxy,
+ createHttpRoute,
+ createHttpsRoute,
+ createPassthroughRoute,
+ createRedirectRoute,
+ createHttpToHttpsRedirect,
+ createHttpsServer,
+ createLoadBalancerRoute
+} from '../ts/proxies/smart-proxy/index.js';
+
+// Import test helpers
+import { getCertificate } from './helpers/certificates.js';
+
+tap.test('Routes: Should create basic HTTP route', async () => {
+ // Create a simple HTTP route
+ const httpRoute = createHttpRoute({
+ ports: 8080,
+ domains: 'example.com',
+ target: {
+ host: 'localhost',
+ port: 3000
+ },
+ name: 'Basic HTTP Route'
+ });
+
+ // Validate the route configuration
+ expect(httpRoute.match.ports).to.equal(8080);
+ expect(httpRoute.match.domains).to.equal('example.com');
+ expect(httpRoute.action.type).to.equal('forward');
+ expect(httpRoute.action.target?.host).to.equal('localhost');
+ expect(httpRoute.action.target?.port).to.equal(3000);
+ expect(httpRoute.name).to.equal('Basic HTTP Route');
+});
+
+tap.test('Routes: Should create HTTPS route with TLS termination', async () => {
+ // Create an HTTPS route with TLS termination
+ const httpsRoute = createHttpsRoute({
+ domains: 'secure.example.com',
+ target: {
+ host: 'localhost',
+ port: 8080
+ },
+ certificate: 'auto',
+ name: 'HTTPS Route'
+ });
+
+ // Validate the route configuration
+ expect(httpsRoute.match.ports).to.equal(443); // Default HTTPS port
+ expect(httpsRoute.match.domains).to.equal('secure.example.com');
+ expect(httpsRoute.action.type).to.equal('forward');
+ expect(httpsRoute.action.tls?.mode).to.equal('terminate');
+ expect(httpsRoute.action.tls?.certificate).to.equal('auto');
+ expect(httpsRoute.action.target?.host).to.equal('localhost');
+ expect(httpsRoute.action.target?.port).to.equal(8080);
+ expect(httpsRoute.name).to.equal('HTTPS Route');
+});
+
+tap.test('Routes: Should create HTTP to HTTPS redirect', async () => {
+ // Create an HTTP to HTTPS redirect
+ const redirectRoute = createHttpToHttpsRedirect({
+ domains: 'example.com',
+ statusCode: 301
+ });
+
+ // Validate the route configuration
+ expect(redirectRoute.match.ports).to.equal(80);
+ expect(redirectRoute.match.domains).to.equal('example.com');
+ expect(redirectRoute.action.type).to.equal('redirect');
+ expect(redirectRoute.action.redirect?.to).to.equal('https://{domain}{path}');
+ expect(redirectRoute.action.redirect?.status).to.equal(301);
+});
+
+tap.test('Routes: Should create complete HTTPS server with redirects', async () => {
+ // Create a complete HTTPS server setup
+ const routes = createHttpsServer({
+ domains: 'example.com',
+ target: {
+ host: 'localhost',
+ port: 8080
+ },
+ certificate: 'auto',
+ addHttpRedirect: true
+ });
+
+ // Validate that we got two routes (HTTPS route and HTTP redirect)
+ expect(routes.length).to.equal(2);
+
+ // Validate HTTPS route
+ const httpsRoute = routes[0];
+ expect(httpsRoute.match.ports).to.equal(443);
+ expect(httpsRoute.match.domains).to.equal('example.com');
+ expect(httpsRoute.action.type).to.equal('forward');
+ expect(httpsRoute.action.tls?.mode).to.equal('terminate');
+
+ // Validate HTTP redirect route
+ const redirectRoute = routes[1];
+ expect(redirectRoute.match.ports).to.equal(80);
+ expect(redirectRoute.action.type).to.equal('redirect');
+ expect(redirectRoute.action.redirect?.to).to.equal('https://{domain}{path}');
+});
+
+tap.test('Routes: Should create load balancer route', async () => {
+ // Create a load balancer route
+ const lbRoute = createLoadBalancerRoute({
+ domains: 'app.example.com',
+ targets: ['10.0.0.1', '10.0.0.2', '10.0.0.3'],
+ targetPort: 8080,
+ tlsMode: 'terminate',
+ certificate: 'auto',
+ name: 'Load Balanced Route'
+ });
+
+ // Validate the route configuration
+ expect(lbRoute.match.domains).to.equal('app.example.com');
+ expect(lbRoute.action.type).to.equal('forward');
+ expect(Array.isArray(lbRoute.action.target?.host)).to.equal(true);
+ expect((lbRoute.action.target?.host as string[]).length).to.equal(3);
+ expect((lbRoute.action.target?.host as string[])[0]).to.equal('10.0.0.1');
+ expect(lbRoute.action.target?.port).to.equal(8080);
+ expect(lbRoute.action.tls?.mode).to.equal('terminate');
+});
+
+tap.test('SmartProxy: Should create instance with route-based config', async () => {
+ // Create TLS certificates for testing
+ const cert = await getCertificate();
+
+ // Create a SmartProxy instance with route-based configuration
+ const proxy = new SmartProxy({
+ routes: [
+ createHttpRoute({
+ ports: 8080,
+ domains: 'example.com',
+ target: {
+ host: 'localhost',
+ port: 3000
+ },
+ name: 'HTTP Route'
+ }),
+ createHttpsRoute({
+ domains: 'secure.example.com',
+ target: {
+ host: 'localhost',
+ port: 8443
+ },
+ certificate: {
+ key: cert.key,
+ cert: cert.cert
+ },
+ name: 'HTTPS Route'
+ })
+ ],
+ defaults: {
+ target: {
+ host: 'localhost',
+ port: 8080
+ },
+ security: {
+ allowedIPs: ['127.0.0.1', '192.168.0.*'],
+ maxConnections: 100
+ }
+ },
+ // Additional settings
+ initialDataTimeout: 10000,
+ inactivityTimeout: 300000,
+ enableDetailedLogging: true
+ });
+
+ // Simply verify the instance was created successfully
+ expect(proxy).to.be.an('object');
+ expect(proxy.start).to.be.a('function');
+ expect(proxy.stop).to.be.a('function');
+});
+
+export default tap.start();
\ No newline at end of file
diff --git a/test/test.smartproxy.ts b/test/test.smartproxy.ts
index ba1234b..4fde61b 100644
--- a/test/test.smartproxy.ts
+++ b/test/test.smartproxy.ts
@@ -66,13 +66,25 @@ function createTestClient(port: number, data: string): Promise {
tap.test('setup port proxy test environment', async () => {
testServer = await createTestServer(TEST_SERVER_PORT);
smartProxy = new SmartProxy({
- fromPort: PROXY_PORT,
- toPort: TEST_SERVER_PORT,
- targetIP: 'localhost',
- domainConfigs: [],
- sniEnabled: false,
- defaultAllowedIPs: ['127.0.0.1'],
- globalPortRanges: []
+ routes: [
+ {
+ match: {
+ ports: PROXY_PORT
+ },
+ action: {
+ type: 'forward',
+ target: {
+ host: 'localhost',
+ port: TEST_SERVER_PORT
+ }
+ }
+ }
+ ],
+ defaults: {
+ security: {
+ allowedIPs: ['127.0.0.1']
+ }
+ }
});
allProxies.push(smartProxy); // Track this proxy
});
@@ -92,13 +104,25 @@ tap.test('should forward TCP connections and data to localhost', async () => {
// Test proxy with a custom target host.
tap.test('should forward TCP connections to custom host', async () => {
const customHostProxy = new SmartProxy({
- fromPort: PROXY_PORT + 1,
- toPort: TEST_SERVER_PORT,
- targetIP: '127.0.0.1',
- domainConfigs: [],
- sniEnabled: false,
- defaultAllowedIPs: ['127.0.0.1'],
- globalPortRanges: []
+ routes: [
+ {
+ match: {
+ ports: PROXY_PORT + 1
+ },
+ action: {
+ type: 'forward',
+ target: {
+ host: '127.0.0.1',
+ port: TEST_SERVER_PORT
+ }
+ }
+ }
+ ],
+ defaults: {
+ security: {
+ allowedIPs: ['127.0.0.1']
+ }
+ }
});
allProxies.push(customHostProxy); // Track this proxy
@@ -125,14 +149,25 @@ tap.test('should forward connections to custom IP', async () => {
// We're simulating routing to a different IP by using a different port
// This tests the core functionality without requiring multiple IPs
const domainProxy = new SmartProxy({
- fromPort: forcedProxyPort, // 4003 - Listen on this port
- toPort: targetServerPort, // 4200 - Forward to this port
- targetIP: '127.0.0.1', // Always use localhost (works in Docker)
- domainConfigs: [], // No domain configs to confuse things
- sniEnabled: false,
- defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], // Allow localhost
- // We'll test the functionality WITHOUT port ranges this time
- globalPortRanges: []
+ routes: [
+ {
+ match: {
+ ports: forcedProxyPort
+ },
+ action: {
+ type: 'forward',
+ target: {
+ host: '127.0.0.1',
+ port: targetServerPort
+ }
+ }
+ }
+ ],
+ defaults: {
+ security: {
+ allowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
+ }
+ }
});
allProxies.push(domainProxy); // Track this proxy
@@ -208,22 +243,46 @@ tap.test('should stop port proxy', async () => {
tap.test('should support optional source IP preservation in chained proxies', async () => {
// Chained proxies without IP preservation.
const firstProxyDefault = new SmartProxy({
- fromPort: PROXY_PORT + 4,
- toPort: PROXY_PORT + 5,
- targetIP: 'localhost',
- domainConfigs: [],
- sniEnabled: false,
- defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
- globalPortRanges: []
+ routes: [
+ {
+ match: {
+ ports: PROXY_PORT + 4
+ },
+ action: {
+ type: 'forward',
+ target: {
+ host: 'localhost',
+ port: PROXY_PORT + 5
+ }
+ }
+ }
+ ],
+ defaults: {
+ security: {
+ allowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
+ }
+ }
});
const secondProxyDefault = new SmartProxy({
- fromPort: PROXY_PORT + 5,
- toPort: TEST_SERVER_PORT,
- targetIP: 'localhost',
- domainConfigs: [],
- sniEnabled: false,
- defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
- globalPortRanges: []
+ routes: [
+ {
+ match: {
+ ports: PROXY_PORT + 5
+ },
+ action: {
+ type: 'forward',
+ target: {
+ host: 'localhost',
+ port: TEST_SERVER_PORT
+ }
+ }
+ }
+ ],
+ defaults: {
+ security: {
+ allowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
+ }
+ }
});
allProxies.push(firstProxyDefault, secondProxyDefault); // Track these proxies
@@ -243,24 +302,50 @@ tap.test('should support optional source IP preservation in chained proxies', as
// Chained proxies with IP preservation.
const firstProxyPreserved = new SmartProxy({
- fromPort: PROXY_PORT + 6,
- toPort: PROXY_PORT + 7,
- targetIP: 'localhost',
- domainConfigs: [],
- sniEnabled: false,
- defaultAllowedIPs: ['127.0.0.1'],
- preserveSourceIP: true,
- globalPortRanges: []
+ routes: [
+ {
+ match: {
+ ports: PROXY_PORT + 6
+ },
+ action: {
+ type: 'forward',
+ target: {
+ host: 'localhost',
+ port: PROXY_PORT + 7
+ }
+ }
+ }
+ ],
+ defaults: {
+ security: {
+ allowedIPs: ['127.0.0.1']
+ },
+ preserveSourceIP: true
+ },
+ preserveSourceIP: true
});
const secondProxyPreserved = new SmartProxy({
- fromPort: PROXY_PORT + 7,
- toPort: TEST_SERVER_PORT,
- targetIP: 'localhost',
- domainConfigs: [],
- sniEnabled: false,
- defaultAllowedIPs: ['127.0.0.1'],
- preserveSourceIP: true,
- globalPortRanges: []
+ routes: [
+ {
+ match: {
+ ports: PROXY_PORT + 7
+ },
+ action: {
+ type: 'forward',
+ target: {
+ host: 'localhost',
+ port: TEST_SERVER_PORT
+ }
+ }
+ }
+ ],
+ defaults: {
+ security: {
+ allowedIPs: ['127.0.0.1']
+ },
+ preserveSourceIP: true
+ },
+ preserveSourceIP: true
});
allProxies.push(firstProxyPreserved, secondProxyPreserved); // Track these proxies
diff --git a/ts/proxies/smart-proxy/index.ts b/ts/proxies/smart-proxy/index.ts
index 8642efc..eabf015 100644
--- a/ts/proxies/smart-proxy/index.ts
+++ b/ts/proxies/smart-proxy/index.ts
@@ -1,5 +1,7 @@
/**
* SmartProxy implementation
+ *
+ * Version 14.0.0: Unified Route-Based Configuration API
*/
// Re-export models
export * from './models/index.js';
@@ -7,12 +9,26 @@ export * from './models/index.js';
// Export the main SmartProxy class
export { SmartProxy } from './smart-proxy.js';
-// Export supporting classes
+// Export core supporting classes
export { ConnectionManager } from './connection-manager.js';
export { SecurityManager } from './security-manager.js';
-export { DomainConfigManager } from './domain-config-manager.js';
export { TimeoutManager } from './timeout-manager.js';
export { TlsManager } from './tls-manager.js';
export { NetworkProxyBridge } from './network-proxy-bridge.js';
-export { PortRangeManager } from './port-range-manager.js';
-export { ConnectionHandler } from './connection-handler.js';
+
+// Export route-based components
+export { RouteManager } from './route-manager.js';
+export { RouteConnectionHandler } from './route-connection-handler.js';
+
+// Export route helpers for configuration
+export {
+ createRoute,
+ createHttpRoute,
+ createHttpsRoute,
+ createPassthroughRoute,
+ createRedirectRoute,
+ createHttpToHttpsRedirect,
+ createBlockRoute,
+ createLoadBalancerRoute,
+ createHttpsServer
+} from './route-helpers.js';
diff --git a/ts/proxies/smart-proxy/models/index.ts b/ts/proxies/smart-proxy/models/index.ts
index 33a81b2..2e5380f 100644
--- a/ts/proxies/smart-proxy/models/index.ts
+++ b/ts/proxies/smart-proxy/models/index.ts
@@ -2,3 +2,7 @@
* SmartProxy models
*/
export * from './interfaces.js';
+export * from './route-types.js';
+
+// Re-export IRoutedSmartProxyOptions explicitly to avoid ambiguity
+export type { ISmartProxyOptions as IRoutedSmartProxyOptions } from './interfaces.js';
diff --git a/ts/proxies/smart-proxy/models/interfaces.ts b/ts/proxies/smart-proxy/models/interfaces.ts
index edbba8a..c2ad26b 100644
--- a/ts/proxies/smart-proxy/models/interfaces.ts
+++ b/ts/proxies/smart-proxy/models/interfaces.ts
@@ -1,5 +1,7 @@
import * as plugins from '../../../plugins.js';
-import type { IForwardConfig } from '../../../forwarding/config/forwarding-types.js';
+import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
+import type { IRouteConfig } from './route-types.js';
+import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
/**
* Provision object for static or HTTP-01 certificate
@@ -7,27 +9,102 @@ import type { IForwardConfig } from '../../../forwarding/config/forwarding-types
export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01';
/**
- * Domain configuration with forwarding configuration
+ * Alias for backward compatibility with code that uses IRoutedSmartProxyOptions
+ */
+export type IRoutedSmartProxyOptions = ISmartProxyOptions;
+
+/**
+ * Legacy domain configuration interface for backward compatibility
*/
export interface IDomainConfig {
- domains: string[]; // Glob patterns for domain(s)
- forwarding: IForwardConfig; // Unified forwarding configuration
+ domains: string[];
+ forwarding: {
+ type: TForwardingType;
+ target: {
+ host: string | string[];
+ port: number;
+ };
+ acme?: {
+ enabled?: boolean;
+ maintenance?: boolean;
+ production?: boolean;
+ forwardChallenges?: {
+ host: string;
+ port: number;
+ useTls?: boolean;
+ };
+ };
+ http?: {
+ enabled?: boolean;
+ redirectToHttps?: boolean;
+ headers?: Record;
+ };
+ https?: {
+ customCert?: {
+ key: string;
+ cert: string;
+ };
+ forwardSni?: boolean;
+ };
+ security?: {
+ allowedIps?: string[];
+ blockedIps?: string[];
+ maxConnections?: number;
+ };
+ advanced?: {
+ portRanges?: Array<{ from: number; to: number }>;
+ networkProxyPort?: number;
+ keepAlive?: boolean;
+ timeout?: number;
+ headers?: Record;
+ };
+ };
}
/**
- * Configuration options for the SmartProxy
+ * Helper functions for type checking - now always assume route-based config
+ */
+export function isLegacyOptions(options: any): boolean {
+ return false; // No longer supporting legacy options
+}
+
+export function isRoutedOptions(options: any): boolean {
+ return true; // Always assume routed options
+}
+
+/**
+ * SmartProxy configuration options
*/
-import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
export interface ISmartProxyOptions {
- fromPort: number;
- toPort: number;
- targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
- domainConfigs: IDomainConfig[];
+ // The unified configuration array (required)
+ routes: IRouteConfig[];
+
+ // Legacy options for backward compatibility
+ fromPort?: number;
+ toPort?: number;
sniEnabled?: boolean;
+ domainConfigs?: IDomainConfig[];
+ targetIP?: string;
defaultAllowedIPs?: string[];
defaultBlockedIPs?: string[];
+ globalPortRanges?: Array<{ from: number; to: number }>;
+ forwardAllGlobalRanges?: boolean;
preserveSourceIP?: boolean;
+ // Global/default settings
+ defaults?: {
+ target?: {
+ host: string; // Default host to use when not specified in routes
+ port: number; // Default port to use when not specified in routes
+ };
+ security?: {
+ allowedIPs?: string[]; // Default allowed IPs
+ blockedIPs?: string[]; // Default blocked IPs
+ maxConnections?: number; // Default max connections
+ };
+ preserveSourceIP?: boolean; // Default source IP preservation
+ };
+
// TLS options
pfx?: Buffer;
key?: string | Buffer | Array;
@@ -50,8 +127,6 @@ export interface ISmartProxyOptions {
inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h)
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
- globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
- forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
// Socket optimization settings
noDelay?: boolean; // Disable Nagle's algorithm (default: true)
@@ -108,6 +183,9 @@ export interface IConnectionRecord {
pendingData: Buffer[]; // Buffer to hold data during connection setup
pendingDataSize: number; // Track total size of pending data
+ // Legacy property for backward compatibility
+ domainConfig?: IDomainConfig;
+
// Enhanced tracking fields
bytesReceived: number; // Total bytes received
bytesSent: number; // Total bytes sent
@@ -116,7 +194,7 @@ export interface IConnectionRecord {
isTLS: boolean; // Whether this connection is a TLS connection
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
hasReceivedInitialData: boolean; // Whether initial data has been received
- domainConfig?: IDomainConfig; // Associated domain config for this connection
+ routeConfig?: IRouteConfig; // Associated route config for this connection
// Keep-alive tracking
hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
diff --git a/ts/proxies/smart-proxy/models/route-types.ts b/ts/proxies/smart-proxy/models/route-types.ts
new file mode 100644
index 0000000..21814a8
--- /dev/null
+++ b/ts/proxies/smart-proxy/models/route-types.ts
@@ -0,0 +1,184 @@
+import * as plugins from '../../../plugins.js';
+import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
+import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
+
+/**
+ * Supported action types for route configurations
+ */
+export type TRouteActionType = 'forward' | 'redirect' | 'block';
+
+/**
+ * TLS handling modes for route configurations
+ */
+export type TTlsMode = 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
+
+/**
+ * Port range specification format
+ */
+export type TPortRange = number | number[] | Array<{ from: number; to: number }>;
+
+/**
+ * Route match criteria for incoming requests
+ */
+export interface IRouteMatch {
+ // Listen on these ports (required)
+ ports: TPortRange;
+
+ // Optional domain patterns to match (default: all domains)
+ domains?: string | string[];
+
+ // Advanced matching criteria
+ path?: string; // Match specific paths
+ clientIp?: string[]; // Match specific client IPs
+ tlsVersion?: string[]; // Match specific TLS versions
+}
+
+/**
+ * Target configuration for forwarding
+ */
+export interface IRouteTarget {
+ host: string | string[]; // Support single host or round-robin
+ port: number;
+ preservePort?: boolean; // Use incoming port as target port
+}
+
+/**
+ * TLS configuration for route actions
+ */
+export interface IRouteTls {
+ mode: TTlsMode;
+ certificate?: 'auto' | { // Auto = use ACME
+ key: string;
+ cert: string;
+ };
+}
+
+/**
+ * Redirect configuration for route actions
+ */
+export interface IRouteRedirect {
+ to: string; // URL or template with {domain}, {port}, etc.
+ status: 301 | 302 | 307 | 308;
+}
+
+/**
+ * Security options for route actions
+ */
+export interface IRouteSecurity {
+ allowedIps?: string[];
+ blockedIps?: string[];
+ maxConnections?: number;
+ authentication?: {
+ type: 'basic' | 'digest' | 'oauth';
+ // Auth-specific options would go here
+ };
+}
+
+/**
+ * Advanced options for route actions
+ */
+export interface IRouteAdvanced {
+ timeout?: number;
+ headers?: Record;
+ keepAlive?: boolean;
+ // Additional advanced options would go here
+}
+
+/**
+ * Action configuration for route handling
+ */
+export interface IRouteAction {
+ // Basic routing
+ type: TRouteActionType;
+
+ // Target for forwarding
+ target?: IRouteTarget;
+
+ // TLS handling
+ tls?: IRouteTls;
+
+ // For redirects
+ redirect?: IRouteRedirect;
+
+ // Security options
+ security?: IRouteSecurity;
+
+ // Advanced options
+ advanced?: IRouteAdvanced;
+}
+
+/**
+ * The core unified configuration interface
+ */
+export interface IRouteConfig {
+ // What to match
+ match: IRouteMatch;
+
+ // What to do with matched traffic
+ action: IRouteAction;
+
+ // Optional metadata
+ name?: string; // Human-readable name for this route
+ description?: string; // Description of the route's purpose
+ priority?: number; // Controls matching order (higher = matched first)
+ tags?: string[]; // Arbitrary tags for categorization
+}
+
+/**
+ * Unified SmartProxy options with routes-based configuration
+ */
+export interface IRoutedSmartProxyOptions {
+ // The unified configuration array (required)
+ routes: IRouteConfig[];
+
+ // Global/default settings
+ defaults?: {
+ target?: {
+ host: string;
+ port: number;
+ };
+ security?: IRouteSecurity;
+ tls?: IRouteTls;
+ // ...other defaults
+ };
+
+ // Other global settings remain (acme, etc.)
+ acme?: IAcmeOptions;
+
+ // Connection timeouts and other global settings
+ initialDataTimeout?: number;
+ socketTimeout?: number;
+ inactivityCheckInterval?: number;
+ maxConnectionLifetime?: number;
+ inactivityTimeout?: number;
+ gracefulShutdownTimeout?: number;
+
+ // Socket optimization settings
+ noDelay?: boolean;
+ keepAlive?: boolean;
+ keepAliveInitialDelay?: number;
+ maxPendingDataSize?: number;
+
+ // Enhanced features
+ disableInactivityCheck?: boolean;
+ enableKeepAliveProbes?: boolean;
+ enableDetailedLogging?: boolean;
+ enableTlsDebugLogging?: boolean;
+ enableRandomizedTimeouts?: boolean;
+ allowSessionTicket?: boolean;
+
+ // Rate limiting and security
+ maxConnectionsPerIP?: number;
+ connectionRateLimitPerMinute?: number;
+
+ // Enhanced keep-alive settings
+ keepAliveTreatment?: 'standard' | 'extended' | 'immortal';
+ keepAliveInactivityMultiplier?: number;
+ extendedKeepAliveLifetime?: number;
+
+ /**
+ * Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
+ * or a static certificate object for immediate provisioning.
+ */
+ certProvisionFunction?: (domain: string) => Promise;
+}
\ No newline at end of file
diff --git a/ts/proxies/smart-proxy/route-connection-handler.ts b/ts/proxies/smart-proxy/route-connection-handler.ts
new file mode 100644
index 0000000..c08a0d4
--- /dev/null
+++ b/ts/proxies/smart-proxy/route-connection-handler.ts
@@ -0,0 +1,1117 @@
+import * as plugins from '../../plugins.js';
+import type {
+ IConnectionRecord,
+ IDomainConfig,
+ ISmartProxyOptions
+} from './models/interfaces.js';
+import {
+ isRoutedOptions,
+ isLegacyOptions
+} from './models/interfaces.js';
+import type {
+ IRouteConfig,
+ IRouteAction
+} from './models/route-types.js';
+import { ConnectionManager } from './connection-manager.js';
+import { SecurityManager } from './security-manager.js';
+import { DomainConfigManager } from './domain-config-manager.js';
+import { TlsManager } from './tls-manager.js';
+import { NetworkProxyBridge } from './network-proxy-bridge.js';
+import { TimeoutManager } from './timeout-manager.js';
+import { RouteManager } from './route-manager.js';
+import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
+import type { TForwardingType } from '../../forwarding/config/forwarding-types.js';
+
+/**
+ * Handles new connection processing and setup logic with support for route-based configuration
+ */
+export class RouteConnectionHandler {
+ private settings: ISmartProxyOptions;
+
+ constructor(
+ settings: ISmartProxyOptions,
+ private connectionManager: ConnectionManager,
+ private securityManager: SecurityManager,
+ private domainConfigManager: DomainConfigManager,
+ private tlsManager: TlsManager,
+ private networkProxyBridge: NetworkProxyBridge,
+ private timeoutManager: TimeoutManager,
+ private routeManager: RouteManager
+ ) {
+ this.settings = settings;
+ }
+
+ /**
+ * Handle a new incoming connection
+ */
+ public handleConnection(socket: plugins.net.Socket): void {
+ const remoteIP = socket.remoteAddress || '';
+ const localPort = socket.localPort || 0;
+
+ // Validate IP against rate limits and connection limits
+ const ipValidation = this.securityManager.validateIP(remoteIP);
+ if (!ipValidation.allowed) {
+ console.log(`Connection rejected from ${remoteIP}: ${ipValidation.reason}`);
+ socket.end();
+ socket.destroy();
+ return;
+ }
+
+ // Create a new connection record
+ const record = this.connectionManager.createConnection(socket);
+ const connectionId = record.id;
+
+ // Apply socket optimizations
+ socket.setNoDelay(this.settings.noDelay);
+
+ // Apply keep-alive settings if enabled
+ if (this.settings.keepAlive) {
+ socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
+ record.hasKeepAlive = true;
+
+ // Apply enhanced TCP keep-alive options if enabled
+ if (this.settings.enableKeepAliveProbes) {
+ try {
+ // These are platform-specific and may not be available
+ if ('setKeepAliveProbes' in socket) {
+ (socket as any).setKeepAliveProbes(10);
+ }
+ if ('setKeepAliveInterval' in socket) {
+ (socket as any).setKeepAliveInterval(1000);
+ }
+ } catch (err) {
+ // Ignore errors - these are optional enhancements
+ if (this.settings.enableDetailedLogging) {
+ console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
+ }
+ }
+ }
+ }
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(
+ `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
+ `Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
+ `Active connections: ${this.connectionManager.getConnectionCount()}`
+ );
+ } else {
+ console.log(
+ `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}`
+ );
+ }
+
+ // Start TLS SNI handling
+ this.handleTlsConnection(socket, record);
+ }
+
+ /**
+ * Handle a connection and wait for TLS handshake for SNI extraction if needed
+ */
+ private handleTlsConnection(socket: plugins.net.Socket, record: IConnectionRecord): void {
+ const connectionId = record.id;
+ const localPort = record.localPort;
+ let initialDataReceived = false;
+
+ // Set an initial timeout for handshake data
+ let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
+ if (!initialDataReceived) {
+ console.log(
+ `[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
+ );
+
+ // Add a grace period
+ setTimeout(() => {
+ if (!initialDataReceived) {
+ console.log(`[${connectionId}] Final initial data timeout after grace period`);
+ if (record.incomingTerminationReason === null) {
+ record.incomingTerminationReason = 'initial_timeout';
+ this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
+ }
+ socket.end();
+ this.connectionManager.cleanupConnection(record, 'initial_timeout');
+ }
+ }, 30000);
+ }
+ }, this.settings.initialDataTimeout!);
+
+ // Make sure timeout doesn't keep the process alive
+ if (initialTimeout.unref) {
+ initialTimeout.unref();
+ }
+
+ // Set up error handler
+ socket.on('error', this.connectionManager.handleError('incoming', record));
+
+ // First data handler to capture initial TLS handshake
+ socket.once('data', (chunk: Buffer) => {
+ // Clear the initial timeout since we've received data
+ if (initialTimeout) {
+ clearTimeout(initialTimeout);
+ initialTimeout = null;
+ }
+
+ initialDataReceived = true;
+ record.hasReceivedInitialData = true;
+
+ // Block non-TLS connections on port 443
+ if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
+ console.log(
+ `[${connectionId}] Non-TLS connection detected on port 443. ` +
+ `Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
+ );
+ if (record.incomingTerminationReason === null) {
+ record.incomingTerminationReason = 'non_tls_blocked';
+ this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
+ }
+ socket.end();
+ this.connectionManager.cleanupConnection(record, 'non_tls_blocked');
+ return;
+ }
+
+ // Check if this looks like a TLS handshake
+ let serverName = '';
+ if (this.tlsManager.isTlsHandshake(chunk)) {
+ record.isTLS = true;
+
+ // Check for ClientHello to extract SNI
+ if (this.tlsManager.isClientHello(chunk)) {
+ // Create connection info for SNI extraction
+ const connInfo = {
+ sourceIp: record.remoteIP,
+ sourcePort: socket.remotePort || 0,
+ destIp: socket.localAddress || '',
+ destPort: socket.localPort || 0,
+ };
+
+ // Extract SNI
+ serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
+
+ // Lock the connection to the negotiated SNI
+ record.lockedDomain = serverName;
+
+ // Check if we should reject connections without SNI
+ if (!serverName && this.settings.allowSessionTicket === false) {
+ console.log(`[${connectionId}] No SNI detected in TLS ClientHello; sending TLS alert.`);
+ if (record.incomingTerminationReason === null) {
+ record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
+ this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
+ }
+ const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
+ try {
+ socket.cork();
+ socket.write(alert);
+ socket.uncork();
+ socket.end();
+ } catch {
+ socket.end();
+ }
+ this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
+ return;
+ }
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(`[${connectionId}] TLS connection with SNI: ${serverName || '(empty)'}`);
+ }
+ }
+ }
+
+ // Find the appropriate route for this connection
+ this.routeConnection(socket, record, serverName, chunk);
+ });
+ }
+
+ /**
+ * Route the connection based on match criteria
+ */
+ private routeConnection(
+ socket: plugins.net.Socket,
+ record: IConnectionRecord,
+ serverName: string,
+ initialChunk?: Buffer
+ ): void {
+ const connectionId = record.id;
+ const localPort = record.localPort;
+ const remoteIP = record.remoteIP;
+
+ // Find matching route
+ const routeMatch = this.routeManager.findMatchingRoute({
+ port: localPort,
+ domain: serverName,
+ clientIp: remoteIP,
+ path: undefined, // We don't have path info at this point
+ tlsVersion: undefined // We don't extract TLS version yet
+ });
+
+ if (!routeMatch) {
+ console.log(`[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}`);
+
+ // Fall back to legacy matching if we're using a hybrid configuration
+ const domainConfig = serverName
+ ? this.domainConfigManager.findDomainConfig(serverName)
+ : this.domainConfigManager.findDomainConfigForPort(localPort);
+
+ if (domainConfig) {
+ if (this.settings.enableDetailedLogging) {
+ console.log(`[${connectionId}] Using legacy domain configuration for ${serverName || 'port ' + localPort}`);
+ }
+
+ // Associate this domain config with the connection
+ record.domainConfig = domainConfig;
+
+ // Handle the connection using the legacy setup
+ return this.handleLegacyConnection(socket, record, serverName, domainConfig, initialChunk);
+ }
+
+ // No matching route or domain config, use default/fallback handling
+ console.log(`[${connectionId}] Using default route handling for connection`);
+
+ // Check default security settings
+ const defaultSecuritySettings = this.settings.defaults?.security;
+ if (defaultSecuritySettings) {
+ if (defaultSecuritySettings.allowedIPs && defaultSecuritySettings.allowedIPs.length > 0) {
+ const isAllowed = this.securityManager.isIPAuthorized(
+ remoteIP,
+ defaultSecuritySettings.allowedIPs,
+ defaultSecuritySettings.blockedIPs || []
+ );
+
+ if (!isAllowed) {
+ console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`);
+ socket.end();
+ this.connectionManager.cleanupConnection(record, 'ip_blocked');
+ return;
+ }
+ }
+ } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
+ // Legacy default IP restrictions
+ const isAllowed = this.securityManager.isIPAuthorized(
+ remoteIP,
+ this.settings.defaultAllowedIPs,
+ this.settings.defaultBlockedIPs || []
+ );
+
+ if (!isAllowed) {
+ console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`);
+ socket.end();
+ this.connectionManager.cleanupConnection(record, 'ip_blocked');
+ return;
+ }
+ }
+
+ // Setup direct connection with default settings
+ let targetHost: string;
+ let targetPort: number;
+
+ if (isRoutedOptions(this.settings) && this.settings.defaults?.target) {
+ // Use defaults from routed configuration
+ targetHost = this.settings.defaults.target.host;
+ targetPort = this.settings.defaults.target.port;
+ } else {
+ // Fall back to legacy settings
+ targetHost = this.settings.targetIP || 'localhost';
+ targetPort = this.settings.toPort;
+ }
+
+ return this.setupDirectConnection(
+ socket,
+ record,
+ undefined,
+ serverName,
+ initialChunk,
+ undefined,
+ targetHost,
+ targetPort
+ );
+ }
+
+ // A matching route was found
+ const route = routeMatch.route;
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(
+ `[${connectionId}] Route matched: "${route.name || 'unnamed'}" for ${serverName || 'connection'} on port ${localPort}`
+ );
+ }
+
+ // Handle the route based on its action type
+ switch (route.action.type) {
+ case 'forward':
+ return this.handleForwardAction(socket, record, route, initialChunk);
+
+ case 'redirect':
+ return this.handleRedirectAction(socket, record, route);
+
+ case 'block':
+ return this.handleBlockAction(socket, record, route);
+
+ default:
+ console.log(`[${connectionId}] Unknown action type: ${(route.action as any).type}`);
+ socket.end();
+ this.connectionManager.cleanupConnection(record, 'unknown_action');
+ }
+ }
+
+ /**
+ * Handle a forward action for a route
+ */
+ private handleForwardAction(
+ socket: plugins.net.Socket,
+ record: IConnectionRecord,
+ route: IRouteConfig,
+ initialChunk?: Buffer
+ ): void {
+ const connectionId = record.id;
+ const action = route.action;
+
+ // We should have a target configuration for forwarding
+ if (!action.target) {
+ console.log(`[${connectionId}] Forward action missing target configuration`);
+ socket.end();
+ this.connectionManager.cleanupConnection(record, 'missing_target');
+ return;
+ }
+
+ // Determine if this needs TLS handling
+ if (action.tls) {
+ switch (action.tls.mode) {
+ case 'passthrough':
+ // For TLS passthrough, just forward directly
+ if (this.settings.enableDetailedLogging) {
+ console.log(`[${connectionId}] Using TLS passthrough to ${action.target.host}`);
+ }
+
+ // Allow for array of hosts
+ const targetHost = Array.isArray(action.target.host)
+ ? action.target.host[Math.floor(Math.random() * action.target.host.length)]
+ : action.target.host;
+
+ // Determine target port - either target port or preserve incoming port
+ const targetPort = action.target.preservePort ? record.localPort : action.target.port;
+
+ return this.setupDirectConnection(
+ socket,
+ record,
+ undefined,
+ record.lockedDomain,
+ initialChunk,
+ undefined,
+ targetHost,
+ targetPort
+ );
+
+ case 'terminate':
+ case 'terminate-and-reencrypt':
+ // For TLS termination, use NetworkProxy
+ if (this.networkProxyBridge.getNetworkProxy()) {
+ if (this.settings.enableDetailedLogging) {
+ console.log(
+ `[${connectionId}] Using NetworkProxy for TLS termination to ${action.target.host}`
+ );
+ }
+
+ // If we have an initial chunk with TLS data, start processing it
+ if (initialChunk && record.isTLS) {
+ return this.networkProxyBridge.forwardToNetworkProxy(
+ connectionId,
+ socket,
+ record,
+ initialChunk,
+ this.settings.networkProxyPort,
+ (reason) => this.connectionManager.initiateCleanupOnce(record, reason)
+ );
+ }
+
+ // This shouldn't normally happen - we should have TLS data at this point
+ console.log(`[${connectionId}] TLS termination route without TLS data`);
+ socket.end();
+ this.connectionManager.cleanupConnection(record, 'tls_error');
+ return;
+ } else {
+ console.log(`[${connectionId}] NetworkProxy not available for TLS termination`);
+ socket.end();
+ this.connectionManager.cleanupConnection(record, 'no_network_proxy');
+ return;
+ }
+ }
+ } else {
+ // No TLS settings - basic forwarding
+ if (this.settings.enableDetailedLogging) {
+ console.log(`[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}`);
+ }
+
+ // Allow for array of hosts
+ const targetHost = Array.isArray(action.target.host)
+ ? action.target.host[Math.floor(Math.random() * action.target.host.length)]
+ : action.target.host;
+
+ // Determine target port - either target port or preserve incoming port
+ const targetPort = action.target.preservePort ? record.localPort : action.target.port;
+
+ return this.setupDirectConnection(
+ socket,
+ record,
+ undefined,
+ record.lockedDomain,
+ initialChunk,
+ undefined,
+ targetHost,
+ targetPort
+ );
+ }
+ }
+
+ /**
+ * Handle a redirect action for a route
+ */
+ private handleRedirectAction(
+ socket: plugins.net.Socket,
+ record: IConnectionRecord,
+ route: IRouteConfig
+ ): void {
+ const connectionId = record.id;
+ const action = route.action;
+
+ // We should have a redirect configuration
+ if (!action.redirect) {
+ console.log(`[${connectionId}] Redirect action missing redirect configuration`);
+ socket.end();
+ this.connectionManager.cleanupConnection(record, 'missing_redirect');
+ return;
+ }
+
+ // For TLS connections, we can't do redirects at the TCP level
+ if (record.isTLS) {
+ console.log(`[${connectionId}] Cannot redirect TLS connection at TCP level`);
+ socket.end();
+ this.connectionManager.cleanupConnection(record, 'tls_redirect_error');
+ return;
+ }
+
+ // Wait for the first HTTP request to perform the redirect
+ const dataListeners: ((chunk: Buffer) => void)[] = [];
+
+ const httpDataHandler = (chunk: Buffer) => {
+ // Remove all data listeners to avoid duplicated processing
+ for (const listener of dataListeners) {
+ socket.removeListener('data', listener);
+ }
+
+ // Parse HTTP request to get path
+ try {
+ const headersEnd = chunk.indexOf('\r\n\r\n');
+ if (headersEnd === -1) {
+ // Not a complete HTTP request, need more data
+ socket.once('data', httpDataHandler);
+ dataListeners.push(httpDataHandler);
+ return;
+ }
+
+ const httpHeaders = chunk.slice(0, headersEnd).toString();
+ const requestLine = httpHeaders.split('\r\n')[0];
+ const [method, path] = requestLine.split(' ');
+
+ // Extract Host header
+ const hostMatch = httpHeaders.match(/Host: (.+?)(\r\n|\r|\n|$)/i);
+ const host = hostMatch ? hostMatch[1].trim() : record.lockedDomain || '';
+
+ // Process the redirect URL with template variables
+ let redirectUrl = action.redirect.to;
+ redirectUrl = redirectUrl.replace(/\{domain\}/g, host);
+ redirectUrl = redirectUrl.replace(/\{path\}/g, path || '');
+ redirectUrl = redirectUrl.replace(/\{port\}/g, record.localPort.toString());
+
+ // Prepare the HTTP redirect response
+ const redirectResponse = [
+ `HTTP/1.1 ${action.redirect.status} Moved`,
+ `Location: ${redirectUrl}`,
+ 'Connection: close',
+ 'Content-Length: 0',
+ '',
+ ''
+ ].join('\r\n');
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(`[${connectionId}] Redirecting to ${redirectUrl} with status ${action.redirect.status}`);
+ }
+
+ // Send the redirect response
+ socket.end(redirectResponse);
+ this.connectionManager.initiateCleanupOnce(record, 'redirect_complete');
+ } catch (err) {
+ console.log(`[${connectionId}] Error processing HTTP redirect: ${err}`);
+ socket.end();
+ this.connectionManager.initiateCleanupOnce(record, 'redirect_error');
+ }
+ };
+
+ // Setup the HTTP data handler
+ socket.once('data', httpDataHandler);
+ dataListeners.push(httpDataHandler);
+ }
+
+ /**
+ * Handle a block action for a route
+ */
+ private handleBlockAction(
+ socket: plugins.net.Socket,
+ record: IConnectionRecord,
+ route: IRouteConfig
+ ): void {
+ const connectionId = record.id;
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(`[${connectionId}] Blocking connection based on route "${route.name || 'unnamed'}"`);
+ }
+
+ // Simply close the connection
+ socket.end();
+ this.connectionManager.initiateCleanupOnce(record, 'route_blocked');
+ }
+
+ /**
+ * Handle a connection using legacy domain configuration
+ */
+ private handleLegacyConnection(
+ socket: plugins.net.Socket,
+ record: IConnectionRecord,
+ serverName: string,
+ domainConfig: IDomainConfig,
+ initialChunk?: Buffer
+ ): void {
+ const connectionId = record.id;
+
+ // Get the forwarding type for this domain
+ const forwardingType = this.domainConfigManager.getForwardingType(domainConfig);
+
+ // IP validation
+ const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
+
+ if (!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) {
+ console.log(
+ `[${connectionId}] Connection rejected: IP ${record.remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`
+ );
+ socket.end();
+ this.connectionManager.initiateCleanupOnce(record, 'ip_blocked');
+ return;
+ }
+
+ // Handle based on forwarding type
+ switch (forwardingType) {
+ case 'http-only':
+ // For HTTP-only configs with TLS traffic
+ if (record.isTLS) {
+ console.log(`[${connectionId}] Received TLS connection for HTTP-only domain ${serverName}`);
+ socket.end();
+ this.connectionManager.initiateCleanupOnce(record, 'wrong_protocol');
+ return;
+ }
+ break;
+
+ case 'https-passthrough':
+ // For TLS passthrough with TLS traffic
+ if (record.isTLS) {
+ try {
+ const handler = this.domainConfigManager.getForwardingHandler(domainConfig);
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(`[${connectionId}] Using forwarding handler for SNI passthrough to ${serverName}`);
+ }
+
+ // Handle the connection using the handler
+ return handler.handleConnection(socket);
+ } catch (err) {
+ console.log(`[${connectionId}] Error using forwarding handler: ${err}`);
+ }
+ }
+ break;
+
+ case 'https-terminate-to-http':
+ case 'https-terminate-to-https':
+ // For TLS termination with TLS traffic
+ if (record.isTLS) {
+ const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(`[${connectionId}] Using TLS termination (${forwardingType}) for ${serverName} on port ${networkProxyPort}`);
+ }
+
+ // Forward to NetworkProxy with domain-specific port
+ return this.networkProxyBridge.forwardToNetworkProxy(
+ connectionId,
+ socket,
+ record,
+ initialChunk!,
+ networkProxyPort,
+ (reason) => this.connectionManager.initiateCleanupOnce(record, reason)
+ );
+ }
+ break;
+ }
+
+ // If we're still here, use the forwarding handler if available
+ try {
+ const handler = this.domainConfigManager.getForwardingHandler(domainConfig);
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(`[${connectionId}] Using general forwarding handler for domain ${serverName || 'unknown'}`);
+ }
+
+ // Handle the connection using the handler
+ return handler.handleConnection(socket);
+ } catch (err) {
+ console.log(`[${connectionId}] Error using forwarding handler: ${err}`);
+ }
+
+ // Fallback: set up direct connection
+ const targetIp = this.domainConfigManager.getTargetIP(domainConfig);
+ const targetPort = this.domainConfigManager.getTargetPort(domainConfig, this.settings.toPort);
+
+ return this.setupDirectConnection(
+ socket,
+ record,
+ domainConfig,
+ serverName,
+ initialChunk,
+ undefined,
+ targetIp,
+ targetPort
+ );
+ }
+
+ /**
+ * Sets up a direct connection to the target
+ */
+ private setupDirectConnection(
+ socket: plugins.net.Socket,
+ record: IConnectionRecord,
+ domainConfig?: IDomainConfig,
+ serverName?: string,
+ initialChunk?: Buffer,
+ overridePort?: number,
+ targetHost?: string,
+ targetPort?: number
+ ): void {
+ const connectionId = record.id;
+
+ // Determine target host and port if not provided
+ const finalTargetHost = targetHost || (domainConfig
+ ? this.domainConfigManager.getTargetIP(domainConfig)
+ : this.settings.defaults?.target?.host
+ ? this.settings.defaults.target.host
+ : this.settings.targetIP!);
+
+ // Determine target port - first try explicit port, then forwarding config, then fallback
+ const finalTargetPort = targetPort || (overridePort !== undefined
+ ? overridePort
+ : domainConfig
+ ? this.domainConfigManager.getTargetPort(domainConfig, this.settings.toPort)
+ : this.settings.defaults?.target?.port
+ ? this.settings.defaults.target.port
+ : this.settings.toPort);
+
+ // Setup connection options
+ const connectionOptions: plugins.net.NetConnectOpts = {
+ host: finalTargetHost,
+ port: finalTargetPort,
+ };
+
+ // Preserve source IP if configured
+ if (this.settings.defaults?.preserveSourceIP || this.settings.preserveSourceIP) {
+ connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
+ }
+
+ // Create a safe queue for incoming data
+ const dataQueue: Buffer[] = [];
+ let queueSize = 0;
+ let processingQueue = false;
+ let drainPending = false;
+ let pipingEstablished = false;
+
+ // Pause the incoming socket to prevent buffer overflows
+ socket.pause();
+
+ // Function to safely process the data queue without losing events
+ const processDataQueue = () => {
+ if (processingQueue || dataQueue.length === 0 || pipingEstablished) return;
+
+ processingQueue = true;
+
+ try {
+ // Process all queued chunks with the current active handler
+ while (dataQueue.length > 0) {
+ const chunk = dataQueue.shift()!;
+ queueSize -= chunk.length;
+
+ // Once piping is established, we shouldn't get here,
+ // but just in case, pass to the outgoing socket directly
+ if (pipingEstablished && record.outgoing) {
+ record.outgoing.write(chunk);
+ continue;
+ }
+
+ // Track bytes received
+ record.bytesReceived += chunk.length;
+
+ // Check for TLS handshake
+ if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) {
+ record.isTLS = true;
+
+ if (this.settings.enableTlsDebugLogging) {
+ console.log(
+ `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
+ );
+ }
+ }
+
+ // Check if adding this chunk would exceed the buffer limit
+ const newSize = record.pendingDataSize + chunk.length;
+
+ if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
+ console.log(
+ `[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
+ );
+ socket.end(); // Gracefully close the socket
+ this.connectionManager.initiateCleanupOnce(record, 'buffer_limit_exceeded');
+ return;
+ }
+
+ // Buffer the chunk and update the size counter
+ record.pendingData.push(Buffer.from(chunk));
+ record.pendingDataSize = newSize;
+ this.timeoutManager.updateActivity(record);
+ }
+ } finally {
+ processingQueue = false;
+
+ // If there's a pending drain and we've processed everything,
+ // signal we're ready for more data if we haven't established piping yet
+ if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
+ drainPending = false;
+ socket.resume();
+ }
+ }
+ };
+
+ // Unified data handler that safely queues incoming data
+ const safeDataHandler = (chunk: Buffer) => {
+ // If piping is already established, just let the pipe handle it
+ if (pipingEstablished) return;
+
+ // Add to our queue for orderly processing
+ dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
+ queueSize += chunk.length;
+
+ // If queue is getting large, pause socket until we catch up
+ if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
+ socket.pause();
+ drainPending = true;
+ }
+
+ // Process the queue
+ processDataQueue();
+ };
+
+ // Add our safe data handler
+ socket.on('data', safeDataHandler);
+
+ // Add initial chunk to pending data if present
+ if (initialChunk) {
+ record.bytesReceived += initialChunk.length;
+ record.pendingData.push(Buffer.from(initialChunk));
+ record.pendingDataSize = initialChunk.length;
+ }
+
+ // Create the target socket but don't set up piping immediately
+ const targetSocket = plugins.net.connect(connectionOptions);
+ record.outgoing = targetSocket;
+ record.outgoingStartTime = Date.now();
+
+ // Apply socket optimizations
+ targetSocket.setNoDelay(this.settings.noDelay);
+
+ // Apply keep-alive settings to the outgoing connection as well
+ if (this.settings.keepAlive) {
+ targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
+
+ // Apply enhanced TCP keep-alive options if enabled
+ if (this.settings.enableKeepAliveProbes) {
+ try {
+ if ('setKeepAliveProbes' in targetSocket) {
+ (targetSocket as any).setKeepAliveProbes(10);
+ }
+ if ('setKeepAliveInterval' in targetSocket) {
+ (targetSocket as any).setKeepAliveInterval(1000);
+ }
+ } catch (err) {
+ // Ignore errors - these are optional enhancements
+ if (this.settings.enableDetailedLogging) {
+ console.log(
+ `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
+ );
+ }
+ }
+ }
+ }
+
+ // Setup specific error handler for connection phase
+ targetSocket.once('error', (err) => {
+ // This handler runs only once during the initial connection phase
+ const code = (err as any).code;
+ console.log(
+ `[${connectionId}] Connection setup error to ${finalTargetHost}:${connectionOptions.port}: ${err.message} (${code})`
+ );
+
+ // Resume the incoming socket to prevent it from hanging
+ socket.resume();
+
+ if (code === 'ECONNREFUSED') {
+ console.log(
+ `[${connectionId}] Target ${finalTargetHost}:${connectionOptions.port} refused connection`
+ );
+ } else if (code === 'ETIMEDOUT') {
+ console.log(
+ `[${connectionId}] Connection to ${finalTargetHost}:${connectionOptions.port} timed out`
+ );
+ } else if (code === 'ECONNRESET') {
+ console.log(
+ `[${connectionId}] Connection to ${finalTargetHost}:${connectionOptions.port} was reset`
+ );
+ } else if (code === 'EHOSTUNREACH') {
+ console.log(`[${connectionId}] Host ${finalTargetHost} is unreachable`);
+ }
+
+ // Clear any existing error handler after connection phase
+ targetSocket.removeAllListeners('error');
+
+ // Re-add the normal error handler for established connections
+ targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
+
+ if (record.outgoingTerminationReason === null) {
+ record.outgoingTerminationReason = 'connection_failed';
+ this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
+ }
+
+ // If we have a forwarding handler for this domain, let it handle the error
+ if (domainConfig) {
+ try {
+ const forwardingHandler = this.domainConfigManager.getForwardingHandler(domainConfig);
+ forwardingHandler.emit('connection_error', {
+ socket,
+ error: err,
+ connectionId
+ });
+ } catch (handlerErr) {
+ // If getting the handler fails, just log and continue with normal cleanup
+ console.log(`Error getting forwarding handler for error handling: ${handlerErr}`);
+ }
+ }
+
+ // Clean up the connection
+ this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
+ });
+
+ // Setup close handler
+ targetSocket.on('close', this.connectionManager.handleClose('outgoing', record));
+ socket.on('close', this.connectionManager.handleClose('incoming', record));
+
+ // Handle timeouts with keep-alive awareness
+ socket.on('timeout', () => {
+ // For keep-alive connections, just log a warning instead of closing
+ if (record.hasKeepAlive) {
+ console.log(
+ `[${connectionId}] Timeout event on incoming keep-alive connection from ${
+ record.remoteIP
+ } after ${plugins.prettyMs(
+ this.settings.socketTimeout || 3600000
+ )}. Connection preserved.`
+ );
+ return;
+ }
+
+ // For non-keep-alive connections, proceed with normal cleanup
+ console.log(
+ `[${connectionId}] Timeout on incoming side from ${
+ record.remoteIP
+ } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
+ );
+ if (record.incomingTerminationReason === null) {
+ record.incomingTerminationReason = 'timeout';
+ this.connectionManager.incrementTerminationStat('incoming', 'timeout');
+ }
+ this.connectionManager.initiateCleanupOnce(record, 'timeout_incoming');
+ });
+
+ targetSocket.on('timeout', () => {
+ // For keep-alive connections, just log a warning instead of closing
+ if (record.hasKeepAlive) {
+ console.log(
+ `[${connectionId}] Timeout event on outgoing keep-alive connection from ${
+ record.remoteIP
+ } after ${plugins.prettyMs(
+ this.settings.socketTimeout || 3600000
+ )}. Connection preserved.`
+ );
+ return;
+ }
+
+ // For non-keep-alive connections, proceed with normal cleanup
+ console.log(
+ `[${connectionId}] Timeout on outgoing side from ${
+ record.remoteIP
+ } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
+ );
+ if (record.outgoingTerminationReason === null) {
+ record.outgoingTerminationReason = 'timeout';
+ this.connectionManager.incrementTerminationStat('outgoing', 'timeout');
+ }
+ this.connectionManager.initiateCleanupOnce(record, 'timeout_outgoing');
+ });
+
+ // Apply socket timeouts
+ this.timeoutManager.applySocketTimeouts(record);
+
+ // Track outgoing data for bytes counting
+ targetSocket.on('data', (chunk: Buffer) => {
+ record.bytesSent += chunk.length;
+ this.timeoutManager.updateActivity(record);
+ });
+
+ // Wait for the outgoing connection to be ready before setting up piping
+ targetSocket.once('connect', () => {
+ // Clear the initial connection error handler
+ targetSocket.removeAllListeners('error');
+
+ // Add the normal error handler for established connections
+ targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
+
+ // Process any remaining data in the queue before switching to piping
+ processDataQueue();
+
+ // Set up piping immediately
+ pipingEstablished = true;
+
+ // Flush all pending data to target
+ if (record.pendingData.length > 0) {
+ const combinedData = Buffer.concat(record.pendingData);
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(
+ `[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
+ );
+ }
+
+ // Write pending data immediately
+ targetSocket.write(combinedData, (err) => {
+ if (err) {
+ console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
+ return this.connectionManager.initiateCleanupOnce(record, 'write_error');
+ }
+ });
+
+ // Clear the buffer now that we've processed it
+ record.pendingData = [];
+ record.pendingDataSize = 0;
+ }
+
+ // Setup piping in both directions without any delays
+ socket.pipe(targetSocket);
+ targetSocket.pipe(socket);
+
+ // Resume the socket to ensure data flows
+ socket.resume();
+
+ // Process any data that might be queued in the interim
+ if (dataQueue.length > 0) {
+ // Write any remaining queued data directly to the target socket
+ for (const chunk of dataQueue) {
+ targetSocket.write(chunk);
+ }
+ // Clear the queue
+ dataQueue.length = 0;
+ queueSize = 0;
+ }
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(
+ `[${connectionId}] Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` +
+ `${
+ serverName
+ ? ` (SNI: ${serverName})`
+ : domainConfig
+ ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
+ : ''
+ }` +
+ ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
+ record.hasKeepAlive ? 'Yes' : 'No'
+ }`
+ );
+ } else {
+ console.log(
+ `Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` +
+ `${
+ serverName
+ ? ` (SNI: ${serverName})`
+ : domainConfig
+ ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
+ : ''
+ }`
+ );
+ }
+
+ // Add the renegotiation handler for SNI validation
+ if (serverName) {
+ // Create connection info object for the existing connection
+ const connInfo = {
+ sourceIp: record.remoteIP,
+ sourcePort: record.incoming.remotePort || 0,
+ destIp: record.incoming.localAddress || '',
+ destPort: record.incoming.localPort || 0,
+ };
+
+ // Create a renegotiation handler function
+ const renegotiationHandler = this.tlsManager.createRenegotiationHandler(
+ connectionId,
+ serverName,
+ connInfo,
+ (connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason)
+ );
+
+ // Store the handler in the connection record so we can remove it during cleanup
+ record.renegotiationHandler = renegotiationHandler;
+
+ // Add the handler to the socket
+ socket.on('data', renegotiationHandler);
+
+ if (this.settings.enableDetailedLogging) {
+ console.log(
+ `[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
+ );
+ if (this.settings.allowSessionTicket === false) {
+ console.log(
+ `[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`
+ );
+ }
+ }
+ }
+
+ // Set connection timeout
+ record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
+ console.log(
+ `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
+ );
+ this.connectionManager.initiateCleanupOnce(record, reason);
+ });
+
+ // Mark TLS handshake as complete for TLS connections
+ if (record.isTLS) {
+ record.tlsHandshakeComplete = true;
+
+ if (this.settings.enableTlsDebugLogging) {
+ console.log(
+ `[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
+ );
+ }
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/ts/proxies/smart-proxy/route-helpers.ts b/ts/proxies/smart-proxy/route-helpers.ts
new file mode 100644
index 0000000..604578a
--- /dev/null
+++ b/ts/proxies/smart-proxy/route-helpers.ts
@@ -0,0 +1,344 @@
+import type {
+ IRouteConfig,
+ IRouteMatch,
+ IRouteAction,
+ IRouteTarget,
+ IRouteTls,
+ IRouteRedirect,
+ IRouteSecurity,
+ IRouteAdvanced
+} from './models/route-types.js';
+
+/**
+ * Basic helper function to create a route configuration
+ */
+export function createRoute(
+ match: IRouteMatch,
+ action: IRouteAction,
+ metadata?: {
+ name?: string;
+ description?: string;
+ priority?: number;
+ tags?: string[];
+ }
+): IRouteConfig {
+ return {
+ match,
+ action,
+ ...metadata
+ };
+}
+
+/**
+ * Create a basic HTTP route configuration
+ */
+export function createHttpRoute(
+ options: {
+ ports?: number | number[]; // Default: 80
+ domains?: string | string[];
+ path?: string;
+ target: IRouteTarget;
+ headers?: Record;
+ security?: IRouteSecurity;
+ name?: string;
+ description?: string;
+ priority?: number;
+ tags?: string[];
+ }
+): IRouteConfig {
+ return createRoute(
+ {
+ ports: options.ports || 80,
+ ...(options.domains ? { domains: options.domains } : {}),
+ ...(options.path ? { path: options.path } : {})
+ },
+ {
+ type: 'forward',
+ target: options.target,
+ ...(options.headers || options.security ? {
+ advanced: {
+ ...(options.headers ? { headers: options.headers } : {})
+ },
+ ...(options.security ? { security: options.security } : {})
+ } : {})
+ },
+ {
+ name: options.name || 'HTTP Route',
+ description: options.description,
+ priority: options.priority,
+ tags: options.tags
+ }
+ );
+}
+
+/**
+ * Create an HTTPS route configuration with TLS termination
+ */
+export function createHttpsRoute(
+ options: {
+ ports?: number | number[]; // Default: 443
+ domains: string | string[];
+ path?: string;
+ target: IRouteTarget;
+ tlsMode?: 'terminate' | 'terminate-and-reencrypt';
+ certificate?: 'auto' | { key: string; cert: string };
+ headers?: Record;
+ security?: IRouteSecurity;
+ name?: string;
+ description?: string;
+ priority?: number;
+ tags?: string[];
+ }
+): IRouteConfig {
+ return createRoute(
+ {
+ ports: options.ports || 443,
+ domains: options.domains,
+ ...(options.path ? { path: options.path } : {})
+ },
+ {
+ type: 'forward',
+ target: options.target,
+ tls: {
+ mode: options.tlsMode || 'terminate',
+ certificate: options.certificate || 'auto'
+ },
+ ...(options.headers || options.security ? {
+ advanced: {
+ ...(options.headers ? { headers: options.headers } : {})
+ },
+ ...(options.security ? { security: options.security } : {})
+ } : {})
+ },
+ {
+ name: options.name || 'HTTPS Route',
+ description: options.description,
+ priority: options.priority,
+ tags: options.tags
+ }
+ );
+}
+
+/**
+ * Create an HTTPS passthrough route configuration
+ */
+export function createPassthroughRoute(
+ options: {
+ ports?: number | number[]; // Default: 443
+ domains?: string | string[];
+ target: IRouteTarget;
+ security?: IRouteSecurity;
+ name?: string;
+ description?: string;
+ priority?: number;
+ tags?: string[];
+ }
+): IRouteConfig {
+ return createRoute(
+ {
+ ports: options.ports || 443,
+ ...(options.domains ? { domains: options.domains } : {})
+ },
+ {
+ type: 'forward',
+ target: options.target,
+ tls: {
+ mode: 'passthrough'
+ },
+ ...(options.security ? { security: options.security } : {})
+ },
+ {
+ name: options.name || 'HTTPS Passthrough Route',
+ description: options.description,
+ priority: options.priority,
+ tags: options.tags
+ }
+ );
+}
+
+/**
+ * Create a redirect route configuration
+ */
+export function createRedirectRoute(
+ options: {
+ ports?: number | number[]; // Default: 80
+ domains?: string | string[];
+ path?: string;
+ redirectTo: string;
+ statusCode?: 301 | 302 | 307 | 308;
+ name?: string;
+ description?: string;
+ priority?: number;
+ tags?: string[];
+ }
+): IRouteConfig {
+ return createRoute(
+ {
+ ports: options.ports || 80,
+ ...(options.domains ? { domains: options.domains } : {}),
+ ...(options.path ? { path: options.path } : {})
+ },
+ {
+ type: 'redirect',
+ redirect: {
+ to: options.redirectTo,
+ status: options.statusCode || 301
+ }
+ },
+ {
+ name: options.name || 'Redirect Route',
+ description: options.description,
+ priority: options.priority,
+ tags: options.tags
+ }
+ );
+}
+
+/**
+ * Create an HTTP to HTTPS redirect route configuration
+ */
+export function createHttpToHttpsRedirect(
+ options: {
+ domains: string | string[];
+ statusCode?: 301 | 302 | 307 | 308;
+ name?: string;
+ priority?: number;
+ }
+): IRouteConfig {
+ const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains];
+
+ return createRedirectRoute({
+ ports: 80,
+ domains: options.domains,
+ redirectTo: 'https://{domain}{path}',
+ statusCode: options.statusCode || 301,
+ name: options.name || `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`,
+ priority: options.priority || 100 // High priority for redirects
+ });
+}
+
+/**
+ * Create a block route configuration
+ */
+export function createBlockRoute(
+ options: {
+ ports: number | number[];
+ domains?: string | string[];
+ clientIp?: string[];
+ name?: string;
+ description?: string;
+ priority?: number;
+ tags?: string[];
+ }
+): IRouteConfig {
+ return createRoute(
+ {
+ ports: options.ports,
+ ...(options.domains ? { domains: options.domains } : {}),
+ ...(options.clientIp ? { clientIp: options.clientIp } : {})
+ },
+ {
+ type: 'block'
+ },
+ {
+ name: options.name || 'Block Route',
+ description: options.description,
+ priority: options.priority || 1000, // Very high priority for blocks
+ tags: options.tags
+ }
+ );
+}
+
+/**
+ * Create a load balancer route configuration
+ */
+export function createLoadBalancerRoute(
+ options: {
+ ports?: number | number[]; // Default: 443
+ domains: string | string[];
+ path?: string;
+ targets: string[]; // Array of host names/IPs for load balancing
+ targetPort: number;
+ tlsMode?: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
+ certificate?: 'auto' | { key: string; cert: string };
+ headers?: Record;
+ security?: IRouteSecurity;
+ name?: string;
+ description?: string;
+ tags?: string[];
+ }
+): IRouteConfig {
+ const useTls = options.tlsMode !== undefined;
+ const defaultPort = useTls ? 443 : 80;
+
+ return createRoute(
+ {
+ ports: options.ports || defaultPort,
+ domains: options.domains,
+ ...(options.path ? { path: options.path } : {})
+ },
+ {
+ type: 'forward',
+ target: {
+ host: options.targets,
+ port: options.targetPort
+ },
+ ...(useTls ? {
+ tls: {
+ mode: options.tlsMode!,
+ ...(options.tlsMode !== 'passthrough' && options.certificate ? {
+ certificate: options.certificate
+ } : {})
+ }
+ } : {}),
+ ...(options.headers || options.security ? {
+ advanced: {
+ ...(options.headers ? { headers: options.headers } : {})
+ },
+ ...(options.security ? { security: options.security } : {})
+ } : {})
+ },
+ {
+ name: options.name || 'Load Balanced Route',
+ description: options.description || `Load balancing across ${options.targets.length} backends`,
+ tags: options.tags
+ }
+ );
+}
+
+/**
+ * Create a complete HTTPS server configuration with HTTP redirect
+ */
+export function createHttpsServer(
+ options: {
+ domains: string | string[];
+ target: IRouteTarget;
+ certificate?: 'auto' | { key: string; cert: string };
+ security?: IRouteSecurity;
+ addHttpRedirect?: boolean;
+ name?: string;
+ }
+): IRouteConfig[] {
+ const routes: IRouteConfig[] = [];
+ const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains];
+
+ // Add HTTPS route
+ routes.push(createHttpsRoute({
+ domains: options.domains,
+ target: options.target,
+ certificate: options.certificate || 'auto',
+ security: options.security,
+ name: options.name || `HTTPS Server for ${domainArray.join(', ')}`
+ }));
+
+ // Add HTTP to HTTPS redirect if requested
+ if (options.addHttpRedirect !== false) {
+ routes.push(createHttpToHttpsRedirect({
+ domains: options.domains,
+ name: `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`,
+ priority: 100
+ }));
+ }
+
+ return routes;
+}
\ No newline at end of file
diff --git a/ts/proxies/smart-proxy/route-manager.ts b/ts/proxies/smart-proxy/route-manager.ts
new file mode 100644
index 0000000..629e0ee
--- /dev/null
+++ b/ts/proxies/smart-proxy/route-manager.ts
@@ -0,0 +1,587 @@
+import * as plugins from '../../plugins.js';
+import type {
+ IRouteConfig,
+ IRouteMatch,
+ IRouteAction,
+ TPortRange
+} from './models/route-types.js';
+import type {
+ ISmartProxyOptions,
+ IRoutedSmartProxyOptions,
+ IDomainConfig
+} from './models/interfaces.js';
+import {
+ isRoutedOptions,
+ isLegacyOptions
+} from './models/interfaces.js';
+
+/**
+ * Result of route matching
+ */
+export interface IRouteMatchResult {
+ route: IRouteConfig;
+ // Additional match parameters (path, query, etc.)
+ params?: Record;
+}
+
+/**
+ * The RouteManager handles all routing decisions based on connections and attributes
+ */
+export class RouteManager extends plugins.EventEmitter {
+ private routes: IRouteConfig[] = [];
+ private portMap: Map = new Map();
+ private options: IRoutedSmartProxyOptions;
+
+ constructor(options: ISmartProxyOptions) {
+ super();
+
+ // We no longer support legacy options, always use provided options
+ this.options = options;
+
+ // Initialize routes from either source
+ this.updateRoutes(this.options.routes);
+ }
+
+ /**
+ * Update routes with new configuration
+ */
+ public updateRoutes(routes: IRouteConfig[] = []): void {
+ // Sort routes by priority (higher first)
+ this.routes = [...(routes || [])].sort((a, b) => {
+ const priorityA = a.priority ?? 0;
+ const priorityB = b.priority ?? 0;
+ return priorityB - priorityA;
+ });
+
+ // Rebuild port mapping for fast lookups
+ this.rebuildPortMap();
+ }
+
+ /**
+ * Rebuild the port mapping for fast lookups
+ */
+ private rebuildPortMap(): void {
+ this.portMap.clear();
+
+ for (const route of this.routes) {
+ const ports = this.expandPortRange(route.match.ports);
+
+ for (const port of ports) {
+ if (!this.portMap.has(port)) {
+ this.portMap.set(port, []);
+ }
+ this.portMap.get(port)!.push(route);
+ }
+ }
+ }
+
+ /**
+ * Expand a port range specification into an array of individual ports
+ */
+ private expandPortRange(portRange: TPortRange): number[] {
+ if (typeof portRange === 'number') {
+ return [portRange];
+ }
+
+ if (Array.isArray(portRange)) {
+ // Handle array of port objects or numbers
+ return portRange.flatMap(item => {
+ if (typeof item === 'number') {
+ return [item];
+ } else if (typeof item === 'object' && 'from' in item && 'to' in item) {
+ // Handle port range object
+ const ports: number[] = [];
+ for (let p = item.from; p <= item.to; p++) {
+ ports.push(p);
+ }
+ return ports;
+ }
+ return [];
+ });
+ }
+
+ return [];
+ }
+
+ /**
+ * Get all ports that should be listened on
+ */
+ public getListeningPorts(): number[] {
+ return Array.from(this.portMap.keys());
+ }
+
+ /**
+ * Get all routes for a given port
+ */
+ public getRoutesForPort(port: number): IRouteConfig[] {
+ return this.portMap.get(port) || [];
+ }
+
+ /**
+ * Test if a pattern matches a domain using glob matching
+ */
+ private matchDomain(pattern: string, domain: string): boolean {
+ // Convert glob pattern to regex
+ const regexPattern = pattern
+ .replace(/\./g, '\\.') // Escape dots
+ .replace(/\*/g, '.*'); // Convert * to .*
+
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
+ return regex.test(domain);
+ }
+
+ /**
+ * Match a domain against all patterns in a route
+ */
+ private matchRouteDomain(route: IRouteConfig, domain: string): boolean {
+ if (!route.match.domains) {
+ // If no domains specified, match all domains
+ return true;
+ }
+
+ const patterns = Array.isArray(route.match.domains)
+ ? route.match.domains
+ : [route.match.domains];
+
+ return patterns.some(pattern => this.matchDomain(pattern, domain));
+ }
+
+ /**
+ * Check if a client IP is allowed by a route's security settings
+ */
+ private isClientIpAllowed(route: IRouteConfig, clientIp: string): boolean {
+ const security = route.action.security;
+
+ if (!security) {
+ return true; // No security settings means allowed
+ }
+
+ // Check blocked IPs first
+ if (security.blockedIps && security.blockedIps.length > 0) {
+ for (const pattern of security.blockedIps) {
+ if (this.matchIpPattern(pattern, clientIp)) {
+ return false; // IP is blocked
+ }
+ }
+ }
+
+ // If there are allowed IPs, check them
+ if (security.allowedIps && security.allowedIps.length > 0) {
+ for (const pattern of security.allowedIps) {
+ if (this.matchIpPattern(pattern, clientIp)) {
+ return true; // IP is allowed
+ }
+ }
+ return false; // IP not in allowed list
+ }
+
+ // No allowed IPs specified, so IP is allowed
+ return true;
+ }
+
+ /**
+ * Match an IP against a pattern
+ */
+ private matchIpPattern(pattern: string, ip: string): boolean {
+ // Handle exact match
+ if (pattern === ip) {
+ return true;
+ }
+
+ // Handle CIDR notation (e.g., 192.168.1.0/24)
+ if (pattern.includes('/')) {
+ return this.matchIpCidr(pattern, ip);
+ }
+
+ // Handle glob pattern (e.g., 192.168.1.*)
+ if (pattern.includes('*')) {
+ const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
+ const regex = new RegExp(`^${regexPattern}$`);
+ return regex.test(ip);
+ }
+
+ return false;
+ }
+
+ /**
+ * Match an IP against a CIDR pattern
+ */
+ private matchIpCidr(cidr: string, ip: string): boolean {
+ try {
+ // In a real implementation, you'd use a proper IP library
+ // This is a simplified implementation
+ const [subnet, bits] = cidr.split('/');
+ const mask = parseInt(bits, 10);
+
+ // Convert IP addresses to numeric values
+ const ipNum = this.ipToNumber(ip);
+ const subnetNum = this.ipToNumber(subnet);
+
+ // Calculate subnet mask
+ const maskNum = ~(2 ** (32 - mask) - 1);
+
+ // Check if IP is in subnet
+ return (ipNum & maskNum) === (subnetNum & maskNum);
+ } catch (e) {
+ console.error(`Error matching IP ${ip} against CIDR ${cidr}:`, e);
+ return false;
+ }
+ }
+
+ /**
+ * Convert an IP address to a numeric value
+ */
+ private ipToNumber(ip: string): number {
+ const parts = ip.split('.').map(part => parseInt(part, 10));
+ return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
+ }
+
+ /**
+ * Find the matching route for a connection
+ */
+ public findMatchingRoute(options: {
+ port: number;
+ domain?: string;
+ clientIp: string;
+ path?: string;
+ tlsVersion?: string;
+ }): IRouteMatchResult | null {
+ const { port, domain, clientIp, path, tlsVersion } = options;
+
+ // Get all routes for this port
+ const routesForPort = this.getRoutesForPort(port);
+
+ // Find the first matching route based on priority order
+ for (const route of routesForPort) {
+ // Check domain match if specified
+ if (domain && !this.matchRouteDomain(route, domain)) {
+ continue;
+ }
+
+ // Check path match if specified in both route and request
+ if (path && route.match.path) {
+ if (!this.matchPath(route.match.path, path)) {
+ continue;
+ }
+ }
+
+ // Check client IP match
+ if (route.match.clientIp && !route.match.clientIp.some(pattern =>
+ this.matchIpPattern(pattern, clientIp))) {
+ continue;
+ }
+
+ // Check TLS version match
+ if (tlsVersion && route.match.tlsVersion &&
+ !route.match.tlsVersion.includes(tlsVersion)) {
+ continue;
+ }
+
+ // Check security settings
+ if (!this.isClientIpAllowed(route, clientIp)) {
+ continue;
+ }
+
+ // All checks passed, this route matches
+ return { route };
+ }
+
+ return null;
+ }
+
+ /**
+ * Match a path against a pattern
+ */
+ private matchPath(pattern: string, path: string): boolean {
+ // Convert the glob pattern to a regex
+ const regexPattern = pattern
+ .replace(/\./g, '\\.') // Escape dots
+ .replace(/\*/g, '.*') // Convert * to .*
+ .replace(/\//g, '\\/'); // Escape slashes
+
+ const regex = new RegExp(`^${regexPattern}$`);
+ return regex.test(path);
+ }
+
+ /**
+ * Convert a domain config to routes
+ * (For backward compatibility with code that still uses domainConfigs)
+ */
+ public domainConfigToRoutes(domainConfig: IDomainConfig): IRouteConfig[] {
+ const routes: IRouteConfig[] = [];
+ const { domains, forwarding } = domainConfig;
+
+ // Determine the action based on forwarding type
+ let action: IRouteAction = {
+ type: 'forward',
+ target: {
+ host: forwarding.target.host,
+ port: forwarding.target.port
+ }
+ };
+
+ // Set TLS mode based on forwarding type
+ switch (forwarding.type) {
+ case 'http-only':
+ // No TLS settings needed
+ break;
+ case 'https-passthrough':
+ action.tls = { mode: 'passthrough' };
+ break;
+ case 'https-terminate-to-http':
+ action.tls = {
+ mode: 'terminate',
+ certificate: forwarding.https?.customCert ? {
+ key: forwarding.https.customCert.key,
+ cert: forwarding.https.customCert.cert
+ } : 'auto'
+ };
+ break;
+ case 'https-terminate-to-https':
+ action.tls = {
+ mode: 'terminate-and-reencrypt',
+ certificate: forwarding.https?.customCert ? {
+ key: forwarding.https.customCert.key,
+ cert: forwarding.https.customCert.cert
+ } : 'auto'
+ };
+ break;
+ }
+
+ // Add security settings if present
+ if (forwarding.security) {
+ action.security = {
+ allowedIps: forwarding.security.allowedIps,
+ blockedIps: forwarding.security.blockedIps,
+ maxConnections: forwarding.security.maxConnections
+ };
+ }
+
+ // Add advanced settings if present
+ if (forwarding.advanced) {
+ action.advanced = {
+ timeout: forwarding.advanced.timeout,
+ headers: forwarding.advanced.headers,
+ keepAlive: forwarding.advanced.keepAlive
+ };
+ }
+
+ // Determine which port to use based on forwarding type
+ const defaultPort = forwarding.type.startsWith('https') ? 443 : 80;
+
+ // Add the main route
+ routes.push({
+ match: {
+ ports: defaultPort,
+ domains
+ },
+ action,
+ name: `Route for ${domains.join(', ')}`
+ });
+
+ // Add HTTP redirect if needed
+ if (forwarding.http?.redirectToHttps) {
+ routes.push({
+ match: {
+ ports: 80,
+ domains
+ },
+ action: {
+ type: 'redirect',
+ redirect: {
+ to: 'https://{domain}{path}',
+ status: 301
+ }
+ },
+ name: `HTTP Redirect for ${domains.join(', ')}`,
+ priority: 100 // Higher priority for redirects
+ });
+ }
+
+ // Add port ranges if specified
+ if (forwarding.advanced?.portRanges) {
+ for (const range of forwarding.advanced.portRanges) {
+ routes.push({
+ match: {
+ ports: [{ from: range.from, to: range.to }],
+ domains
+ },
+ action,
+ name: `Port Range ${range.from}-${range.to} for ${domains.join(', ')}`
+ });
+ }
+ }
+
+ return routes;
+ }
+
+ /**
+ * Update routes based on domain configs
+ * (For backward compatibility with code that still uses domainConfigs)
+ */
+ public updateFromDomainConfigs(domainConfigs: IDomainConfig[]): void {
+ const routes: IRouteConfig[] = [];
+
+ // Convert each domain config to routes
+ for (const config of domainConfigs) {
+ routes.push(...this.domainConfigToRoutes(config));
+ }
+
+ // Merge with existing routes that aren't derived from domain configs
+ const nonDomainRoutes = this.routes.filter(r =>
+ !r.name || !r.name.includes('for '));
+
+ this.updateRoutes([...nonDomainRoutes, ...routes]);
+ }
+
+ /**
+ * Validate the route configuration and return any warnings
+ */
+ public validateConfiguration(): string[] {
+ const warnings: string[] = [];
+ const duplicatePorts = new Map();
+
+ // Check for routes with the same exact match criteria
+ for (let i = 0; i < this.routes.length; i++) {
+ for (let j = i + 1; j < this.routes.length; j++) {
+ const route1 = this.routes[i];
+ const route2 = this.routes[j];
+
+ // Check if route match criteria are the same
+ if (this.areMatchesSimilar(route1.match, route2.match)) {
+ warnings.push(
+ `Routes "${route1.name || i}" and "${route2.name || j}" have similar match criteria. ` +
+ `The route with higher priority (${Math.max(route1.priority || 0, route2.priority || 0)}) will be used.`
+ );
+ }
+ }
+ }
+
+ // Check for routes that may never be matched due to priority
+ for (let i = 0; i < this.routes.length; i++) {
+ const route = this.routes[i];
+ const higherPriorityRoutes = this.routes.filter(r =>
+ (r.priority || 0) > (route.priority || 0));
+
+ for (const higherRoute of higherPriorityRoutes) {
+ if (this.isRouteShadowed(route, higherRoute)) {
+ warnings.push(
+ `Route "${route.name || i}" may never be matched because it is shadowed by ` +
+ `higher priority route "${higherRoute.name || 'unnamed'}"`
+ );
+ break;
+ }
+ }
+ }
+
+ return warnings;
+ }
+
+ /**
+ * Check if two route matches are similar (potential conflict)
+ */
+ private areMatchesSimilar(match1: IRouteMatch, match2: IRouteMatch): boolean {
+ // Check port overlap
+ const ports1 = new Set(this.expandPortRange(match1.ports));
+ const ports2 = new Set(this.expandPortRange(match2.ports));
+
+ let havePortOverlap = false;
+ for (const port of ports1) {
+ if (ports2.has(port)) {
+ havePortOverlap = true;
+ break;
+ }
+ }
+
+ if (!havePortOverlap) {
+ return false;
+ }
+
+ // Check domain overlap
+ if (match1.domains && match2.domains) {
+ const domains1 = Array.isArray(match1.domains) ? match1.domains : [match1.domains];
+ const domains2 = Array.isArray(match2.domains) ? match2.domains : [match2.domains];
+
+ // Check if any domain pattern from match1 could match any from match2
+ let haveDomainOverlap = false;
+ for (const domain1 of domains1) {
+ for (const domain2 of domains2) {
+ if (domain1 === domain2 ||
+ (domain1.includes('*') || domain2.includes('*'))) {
+ haveDomainOverlap = true;
+ break;
+ }
+ }
+ if (haveDomainOverlap) break;
+ }
+
+ if (!haveDomainOverlap) {
+ return false;
+ }
+ } else if (match1.domains || match2.domains) {
+ // One has domains, the other doesn't - they could overlap
+ // The one with domains is more specific, so it's not exactly a conflict
+ return false;
+ }
+
+ // Check path overlap
+ if (match1.path && match2.path) {
+ // This is a simplified check - in a real implementation,
+ // you'd need to check if the path patterns could match the same paths
+ return match1.path === match2.path ||
+ match1.path.includes('*') ||
+ match2.path.includes('*');
+ } else if (match1.path || match2.path) {
+ // One has a path, the other doesn't
+ return false;
+ }
+
+ // If we get here, the matches have significant overlap
+ return true;
+ }
+
+ /**
+ * Check if a route is completely shadowed by a higher priority route
+ */
+ private isRouteShadowed(route: IRouteConfig, higherPriorityRoute: IRouteConfig): boolean {
+ // If they don't have similar match criteria, no shadowing occurs
+ if (!this.areMatchesSimilar(route.match, higherPriorityRoute.match)) {
+ return false;
+ }
+
+ // If higher priority route has more specific criteria, no shadowing
+ if (this.isRouteMoreSpecific(higherPriorityRoute.match, route.match)) {
+ return false;
+ }
+
+ // If higher priority route is equally or less specific but has higher priority,
+ // it shadows the lower priority route
+ return true;
+ }
+
+ /**
+ * Check if route1 is more specific than route2
+ */
+ private isRouteMoreSpecific(match1: IRouteMatch, match2: IRouteMatch): boolean {
+ // Check if match1 has more specific criteria
+ let match1Points = 0;
+ let match2Points = 0;
+
+ // Path is the most specific
+ if (match1.path) match1Points += 3;
+ if (match2.path) match2Points += 3;
+
+ // Domain is next most specific
+ if (match1.domains) match1Points += 2;
+ if (match2.domains) match2Points += 2;
+
+ // Client IP and TLS version are least specific
+ if (match1.clientIp) match1Points += 1;
+ if (match2.clientIp) match2Points += 1;
+
+ if (match1.tlsVersion) match1Points += 1;
+ if (match2.tlsVersion) match2Points += 1;
+
+ return match1Points > match2Points;
+ }
+}
\ No newline at end of file
diff --git a/ts/proxies/smart-proxy/smart-proxy.ts b/ts/proxies/smart-proxy/smart-proxy.ts
index d258daf..f05f1b7 100644
--- a/ts/proxies/smart-proxy/smart-proxy.ts
+++ b/ts/proxies/smart-proxy/smart-proxy.ts
@@ -1,6 +1,6 @@
import * as plugins from '../../plugins.js';
-// Importing from the new structure
+// Importing required components
import { ConnectionManager } from './connection-manager.js';
import { SecurityManager } from './security-manager.js';
import { DomainConfigManager } from './domain-config-manager.js';
@@ -8,23 +8,27 @@ import { TlsManager } from './tls-manager.js';
import { NetworkProxyBridge } from './network-proxy-bridge.js';
import { TimeoutManager } from './timeout-manager.js';
import { PortRangeManager } from './port-range-manager.js';
-import { ConnectionHandler } from './connection-handler.js';
+import { RouteManager } from './route-manager.js';
+import { RouteConnectionHandler } from './route-connection-handler.js';
-// External dependencies from migrated modules
+// External dependencies
import { Port80Handler } from '../../http/port80/port80-handler.js';
import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js';
import type { ICertificateData } from '../../certificate/models/certificate-types.js';
import { buildPort80Handler } from '../../certificate/acme/acme-factory.js';
-import type { TForwardingType } from '../../forwarding/config/forwarding-types.js';
import { createPort80HandlerOptions } from '../../common/port80-adapter.js';
-// Import types from models
-import type { ISmartProxyOptions, IDomainConfig } from './models/interfaces.js';
-// Provide backward compatibility types
-export type { ISmartProxyOptions as IPortProxySettings, IDomainConfig };
+// Import types and utilities
+import type {
+ ISmartProxyOptions,
+ IRoutedSmartProxyOptions,
+ IDomainConfig
+} from './models/interfaces.js';
+import { isRoutedOptions, isLegacyOptions } from './models/interfaces.js';
+import type { IRouteConfig } from './models/route-types.js';
/**
- * SmartProxy - Main class that coordinates all components
+ * SmartProxy - Unified route-based API
*/
export class SmartProxy extends plugins.EventEmitter {
private netServers: plugins.net.Server[] = [];
@@ -34,24 +38,28 @@ export class SmartProxy extends plugins.EventEmitter {
// Component managers
private connectionManager: ConnectionManager;
private securityManager: SecurityManager;
- public domainConfigManager: DomainConfigManager;
+ private domainConfigManager: DomainConfigManager;
private tlsManager: TlsManager;
private networkProxyBridge: NetworkProxyBridge;
private timeoutManager: TimeoutManager;
private portRangeManager: PortRangeManager;
- private connectionHandler: ConnectionHandler;
+ private routeManager: RouteManager;
+ private routeConnectionHandler: RouteConnectionHandler;
// Port80Handler for ACME certificate management
private port80Handler: Port80Handler | null = null;
// CertProvisioner for unified certificate workflows
private certProvisioner?: CertProvisioner;
+ /**
+ * Constructor that supports both legacy and route-based configuration
+ */
constructor(settingsArg: ISmartProxyOptions) {
super();
+
// Set reasonable defaults for all settings
this.settings = {
...settingsArg,
- targetIP: settingsArg.targetIP || 'localhost',
initialDataTimeout: settingsArg.initialDataTimeout || 120000,
socketTimeout: settingsArg.socketTimeout || 3600000,
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000,
@@ -76,12 +84,11 @@ export class SmartProxy extends plugins.EventEmitter {
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000,
networkProxyPort: settingsArg.networkProxyPort || 8443,
- acme: settingsArg.acme || {},
- globalPortRanges: settingsArg.globalPortRanges || [],
};
// Set default ACME options if not provided
- if (!this.settings.acme || Object.keys(this.settings.acme).length === 0) {
+ this.settings.acme = this.settings.acme || {};
+ if (Object.keys(this.settings.acme).length === 0) {
this.settings.acme = {
enabled: false,
port: 80,
@@ -91,7 +98,7 @@ export class SmartProxy extends plugins.EventEmitter {
autoRenew: true,
certificateStore: './certs',
skipConfiguredCerts: false,
- httpsRedirectPort: this.settings.fromPort,
+ httpsRedirectPort: this.settings.fromPort || 443,
renewCheckIntervalHours: 24,
domainForwards: []
};
@@ -105,13 +112,20 @@ export class SmartProxy extends plugins.EventEmitter {
this.securityManager,
this.timeoutManager
);
+
+ // Create domain config manager and port range manager (for backward compatibility)
this.domainConfigManager = new DomainConfigManager(this.settings);
- this.tlsManager = new TlsManager(this.settings);
- this.networkProxyBridge = new NetworkProxyBridge(this.settings);
this.portRangeManager = new PortRangeManager(this.settings);
- // Initialize connection handler
- this.connectionHandler = new ConnectionHandler(
+ // Create the new route manager
+ this.routeManager = new RouteManager(this.settings);
+
+ // Create other required components
+ this.tlsManager = new TlsManager(this.settings);
+ this.networkProxyBridge = new NetworkProxyBridge(this.settings);
+
+ // Initialize connection handler with route support
+ this.routeConnectionHandler = new RouteConnectionHandler(
this.settings,
this.connectionManager,
this.securityManager,
@@ -119,12 +133,12 @@ export class SmartProxy extends plugins.EventEmitter {
this.tlsManager,
this.networkProxyBridge,
this.timeoutManager,
- this.portRangeManager
+ this.routeManager
);
}
/**
- * The settings for the port proxy
+ * The settings for the SmartProxy
*/
public settings: ISmartProxyOptions;
@@ -142,8 +156,9 @@ export class SmartProxy extends plugins.EventEmitter {
// Build and start the Port80Handler
this.port80Handler = buildPort80Handler({
...config,
- httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort
+ httpsRedirectPort: config.httpsRedirectPort || (isLegacyOptions(this.settings) ? this.settings.fromPort : 443)
});
+
// Share Port80Handler with NetworkProxyBridge before start
this.networkProxyBridge.setPort80Handler(this.port80Handler);
await this.port80Handler.start();
@@ -154,7 +169,7 @@ export class SmartProxy extends plugins.EventEmitter {
}
/**
- * Start the proxy server
+ * Start the proxy server with support for both configuration types
*/
public async start() {
// Don't start if already shutting down
@@ -163,11 +178,11 @@ export class SmartProxy extends plugins.EventEmitter {
return;
}
- // Process domain configs
- // Note: ensureForwardingConfig is no longer needed since forwarding is now required
-
- // Initialize domain config manager with the processed configs
- this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs);
+ // If using legacy format, make sure domainConfigs are initialized
+ if (isLegacyOptions(this.settings)) {
+ // Initialize domain config manager with the processed configs
+ this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs);
+ }
// Initialize Port80Handler if enabled
await this.initializePort80Handler();
@@ -176,20 +191,39 @@ export class SmartProxy extends plugins.EventEmitter {
if (this.port80Handler) {
const acme = this.settings.acme!;
- // Convert domain forwards to use the new forwarding system if possible
+ // Setup domain forwards based on configuration type
const domainForwards = acme.domainForwards?.map(f => {
- // If the domain has a forwarding config in domainConfigs, use that
- const domainConfig = this.settings.domainConfigs.find(
- dc => dc.domains.some(d => d === f.domain)
- );
+ if (isLegacyOptions(this.settings)) {
+ // If using legacy mode, check if domain config exists
+ const domainConfig = this.settings.domainConfigs.find(
+ dc => dc.domains.some(d => d === f.domain)
+ );
- if (domainConfig?.forwarding) {
- return {
+ if (domainConfig?.forwarding) {
+ return {
+ domain: f.domain,
+ forwardConfig: f.forwardConfig,
+ acmeForwardConfig: f.acmeForwardConfig,
+ sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false
+ };
+ }
+ } else {
+ // In route mode, look for matching route
+ const route = this.routeManager.findMatchingRoute({
+ port: 443,
domain: f.domain,
- forwardConfig: f.forwardConfig,
- acmeForwardConfig: f.acmeForwardConfig,
- sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false
- };
+ clientIp: '127.0.0.1' // Dummy IP for finding routes
+ })?.route;
+
+ if (route && route.action.type === 'forward' && route.action.tls) {
+ // If we found a matching route with TLS settings
+ return {
+ domain: f.domain,
+ forwardConfig: f.forwardConfig,
+ acmeForwardConfig: f.acmeForwardConfig,
+ sslRedirect: f.sslRedirect || false
+ };
+ }
}
// Otherwise use the existing configuration
@@ -201,17 +235,38 @@ export class SmartProxy extends plugins.EventEmitter {
};
}) || [];
- this.certProvisioner = new CertProvisioner(
- this.settings.domainConfigs,
- this.port80Handler,
- this.networkProxyBridge,
- this.settings.certProvisionFunction,
- acme.renewThresholdDays!,
- acme.renewCheckIntervalHours!,
- acme.autoRenew!,
- domainForwards
- );
+ // Create CertProvisioner with appropriate parameters
+ if (isLegacyOptions(this.settings)) {
+ this.certProvisioner = new CertProvisioner(
+ this.settings.domainConfigs,
+ this.port80Handler,
+ this.networkProxyBridge,
+ this.settings.certProvisionFunction,
+ acme.renewThresholdDays!,
+ acme.renewCheckIntervalHours!,
+ acme.autoRenew!,
+ domainForwards
+ );
+ } else {
+ // For route-based configuration, we need to adapt the interface
+ // Convert routes to domain configs for CertProvisioner
+ const domainConfigs: IDomainConfig[] = this.extractDomainConfigsFromRoutes(
+ (this.settings as IRoutedSmartProxyOptions).routes
+ );
+ this.certProvisioner = new CertProvisioner(
+ domainConfigs,
+ this.port80Handler,
+ this.networkProxyBridge,
+ this.settings.certProvisionFunction,
+ acme.renewThresholdDays!,
+ acme.renewCheckIntervalHours!,
+ acme.autoRenew!,
+ domainForwards
+ );
+ }
+
+ // Register certificate event handler
this.certProvisioner.on('certificate', (certData) => {
this.emit('certificate', {
domain: certData.domain,
@@ -228,25 +283,22 @@ export class SmartProxy extends plugins.EventEmitter {
}
// Initialize and start NetworkProxy if needed
- if (
- this.settings.useNetworkProxy &&
- this.settings.useNetworkProxy.length > 0
- ) {
+ if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
await this.networkProxyBridge.initialize();
await this.networkProxyBridge.start();
}
- // Validate port configuration
- const configWarnings = this.portRangeManager.validateConfiguration();
+ // Validate the route configuration
+ const configWarnings = this.routeManager.validateConfiguration();
if (configWarnings.length > 0) {
- console.log("Port configuration warnings:");
+ console.log("Route configuration warnings:");
for (const warning of configWarnings) {
console.log(` - ${warning}`);
}
}
- // Get listening ports from PortRangeManager
- const listeningPorts = this.portRangeManager.getListeningPorts();
+ // Get listening ports from RouteManager
+ const listeningPorts = this.routeManager.getListeningPorts();
// Create servers for each port
for (const port of listeningPorts) {
@@ -258,8 +310,8 @@ export class SmartProxy extends plugins.EventEmitter {
return;
}
- // Delegate to connection handler
- this.connectionHandler.handleConnection(socket);
+ // Delegate to route connection handler
+ this.routeConnectionHandler.handleConnection(socket);
}).on('error', (err: Error) => {
console.log(`Server Error on port ${port}: ${err.message}`);
});
@@ -268,7 +320,9 @@ export class SmartProxy extends plugins.EventEmitter {
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
console.log(
`SmartProxy -> OK: Now listening on port ${port}${
- this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
+ isLegacyOptions(this.settings) && this.settings.sniEnabled && !isNetworkProxyPort ?
+ ' (SNI passthrough enabled)' :
+ ''
}${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
);
});
@@ -348,12 +402,70 @@ export class SmartProxy extends plugins.EventEmitter {
}
}
+ /**
+ * Extract domain configurations from routes for certificate provisioning
+ */
+ private extractDomainConfigsFromRoutes(routes: IRouteConfig[]): IDomainConfig[] {
+ const domainConfigs: IDomainConfig[] = [];
+
+ for (const route of routes) {
+ // Skip routes without domain specs
+ if (!route.match.domains) continue;
+
+ // Skip non-forward routes
+ if (route.action.type !== 'forward') continue;
+
+ // Only process routes that need TLS termination (those with certificates)
+ if (!route.action.tls ||
+ route.action.tls.mode === 'passthrough' ||
+ !route.action.target) continue;
+
+ const domains = Array.isArray(route.match.domains)
+ ? route.match.domains
+ : [route.match.domains];
+
+ // Determine forwarding type based on TLS mode
+ const forwardingType = route.action.tls.mode === 'terminate'
+ ? 'https-terminate-to-http'
+ : 'https-terminate-to-https';
+
+ // Create a forwarding config
+ const forwarding = {
+ type: forwardingType as any,
+ target: {
+ host: Array.isArray(route.action.target.host)
+ ? route.action.target.host[0]
+ : route.action.target.host,
+ port: route.action.target.port
+ },
+ // Add TLS settings
+ https: {
+ customCert: route.action.tls.certificate !== 'auto'
+ ? route.action.tls.certificate
+ : undefined
+ },
+ // Add security settings if present
+ security: route.action.security,
+ // Add advanced settings if present
+ advanced: route.action.advanced
+ };
+
+ domainConfigs.push({
+ domains,
+ forwarding
+ });
+ }
+
+ return domainConfigs;
+ }
+
/**
* Stop the proxy server
*/
public async stop() {
console.log('SmartProxy shutting down...');
this.isShuttingDown = true;
+
// Stop CertProvisioner if active
if (this.certProvisioner) {
await this.certProvisioner.stop();
@@ -411,14 +523,17 @@ export class SmartProxy extends plugins.EventEmitter {
}
/**
- * Updates the domain configurations for the proxy
+ * Updates the domain configurations for the proxy (legacy support)
*/
public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise {
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
- // Update domain configs in DomainConfigManager
+ // Update domain configs in DomainConfigManager (legacy)
this.domainConfigManager.updateDomainConfigs(newDomainConfigs);
+ // Also update the RouteManager with these domain configs
+ this.routeManager.updateFromDomainConfigs(newDomainConfigs);
+
// If NetworkProxy is initialized, resync the configurations
if (this.networkProxyBridge.getNetworkProxy()) {
await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
@@ -428,7 +543,7 @@ export class SmartProxy extends plugins.EventEmitter {
if (this.port80Handler && this.settings.acme?.enabled) {
for (const domainConfig of newDomainConfigs) {
// Skip certificate provisioning for http-only or passthrough configs that don't need certs
- const forwardingType = domainConfig.forwarding.type;
+ const forwardingType = this.domainConfigManager.getForwardingType(domainConfig);
const needsCertificate =
forwardingType === 'https-terminate-to-http' ||
forwardingType === 'https-terminate-to-https';
@@ -490,6 +605,95 @@ export class SmartProxy extends plugins.EventEmitter {
}
}
+ /**
+ * Update routes with new configuration (new API)
+ */
+ public async updateRoutes(newRoutes: IRouteConfig[]): Promise {
+ console.log(`Updating routes (${newRoutes.length} routes)`);
+
+ // Update routes in RouteManager
+ this.routeManager.updateRoutes(newRoutes);
+
+ // If NetworkProxy is initialized, resync the configurations
+ if (this.networkProxyBridge.getNetworkProxy()) {
+ // Create equivalent domain configs for NetworkProxy
+ const domainConfigs = this.extractDomainConfigsFromRoutes(newRoutes);
+
+ // Update domain configs in DomainConfigManager for sync
+ this.domainConfigManager.updateDomainConfigs(domainConfigs);
+
+ // Sync with NetworkProxy
+ await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
+ }
+
+ // If Port80Handler is running, provision certificates based on routes
+ if (this.port80Handler && this.settings.acme?.enabled) {
+ for (const route of newRoutes) {
+ // Skip routes without domains
+ if (!route.match.domains) continue;
+
+ // Skip non-forward routes
+ if (route.action.type !== 'forward') continue;
+
+ // Skip routes without TLS termination
+ if (!route.action.tls ||
+ route.action.tls.mode === 'passthrough' ||
+ !route.action.target) continue;
+
+ // Skip certificate provisioning if certificate is not auto
+ if (route.action.tls.certificate !== 'auto') continue;
+
+ const domains = Array.isArray(route.match.domains)
+ ? route.match.domains
+ : [route.match.domains];
+
+ for (const domain of domains) {
+ const isWildcard = domain.includes('*');
+ let provision: string | plugins.tsclass.network.ICert = 'http01';
+
+ if (this.settings.certProvisionFunction) {
+ try {
+ provision = await this.settings.certProvisionFunction(domain);
+ } catch (err) {
+ console.log(`certProvider error for ${domain}: ${err}`);
+ }
+ } else if (isWildcard) {
+ console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`);
+ continue;
+ }
+
+ if (provision === 'http01') {
+ if (isWildcard) {
+ console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
+ continue;
+ }
+
+ // Register domain with Port80Handler
+ this.port80Handler.addDomain({
+ domainName: domain,
+ sslRedirect: true,
+ acmeMaintenance: true
+ });
+
+ console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
+ } else {
+ // Handle static certificate (e.g., DNS-01 provisioned)
+ const certObj = provision as plugins.tsclass.network.ICert;
+ const certData: ICertificateData = {
+ domain: certObj.domainName,
+ certificate: certObj.publicKey,
+ privateKey: certObj.privateKey,
+ expiryDate: new Date(certObj.validUntil)
+ };
+ this.networkProxyBridge.applyExternalCertificate(certData);
+ console.log(`Applied static certificate for ${domain} from certProvider`);
+ }
+ }
+ }
+
+ console.log('Provisioned certificates for new routes');
+ }
+ }
/**
* Request a certificate for a specific domain
@@ -583,7 +787,8 @@ export class SmartProxy extends plugins.EventEmitter {
networkProxyConnections,
terminationStats,
acmeEnabled: !!this.port80Handler,
- port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null
+ port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null,
+ routes: this.routeManager.getListeningPorts().length
};
}
@@ -591,18 +796,44 @@ export class SmartProxy extends plugins.EventEmitter {
* Get a list of eligible domains for ACME certificates
*/
public getEligibleDomainsForCertificates(): string[] {
- // Collect all non-wildcard domains from domain configs
const domains: string[] = [];
- for (const config of this.settings.domainConfigs) {
+ // Get domains from routes
+ const routes = isRoutedOptions(this.settings) ? this.settings.routes : [];
+
+ for (const route of routes) {
+ if (!route.match.domains) continue;
+
+ // Skip routes without TLS termination or auto certificates
+ if (route.action.type !== 'forward' ||
+ !route.action.tls ||
+ route.action.tls.mode === 'passthrough' ||
+ route.action.tls.certificate !== 'auto') continue;
+
+ const routeDomains = Array.isArray(route.match.domains)
+ ? route.match.domains
+ : [route.match.domains];
+
// Skip domains that can't be used with ACME
- const eligibleDomains = config.domains.filter(domain =>
+ const eligibleDomains = routeDomains.filter(domain =>
!domain.includes('*') && this.isValidDomain(domain)
);
domains.push(...eligibleDomains);
}
+ // For legacy mode, also get domains from domain configs
+ if (isLegacyOptions(this.settings)) {
+ for (const config of this.settings.domainConfigs) {
+ // Skip domains that can't be used with ACME
+ const eligibleDomains = config.domains.filter(domain =>
+ !domain.includes('*') && this.isValidDomain(domain)
+ );
+
+ domains.push(...eligibleDomains);
+ }
+ }
+
return domains;
}