BREAKING CHANGE(forwarding): Refactor unified forwarding API and remove redundant documentation. Removed docs/forwarding-system.md (its content is migrated into readme.md) and updated helper functions (e.g. replacing sniPassthrough with httpsPassthrough) to accept configuration objects. Legacy fields in domain configurations (allowedIPs, blockedIPs, useNetworkProxy, networkProxyPort, connectionTimeout) have been removed in favor of forwarding.security and advanced options. Tests and examples have been updated accordingly.
This commit is contained in:
parent
f00bae4631
commit
1a902a04fb
@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-05-09 - 11.0.0 - BREAKING CHANGE(forwarding)
|
||||||
|
Refactor unified forwarding API and remove redundant documentation. Removed docs/forwarding-system.md (its content is migrated into readme.md) and updated helper functions (e.g. replacing sniPassthrough with httpsPassthrough) to accept configuration objects. Legacy fields in domain configurations (allowedIPs, blockedIPs, useNetworkProxy, networkProxyPort, connectionTimeout) have been removed in favor of forwarding.security and advanced options. Tests and examples have been updated accordingly.
|
||||||
|
|
||||||
|
- Removed docs/forwarding-system.md; forwarding system docs now reside in readme.md.
|
||||||
|
- Updated helper functions (httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, httpsPassthrough) to accept object parameters rather than individual arguments.
|
||||||
|
- Removed legacy domain configuration properties, shifting IP filtering to the forwarding.security field.
|
||||||
|
- Adjusted return types and API contracts for certificate provisioning and SNI handling in the unified forwarding system.
|
||||||
|
- Updated tests and examples to align with the new configuration interface.
|
||||||
|
|
||||||
## 2025-05-09 - 10.3.0 - feat(forwarding)
|
## 2025-05-09 - 10.3.0 - feat(forwarding)
|
||||||
Add unified forwarding system docs and tests; update build script and .gitignore
|
Add unified forwarding system docs and tests; update build script and .gitignore
|
||||||
|
|
||||||
|
@ -1,242 +0,0 @@
|
|||||||
# SmartProxy Unified Forwarding System
|
|
||||||
|
|
||||||
This document describes the new unified forwarding system in SmartProxy.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The forwarding system provides a clean, use-case driven approach to configuring different types of traffic forwarding. It replaces the previous disparate configuration mechanisms with a unified interface.
|
|
||||||
|
|
||||||
## Forwarding Types
|
|
||||||
|
|
||||||
The system supports four primary forwarding types:
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Basic Configuration
|
|
||||||
|
|
||||||
Each domain is configured with a forwarding type and target:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
domains: ['example.com'],
|
|
||||||
forwarding: {
|
|
||||||
type: 'http-only',
|
|
||||||
target: {
|
|
||||||
host: 'localhost',
|
|
||||||
port: 3000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Helper Functions
|
|
||||||
|
|
||||||
Helper functions are provided for common configurations:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { helpers } from '../smartproxy/forwarding/index.js';
|
|
||||||
|
|
||||||
// HTTP-only
|
|
||||||
const httpConfig = helpers.httpOnly('localhost', 3000);
|
|
||||||
|
|
||||||
// HTTPS termination to HTTP
|
|
||||||
const terminateToHttpConfig = helpers.tlsTerminateToHttp('localhost', 3000);
|
|
||||||
|
|
||||||
// HTTPS termination to HTTPS
|
|
||||||
const terminateToHttpsConfig = helpers.tlsTerminateToHttps('localhost', 8443);
|
|
||||||
|
|
||||||
// HTTPS passthrough (SNI)
|
|
||||||
const passthroughConfig = helpers.sniPassthrough('localhost', 443);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Advanced Configuration
|
|
||||||
|
|
||||||
For more complex scenarios, additional options can be specified:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
domains: ['api.example.com'],
|
|
||||||
forwarding: {
|
|
||||||
type: 'https-terminate-to-https',
|
|
||||||
target: {
|
|
||||||
host: ['10.0.0.10', '10.0.0.11'], // Round-robin load balancing
|
|
||||||
port: 8443
|
|
||||||
},
|
|
||||||
http: {
|
|
||||||
enabled: true,
|
|
||||||
redirectToHttps: true
|
|
||||||
},
|
|
||||||
https: {
|
|
||||||
// Custom certificate instead of ACME-provisioned
|
|
||||||
customCert: {
|
|
||||||
key: '-----BEGIN PRIVATE KEY-----\n...',
|
|
||||||
cert: '-----BEGIN CERTIFICATE-----\n...'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
security: {
|
|
||||||
allowedIps: ['10.0.0.*', '192.168.1.*'],
|
|
||||||
blockedIps: ['1.2.3.4'],
|
|
||||||
maxConnections: 100
|
|
||||||
},
|
|
||||||
advanced: {
|
|
||||||
timeout: 30000,
|
|
||||||
headers: {
|
|
||||||
'X-Forwarded-For': '{clientIp}',
|
|
||||||
'X-Original-Host': '{sni}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## DomainManager
|
|
||||||
|
|
||||||
The `DomainManager` class manages domains and their forwarding handlers:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { DomainManager, createDomainConfig, helpers } from '../smartproxy/forwarding/index.js';
|
|
||||||
|
|
||||||
// Create the domain manager
|
|
||||||
const domainManager = new DomainManager();
|
|
||||||
|
|
||||||
// Add a domain
|
|
||||||
await domainManager.addDomainConfig(
|
|
||||||
createDomainConfig('example.com', helpers.httpOnly('localhost', 3000))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle a connection
|
|
||||||
domainManager.handleConnection('example.com', socket);
|
|
||||||
|
|
||||||
// Handle an HTTP request
|
|
||||||
domainManager.handleHttpRequest('example.com', req, res);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Basic HTTP Server
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
domains: ['example.com'],
|
|
||||||
forwarding: {
|
|
||||||
type: 'http-only',
|
|
||||||
target: {
|
|
||||||
host: 'localhost',
|
|
||||||
port: 3000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTPS Termination with HTTP Backend
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
domains: ['secure.example.com'],
|
|
||||||
forwarding: {
|
|
||||||
type: 'https-terminate-to-http',
|
|
||||||
target: {
|
|
||||||
host: 'localhost',
|
|
||||||
port: 3000
|
|
||||||
},
|
|
||||||
acme: {
|
|
||||||
production: true // Use production Let's Encrypt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTPS Termination with HTTPS Backend
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
domains: ['secure-backend.example.com'],
|
|
||||||
forwarding: {
|
|
||||||
type: 'https-terminate-to-https',
|
|
||||||
target: {
|
|
||||||
host: 'internal-api',
|
|
||||||
port: 8443
|
|
||||||
},
|
|
||||||
http: {
|
|
||||||
redirectToHttps: true // Redirect HTTP requests to HTTPS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### SNI Passthrough
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
domains: ['passthrough.example.com'],
|
|
||||||
forwarding: {
|
|
||||||
type: 'https-passthrough',
|
|
||||||
target: {
|
|
||||||
host: '10.0.0.5',
|
|
||||||
port: 443
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Load Balancing
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
domains: ['api.example.com'],
|
|
||||||
forwarding: {
|
|
||||||
type: 'https-terminate-to-https',
|
|
||||||
target: {
|
|
||||||
host: ['10.0.0.10', '10.0.0.11', '10.0.0.12'], // Round-robin
|
|
||||||
port: 8443
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration with SmartProxy
|
|
||||||
|
|
||||||
The unified forwarding system integrates with SmartProxy by replacing the existing domain configuration mechanism. The `DomainManager` handles all domain matching and forwarding, while the individual forwarding handlers handle the connections and requests.
|
|
||||||
|
|
||||||
The system is designed to be used in SmartProxy's `ConnectionHandler` and in the `Port80Handler` for HTTP traffic.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
See the `test.forwarding.ts` file for examples of how to test the forwarding system.
|
|
||||||
|
|
||||||
## Migration
|
|
||||||
|
|
||||||
When migrating from the older configuration system, map the existing configuration to the appropriate forwarding type:
|
|
||||||
|
|
||||||
1. HTTP forwarding → `http-only`
|
|
||||||
2. SNI forwarding → `https-passthrough`
|
|
||||||
3. NetworkProxy with HTTP backend → `https-terminate-to-http`
|
|
||||||
4. NetworkProxy with HTTPS backend → `https-terminate-to-https`
|
|
||||||
|
|
||||||
## Extensibility
|
|
||||||
|
|
||||||
The forwarding system is designed to be extensible:
|
|
||||||
|
|
||||||
1. New forwarding types can be added by:
|
|
||||||
- Adding a new type to `ForwardingType`
|
|
||||||
- Creating a new handler class
|
|
||||||
- Adding the handler to `ForwardingHandlerFactory`
|
|
||||||
|
|
||||||
2. Existing types can be extended with new options by updating the interface and handler implementations.
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
The system uses a factory pattern to create the appropriate handler for each forwarding type. Each handler extends a base `ForwardingHandler` class that provides common functionality.
|
|
||||||
|
|
||||||
The `DomainManager` manages the domains and their handlers, and delegates connections and requests to the appropriate handler.
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
- The system uses a map for fast domain lookups
|
|
||||||
- Wildcard domains are supported through pattern matching
|
|
||||||
- Handlers are reused for multiple domains with the same configuration
|
|
164
readme.md
164
readme.md
@ -6,6 +6,7 @@ A high-performance proxy toolkit for Node.js, offering:
|
|||||||
- Low-level port forwarding via nftables
|
- Low-level port forwarding via nftables
|
||||||
- HTTP-to-HTTPS and custom URL redirects
|
- HTTP-to-HTTPS and custom URL redirects
|
||||||
- Advanced TCP/SNI-based proxying with IP filtering and rules
|
- Advanced TCP/SNI-based proxying with IP filtering and rules
|
||||||
|
- Unified forwarding configuration system for all proxy types
|
||||||
|
|
||||||
## Exports
|
## Exports
|
||||||
The following classes and interfaces are provided:
|
The following classes and interfaces are provided:
|
||||||
@ -23,11 +24,14 @@ The following classes and interfaces are provided:
|
|||||||
TCP/SNI-based proxy with dynamic routing, IP filtering, and unified certificates.
|
TCP/SNI-based proxy with dynamic routing, IP filtering, and unified certificates.
|
||||||
- **SniHandler** (ts/smartproxy/classes.pp.snihandler.ts)
|
- **SniHandler** (ts/smartproxy/classes.pp.snihandler.ts)
|
||||||
Static utilities to extract SNI hostnames from TLS handshakes.
|
Static utilities to extract SNI hostnames from TLS handshakes.
|
||||||
|
- **Forwarding Handlers** (ts/smartproxy/forwarding/*.ts)
|
||||||
|
Unified forwarding handlers for different connection types (HTTP, HTTPS passthrough, TLS termination).
|
||||||
- **Interfaces**
|
- **Interfaces**
|
||||||
- IPortProxySettings, IDomainConfig (ts/smartproxy/classes.pp.interfaces.ts)
|
- IPortProxySettings, IDomainConfig (ts/smartproxy/classes.pp.interfaces.ts)
|
||||||
- INetworkProxyOptions (ts/networkproxy/classes.np.types.ts)
|
- INetworkProxyOptions (ts/networkproxy/classes.np.types.ts)
|
||||||
- IAcmeOptions, IDomainOptions, IForwardConfig (ts/common/types.ts)
|
- IAcmeOptions, IDomainOptions (ts/common/types.ts)
|
||||||
- INfTableProxySettings (ts/nfttablesproxy/classes.nftablesproxy.ts)
|
- INfTableProxySettings (ts/nfttablesproxy/classes.nftablesproxy.ts)
|
||||||
|
- IForwardConfig, ForwardingType (ts/smartproxy/types/forwarding.types.ts)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Install via npm:
|
Install via npm:
|
||||||
@ -134,16 +138,37 @@ await nft.stop();
|
|||||||
### 5. TCP/SNI Proxy (SmartProxy)
|
### 5. TCP/SNI Proxy (SmartProxy)
|
||||||
```typescript
|
```typescript
|
||||||
import { SmartProxy } from '@push.rocks/smartproxy';
|
import { SmartProxy } from '@push.rocks/smartproxy';
|
||||||
|
import { createDomainConfig, httpOnly, tlsTerminateToHttp, httpsPassthrough } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
const smart = new SmartProxy({
|
const smart = new SmartProxy({
|
||||||
fromPort: 443,
|
fromPort: 443,
|
||||||
toPort: 8443,
|
toPort: 8443,
|
||||||
domainConfigs: [
|
domainConfigs: [
|
||||||
{
|
// HTTPS passthrough example
|
||||||
domains: ['example.com', '*.example.com'],
|
createDomainConfig(['example.com', '*.example.com'],
|
||||||
allowedIPs: ['*'],
|
httpsPassthrough({
|
||||||
targetIPs: ['127.0.0.1'],
|
target: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 443
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
allowedIps: ['*']
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
// HTTPS termination example
|
||||||
|
createDomainConfig('secure.example.com',
|
||||||
|
tlsTerminateToHttp({
|
||||||
|
target: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 3000
|
||||||
|
},
|
||||||
|
acme: {
|
||||||
|
enabled: true,
|
||||||
|
production: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
],
|
],
|
||||||
sniEnabled: true
|
sniEnabled: true
|
||||||
});
|
});
|
||||||
@ -386,6 +411,126 @@ Listen for certificate events via EventEmitter:
|
|||||||
|
|
||||||
Provide a `certProvisionFunction(domain)` in SmartProxy settings to supply static certs or return `'http01'`.
|
Provide a `certProvisionFunction(domain)` in SmartProxy settings to supply static certs or return `'http01'`.
|
||||||
|
|
||||||
|
## Unified Forwarding System
|
||||||
|
|
||||||
|
The SmartProxy Unified Forwarding System provides a clean, use-case driven approach to configuring different types of traffic forwarding. It replaces disparate configuration mechanisms with a unified interface.
|
||||||
|
|
||||||
|
### Forwarding Types
|
||||||
|
|
||||||
|
The system supports four primary forwarding types:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Basic Configuration
|
||||||
|
|
||||||
|
Each domain is configured with a forwarding type and target:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
domains: ['example.com'],
|
||||||
|
forwarding: {
|
||||||
|
type: 'http-only',
|
||||||
|
target: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helper Functions
|
||||||
|
|
||||||
|
Helper functions are provided for common configurations:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createDomainConfig, httpOnly, tlsTerminateToHttp,
|
||||||
|
tlsTerminateToHttps, httpsPassthrough } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
// HTTP-only
|
||||||
|
await domainManager.addDomainConfig(
|
||||||
|
createDomainConfig('example.com', httpOnly({
|
||||||
|
target: { host: 'localhost', port: 3000 }
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// HTTPS termination to HTTP
|
||||||
|
await domainManager.addDomainConfig(
|
||||||
|
createDomainConfig('secure.example.com', tlsTerminateToHttp({
|
||||||
|
target: { host: 'localhost', port: 3000 },
|
||||||
|
acme: { production: true }
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// HTTPS termination to HTTPS
|
||||||
|
await domainManager.addDomainConfig(
|
||||||
|
createDomainConfig('api.example.com', tlsTerminateToHttps({
|
||||||
|
target: { host: 'internal-api', port: 8443 },
|
||||||
|
http: { redirectToHttps: true }
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// HTTPS passthrough (SNI)
|
||||||
|
await domainManager.addDomainConfig(
|
||||||
|
createDomainConfig('passthrough.example.com', httpsPassthrough({
|
||||||
|
target: { host: '10.0.0.5', port: 443 }
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
For more complex scenarios, additional options can be specified:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
domains: ['api.example.com'],
|
||||||
|
forwarding: {
|
||||||
|
type: 'https-terminate-to-https',
|
||||||
|
target: {
|
||||||
|
host: ['10.0.0.10', '10.0.0.11'], // Round-robin load balancing
|
||||||
|
port: 8443
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
enabled: true,
|
||||||
|
redirectToHttps: true
|
||||||
|
},
|
||||||
|
https: {
|
||||||
|
// Custom certificate instead of ACME-provisioned
|
||||||
|
customCert: {
|
||||||
|
key: '-----BEGIN PRIVATE KEY-----\n...',
|
||||||
|
cert: '-----BEGIN CERTIFICATE-----\n...'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
allowedIps: ['10.0.0.*', '192.168.1.*'],
|
||||||
|
blockedIps: ['1.2.3.4'],
|
||||||
|
maxConnections: 100
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
'X-Forwarded-For': '{clientIp}',
|
||||||
|
'X-Original-Host': '{sni}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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<string, string> }
|
||||||
|
- `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<string, string> }
|
||||||
|
|
||||||
## Configuration Options
|
## Configuration Options
|
||||||
|
|
||||||
### NetworkProxy (INetworkProxyOptions)
|
### NetworkProxy (INetworkProxyOptions)
|
||||||
@ -425,12 +570,14 @@ Provide a `certProvisionFunction(domain)` in SmartProxy settings to supply stati
|
|||||||
|
|
||||||
### SmartProxy (IPortProxySettings)
|
### SmartProxy (IPortProxySettings)
|
||||||
- `fromPort`, `toPort` (number)
|
- `fromPort`, `toPort` (number)
|
||||||
- `domainConfigs` (IDomainConfig[])
|
- `domainConfigs` (IDomainConfig[]) - Using unified forwarding configuration
|
||||||
- `sniEnabled`, `defaultAllowedIPs`, `preserveSourceIP` (booleans)
|
- `sniEnabled`, `preserveSourceIP` (booleans)
|
||||||
|
- `defaultAllowedIPs`, `defaultBlockedIPs` (string[]) - Default IP allowlists/blocklists
|
||||||
- Timeouts: `initialDataTimeout`, `socketTimeout`, `inactivityTimeout`, etc.
|
- Timeouts: `initialDataTimeout`, `socketTimeout`, `inactivityTimeout`, etc.
|
||||||
- Socket opts: `noDelay`, `keepAlive`, `enableKeepAliveProbes`
|
- Socket opts: `noDelay`, `keepAlive`, `enableKeepAliveProbes`
|
||||||
- `acme` (IAcmeOptions), `certProvisionFunction` (callback)
|
- `acme` (IAcmeOptions), `certProvisionFunction` (callback)
|
||||||
- `useNetworkProxy` (number[]), `networkProxyPort` (number)
|
- `useNetworkProxy` (number[]), `networkProxyPort` (number)
|
||||||
|
- `globalPortRanges` (Array<{ from: number; to: number }>)
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
@ -455,6 +602,9 @@ Provide a `certProvisionFunction(domain)` in SmartProxy settings to supply stati
|
|||||||
- Increase `initialDataTimeout`/`maxPendingDataSize` for large ClientHello
|
- Increase `initialDataTimeout`/`maxPendingDataSize` for large ClientHello
|
||||||
- Enable `enableTlsDebugLogging` to trace handshake
|
- Enable `enableTlsDebugLogging` to trace handshake
|
||||||
- Ensure `allowSessionTicket` and fragmentation support for resumption
|
- 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
|
## License and Legal Information
|
||||||
|
|
||||||
|
@ -26,7 +26,13 @@ class FakeNetworkProxyBridge {
|
|||||||
|
|
||||||
tap.test('CertProvisioner handles static provisioning', async () => {
|
tap.test('CertProvisioner handles static provisioning', async () => {
|
||||||
const domain = 'static.com';
|
const domain = 'static.com';
|
||||||
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
|
const domainConfigs: IDomainConfig[] = [{
|
||||||
|
domains: [domain],
|
||||||
|
forwarding: {
|
||||||
|
type: 'https-terminate-to-https',
|
||||||
|
target: { host: 'localhost', port: 443 }
|
||||||
|
}
|
||||||
|
}];
|
||||||
const fakePort80 = new FakePort80Handler();
|
const fakePort80 = new FakePort80Handler();
|
||||||
const fakeBridge = new FakeNetworkProxyBridge();
|
const fakeBridge = new FakeNetworkProxyBridge();
|
||||||
// certProvider returns static certificate
|
// certProvider returns static certificate
|
||||||
@ -68,7 +74,13 @@ tap.test('CertProvisioner handles static provisioning', async () => {
|
|||||||
|
|
||||||
tap.test('CertProvisioner handles http01 provisioning', async () => {
|
tap.test('CertProvisioner handles http01 provisioning', async () => {
|
||||||
const domain = 'http01.com';
|
const domain = 'http01.com';
|
||||||
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
|
const domainConfigs: IDomainConfig[] = [{
|
||||||
|
domains: [domain],
|
||||||
|
forwarding: {
|
||||||
|
type: 'https-terminate-to-http',
|
||||||
|
target: { host: 'localhost', port: 80 }
|
||||||
|
}
|
||||||
|
}];
|
||||||
const fakePort80 = new FakePort80Handler();
|
const fakePort80 = new FakePort80Handler();
|
||||||
const fakeBridge = new FakeNetworkProxyBridge();
|
const fakeBridge = new FakeNetworkProxyBridge();
|
||||||
// certProvider returns http01 directive
|
// certProvider returns http01 directive
|
||||||
@ -93,7 +105,13 @@ tap.test('CertProvisioner handles http01 provisioning', async () => {
|
|||||||
|
|
||||||
tap.test('CertProvisioner on-demand http01 renewal', async () => {
|
tap.test('CertProvisioner on-demand http01 renewal', async () => {
|
||||||
const domain = 'renew.com';
|
const domain = 'renew.com';
|
||||||
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
|
const domainConfigs: IDomainConfig[] = [{
|
||||||
|
domains: [domain],
|
||||||
|
forwarding: {
|
||||||
|
type: 'https-terminate-to-http',
|
||||||
|
target: { host: 'localhost', port: 80 }
|
||||||
|
}
|
||||||
|
}];
|
||||||
const fakePort80 = new FakePort80Handler();
|
const fakePort80 = new FakePort80Handler();
|
||||||
const fakeBridge = new FakeNetworkProxyBridge();
|
const fakeBridge = new FakeNetworkProxyBridge();
|
||||||
const certProvider = async (): Promise<ISmartProxyCertProvisionObject> => 'http01';
|
const certProvider = async (): Promise<ISmartProxyCertProvisionObject> => 'http01';
|
||||||
@ -113,7 +131,13 @@ tap.test('CertProvisioner on-demand http01 renewal', async () => {
|
|||||||
|
|
||||||
tap.test('CertProvisioner on-demand static provisioning', async () => {
|
tap.test('CertProvisioner on-demand static provisioning', async () => {
|
||||||
const domain = 'ondemand.com';
|
const domain = 'ondemand.com';
|
||||||
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
|
const domainConfigs: IDomainConfig[] = [{
|
||||||
|
domains: [domain],
|
||||||
|
forwarding: {
|
||||||
|
type: 'https-terminate-to-https',
|
||||||
|
target: { host: 'localhost', port: 443 }
|
||||||
|
}
|
||||||
|
}];
|
||||||
const fakePort80 = new FakePort80Handler();
|
const fakePort80 = new FakePort80Handler();
|
||||||
const fakeBridge = new FakeNetworkProxyBridge();
|
const fakeBridge = new FakeNetworkProxyBridge();
|
||||||
const certProvider = async (): Promise<ISmartProxyCertProvisionObject> => ({
|
const certProvider = async (): Promise<ISmartProxyCertProvisionObject> => ({
|
||||||
|
@ -16,11 +16,13 @@ tap.test('Forwarding configuration examples', async (tools) => {
|
|||||||
// Example 1: HTTP-only configuration
|
// Example 1: HTTP-only configuration
|
||||||
const httpOnlyConfig: IDomainConfig = {
|
const httpOnlyConfig: IDomainConfig = {
|
||||||
domains: ['http.example.com'],
|
domains: ['http.example.com'],
|
||||||
allowedIPs: [],
|
|
||||||
forwarding: httpOnly({
|
forwarding: httpOnly({
|
||||||
target: {
|
target: {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000
|
port: 3000
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
allowedIps: ['*'] // Allow all
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@ -30,11 +32,13 @@ tap.test('Forwarding configuration examples', async (tools) => {
|
|||||||
// Example 2: HTTPS Passthrough (SNI)
|
// Example 2: HTTPS Passthrough (SNI)
|
||||||
const httpsPassthroughConfig: IDomainConfig = {
|
const httpsPassthroughConfig: IDomainConfig = {
|
||||||
domains: ['pass.example.com'],
|
domains: ['pass.example.com'],
|
||||||
allowedIPs: [],
|
|
||||||
forwarding: httpsPassthrough({
|
forwarding: httpsPassthrough({
|
||||||
target: {
|
target: {
|
||||||
host: ['10.0.0.1', '10.0.0.2'], // Round-robin target IPs
|
host: ['10.0.0.1', '10.0.0.2'], // Round-robin target IPs
|
||||||
port: 443
|
port: 443
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
allowedIps: ['*'] // Allow all
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@ -45,7 +49,6 @@ tap.test('Forwarding configuration examples', async (tools) => {
|
|||||||
// Example 3: HTTPS Termination to HTTP Backend
|
// Example 3: HTTPS Termination to HTTP Backend
|
||||||
const terminateToHttpConfig: IDomainConfig = {
|
const terminateToHttpConfig: IDomainConfig = {
|
||||||
domains: ['secure.example.com'],
|
domains: ['secure.example.com'],
|
||||||
allowedIPs: [],
|
|
||||||
forwarding: tlsTerminateToHttp({
|
forwarding: tlsTerminateToHttp({
|
||||||
target: {
|
target: {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
@ -61,6 +64,9 @@ tap.test('Forwarding configuration examples', async (tools) => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
maintenance: true,
|
maintenance: true,
|
||||||
production: false // Use staging ACME server for testing
|
production: false // Use staging ACME server for testing
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
allowedIps: ['*'] // Allow all
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@ -71,7 +77,6 @@ tap.test('Forwarding configuration examples', async (tools) => {
|
|||||||
// Example 4: HTTPS Termination to HTTPS Backend
|
// Example 4: HTTPS Termination to HTTPS Backend
|
||||||
const terminateToHttpsConfig: IDomainConfig = {
|
const terminateToHttpsConfig: IDomainConfig = {
|
||||||
domains: ['proxy.example.com'],
|
domains: ['proxy.example.com'],
|
||||||
allowedIPs: [],
|
|
||||||
forwarding: tlsTerminateToHttps({
|
forwarding: tlsTerminateToHttps({
|
||||||
target: {
|
target: {
|
||||||
host: 'internal-api.local',
|
host: 'internal-api.local',
|
||||||
|
@ -6,13 +6,13 @@ import type { IForwardConfig, ForwardingType } from '../ts/smartproxy/types/forw
|
|||||||
import { ForwardingHandlerFactory } from '../ts/smartproxy/forwarding/forwarding.factory.js';
|
import { ForwardingHandlerFactory } from '../ts/smartproxy/forwarding/forwarding.factory.js';
|
||||||
import { createDomainConfig } from '../ts/smartproxy/forwarding/domain-config.js';
|
import { createDomainConfig } from '../ts/smartproxy/forwarding/domain-config.js';
|
||||||
import { DomainManager } from '../ts/smartproxy/forwarding/domain-manager.js';
|
import { DomainManager } from '../ts/smartproxy/forwarding/domain-manager.js';
|
||||||
import { httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, sniPassthrough } from '../ts/smartproxy/types/forwarding.types.js';
|
import { httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, httpsPassthrough } from '../ts/smartproxy/types/forwarding.types.js';
|
||||||
|
|
||||||
const helpers = {
|
const helpers = {
|
||||||
httpOnly,
|
httpOnly,
|
||||||
tlsTerminateToHttp,
|
tlsTerminateToHttp,
|
||||||
tlsTerminateToHttps,
|
tlsTerminateToHttps,
|
||||||
sniPassthrough
|
sniPassthrough: httpsPassthrough
|
||||||
};
|
};
|
||||||
|
|
||||||
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
|
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
|
||||||
@ -107,7 +107,9 @@ tap.test('DomainManager - manage domain configurations', async () => {
|
|||||||
|
|
||||||
// Add a domain configuration
|
// Add a domain configuration
|
||||||
await domainManager.addDomainConfig(
|
await domainManager.addDomainConfig(
|
||||||
createDomainConfig('example.com', helpers.httpOnly('localhost', 3000))
|
createDomainConfig('example.com', helpers.httpOnly({
|
||||||
|
target: { host: 'localhost', port: 3000 }
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check that the configuration was added
|
// Check that the configuration was added
|
||||||
@ -138,7 +140,9 @@ tap.test('DomainManager - support wildcard domains', async () => {
|
|||||||
|
|
||||||
// Add a wildcard domain configuration
|
// Add a wildcard domain configuration
|
||||||
await domainManager.addDomainConfig(
|
await domainManager.addDomainConfig(
|
||||||
createDomainConfig('*.example.com', helpers.httpOnly('localhost', 3000))
|
createDomainConfig('*.example.com', helpers.httpOnly({
|
||||||
|
target: { host: 'localhost', port: 3000 }
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Find a handler for a subdomain
|
// Find a handler for a subdomain
|
||||||
@ -150,7 +154,9 @@ tap.test('DomainManager - support wildcard domains', async () => {
|
|||||||
expect(noHandler).toBeUndefined();
|
expect(noHandler).toBeUndefined();
|
||||||
});
|
});
|
||||||
tap.test('Helper Functions - create http-only forwarding config', async () => {
|
tap.test('Helper Functions - create http-only forwarding config', async () => {
|
||||||
const config = helpers.httpOnly('localhost', 3000);
|
const config = helpers.httpOnly({
|
||||||
|
target: { host: 'localhost', port: 3000 }
|
||||||
|
});
|
||||||
expect(config.type).toEqual('http-only');
|
expect(config.type).toEqual('http-only');
|
||||||
expect(config.target.host).toEqual('localhost');
|
expect(config.target.host).toEqual('localhost');
|
||||||
expect(config.target.port).toEqual(3000);
|
expect(config.target.port).toEqual(3000);
|
||||||
@ -158,7 +164,9 @@ tap.test('Helper Functions - create http-only forwarding config', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Helper Functions - create https-terminate-to-http config', async () => {
|
tap.test('Helper Functions - create https-terminate-to-http config', async () => {
|
||||||
const config = helpers.tlsTerminateToHttp('localhost', 3000);
|
const config = helpers.tlsTerminateToHttp({
|
||||||
|
target: { host: 'localhost', port: 3000 }
|
||||||
|
});
|
||||||
expect(config.type).toEqual('https-terminate-to-http');
|
expect(config.type).toEqual('https-terminate-to-http');
|
||||||
expect(config.target.host).toEqual('localhost');
|
expect(config.target.host).toEqual('localhost');
|
||||||
expect(config.target.port).toEqual(3000);
|
expect(config.target.port).toEqual(3000);
|
||||||
@ -168,7 +176,9 @@ tap.test('Helper Functions - create https-terminate-to-http config', async () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Helper Functions - create https-terminate-to-https config', async () => {
|
tap.test('Helper Functions - create https-terminate-to-https config', async () => {
|
||||||
const config = helpers.tlsTerminateToHttps('localhost', 8443);
|
const config = helpers.tlsTerminateToHttps({
|
||||||
|
target: { host: 'localhost', port: 8443 }
|
||||||
|
});
|
||||||
expect(config.type).toEqual('https-terminate-to-https');
|
expect(config.type).toEqual('https-terminate-to-https');
|
||||||
expect(config.target.host).toEqual('localhost');
|
expect(config.target.host).toEqual('localhost');
|
||||||
expect(config.target.port).toEqual(8443);
|
expect(config.target.port).toEqual(8443);
|
||||||
@ -178,7 +188,9 @@ tap.test('Helper Functions - create https-terminate-to-https config', async () =
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Helper Functions - create https-passthrough config', async () => {
|
tap.test('Helper Functions - create https-passthrough config', async () => {
|
||||||
const config = helpers.sniPassthrough('localhost', 443);
|
const config = helpers.sniPassthrough({
|
||||||
|
target: { host: 'localhost', port: 443 }
|
||||||
|
});
|
||||||
expect(config.type).toEqual('https-passthrough');
|
expect(config.type).toEqual('https-passthrough');
|
||||||
expect(config.target.host).toEqual('localhost');
|
expect(config.target.host).toEqual('localhost');
|
||||||
expect(config.target.port).toEqual(443);
|
expect(config.target.port).toEqual(443);
|
||||||
|
@ -6,13 +6,13 @@ import type { IForwardConfig } from '../ts/smartproxy/types/forwarding.types.js'
|
|||||||
import { ForwardingHandlerFactory } from '../ts/smartproxy/forwarding/forwarding.factory.js';
|
import { ForwardingHandlerFactory } from '../ts/smartproxy/forwarding/forwarding.factory.js';
|
||||||
import { createDomainConfig } from '../ts/smartproxy/forwarding/domain-config.js';
|
import { createDomainConfig } from '../ts/smartproxy/forwarding/domain-config.js';
|
||||||
import { DomainManager } from '../ts/smartproxy/forwarding/domain-manager.js';
|
import { DomainManager } from '../ts/smartproxy/forwarding/domain-manager.js';
|
||||||
import { httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, sniPassthrough } from '../ts/smartproxy/types/forwarding.types.js';
|
import { httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, httpsPassthrough } from '../ts/smartproxy/types/forwarding.types.js';
|
||||||
|
|
||||||
const helpers = {
|
const helpers = {
|
||||||
httpOnly,
|
httpOnly,
|
||||||
tlsTerminateToHttp,
|
tlsTerminateToHttp,
|
||||||
tlsTerminateToHttps,
|
tlsTerminateToHttps,
|
||||||
sniPassthrough
|
sniPassthrough: httpsPassthrough
|
||||||
};
|
};
|
||||||
|
|
||||||
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
|
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
|
||||||
@ -107,7 +107,9 @@ tap.test('DomainManager - manage domain configurations', async () => {
|
|||||||
|
|
||||||
// Add a domain configuration
|
// Add a domain configuration
|
||||||
await domainManager.addDomainConfig(
|
await domainManager.addDomainConfig(
|
||||||
createDomainConfig('example.com', helpers.httpOnly('localhost', 3000))
|
createDomainConfig('example.com', helpers.httpOnly({
|
||||||
|
target: { host: 'localhost', port: 3000 }
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check that the configuration was added
|
// Check that the configuration was added
|
||||||
@ -125,7 +127,9 @@ tap.test('DomainManager - manage domain configurations', async () => {
|
|||||||
expect(configsAfterRemoval.length).toEqual(0);
|
expect(configsAfterRemoval.length).toEqual(0);
|
||||||
});
|
});
|
||||||
tap.test('Helper Functions - create http-only forwarding config', async () => {
|
tap.test('Helper Functions - create http-only forwarding config', async () => {
|
||||||
const config = helpers.httpOnly('localhost', 3000);
|
const config = helpers.httpOnly({
|
||||||
|
target: { host: 'localhost', port: 3000 }
|
||||||
|
});
|
||||||
expect(config.type).toEqual('http-only');
|
expect(config.type).toEqual('http-only');
|
||||||
expect(config.target.host).toEqual('localhost');
|
expect(config.target.host).toEqual('localhost');
|
||||||
expect(config.target.port).toEqual(3000);
|
expect(config.target.port).toEqual(3000);
|
||||||
@ -133,7 +137,9 @@ tap.test('Helper Functions - create http-only forwarding config', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Helper Functions - create https-terminate-to-http config', async () => {
|
tap.test('Helper Functions - create https-terminate-to-http config', async () => {
|
||||||
const config = helpers.tlsTerminateToHttp('localhost', 3000);
|
const config = helpers.tlsTerminateToHttp({
|
||||||
|
target: { host: 'localhost', port: 3000 }
|
||||||
|
});
|
||||||
expect(config.type).toEqual('https-terminate-to-http');
|
expect(config.type).toEqual('https-terminate-to-http');
|
||||||
expect(config.target.host).toEqual('localhost');
|
expect(config.target.host).toEqual('localhost');
|
||||||
expect(config.target.port).toEqual(3000);
|
expect(config.target.port).toEqual(3000);
|
||||||
@ -143,7 +149,9 @@ tap.test('Helper Functions - create https-terminate-to-http config', async () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Helper Functions - create https-terminate-to-https config', async () => {
|
tap.test('Helper Functions - create https-terminate-to-https config', async () => {
|
||||||
const config = helpers.tlsTerminateToHttps('localhost', 8443);
|
const config = helpers.tlsTerminateToHttps({
|
||||||
|
target: { host: 'localhost', port: 8443 }
|
||||||
|
});
|
||||||
expect(config.type).toEqual('https-terminate-to-https');
|
expect(config.type).toEqual('https-terminate-to-https');
|
||||||
expect(config.target.host).toEqual('localhost');
|
expect(config.target.host).toEqual('localhost');
|
||||||
expect(config.target.port).toEqual(8443);
|
expect(config.target.port).toEqual(8443);
|
||||||
@ -153,7 +161,9 @@ tap.test('Helper Functions - create https-terminate-to-https config', async () =
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Helper Functions - create https-passthrough config', async () => {
|
tap.test('Helper Functions - create https-passthrough config', async () => {
|
||||||
const config = helpers.sniPassthrough('localhost', 443);
|
const config = helpers.sniPassthrough({
|
||||||
|
target: { host: 'localhost', port: 443 }
|
||||||
|
});
|
||||||
expect(config.type).toEqual('https-passthrough');
|
expect(config.type).toEqual('https-passthrough');
|
||||||
expect(config.target.host).toEqual('localhost');
|
expect(config.target.host).toEqual('localhost');
|
||||||
expect(config.target.port).toEqual(443);
|
expect(config.target.port).toEqual(443);
|
||||||
|
@ -575,4 +575,4 @@ process.on('exit', () => {
|
|||||||
testProxy.stop().then(() => console.log('[TEST] Proxy server stopped'));
|
testProxy.stop().then(() => console.log('[TEST] Proxy server stopped'));
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@ -279,13 +279,20 @@ tap.test('should support optional source IP preservation in chained proxies', as
|
|||||||
if (index4 !== -1) allProxies.splice(index4, 1);
|
if (index4 !== -1) allProxies.splice(index4, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test round-robin behavior for multiple target IPs in a domain config.
|
// Test round-robin behavior for multiple target hosts in a domain config.
|
||||||
tap.test('should use round robin for multiple target IPs in domain config', async () => {
|
tap.test('should use round robin for multiple target hosts in domain config', async () => {
|
||||||
|
// Create a domain config with multiple hosts in the target
|
||||||
const domainConfig = {
|
const domainConfig = {
|
||||||
domains: ['rr.test'],
|
domains: ['rr.test'],
|
||||||
allowedIPs: ['127.0.0.1'],
|
forwarding: {
|
||||||
targetIPs: ['hostA', 'hostB']
|
type: 'http-only',
|
||||||
} as any;
|
target: {
|
||||||
|
host: ['hostA', 'hostB'], // Array of hosts for round-robin
|
||||||
|
port: 80
|
||||||
|
},
|
||||||
|
http: { enabled: true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const proxyInstance = new SmartProxy({
|
const proxyInstance = new SmartProxy({
|
||||||
fromPort: 0,
|
fromPort: 0,
|
||||||
@ -299,8 +306,11 @@ tap.test('should use round robin for multiple target IPs in domain config', asyn
|
|||||||
|
|
||||||
// Don't track this proxy as it doesn't actually start or listen
|
// Don't track this proxy as it doesn't actually start or listen
|
||||||
|
|
||||||
const firstTarget = proxyInstance.domainConfigManager.getTargetIP(domainConfig);
|
// Get the first target host from the forwarding config
|
||||||
const secondTarget = proxyInstance.domainConfigManager.getTargetIP(domainConfig);
|
const firstTarget = proxyInstance.domainConfigManager.getTargetHost(domainConfig);
|
||||||
|
// Get the second target host - should be different due to round-robin
|
||||||
|
const secondTarget = proxyInstance.domainConfigManager.getTargetHost(domainConfig);
|
||||||
|
|
||||||
expect(firstTarget).toEqual('hostA');
|
expect(firstTarget).toEqual('hostA');
|
||||||
expect(secondTarget).toEqual('hostB');
|
expect(secondTarget).toEqual('hostB');
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '10.3.0',
|
version: '11.0.0',
|
||||||
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 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.'
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,15 @@ import type {
|
|||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IForwardConfig as INewForwardConfig
|
IForwardConfig
|
||||||
} from '../smartproxy/types/forwarding.types.js';
|
} from '../smartproxy/types/forwarding.types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a new forwarding configuration target to the legacy format
|
* Converts a forwarding configuration target to the legacy format
|
||||||
* for Port80Handler
|
* for Port80Handler
|
||||||
*/
|
*/
|
||||||
export function convertToLegacyForwardConfig(
|
export function convertToLegacyForwardConfig(
|
||||||
forwardConfig: INewForwardConfig
|
forwardConfig: IForwardConfig
|
||||||
): ILegacyForwardConfig {
|
): ILegacyForwardConfig {
|
||||||
// Determine host from the target configuration
|
// Determine host from the target configuration
|
||||||
const host = Array.isArray(forwardConfig.target.host)
|
const host = Array.isArray(forwardConfig.target.host)
|
||||||
@ -32,7 +32,7 @@ export function convertToLegacyForwardConfig(
|
|||||||
*/
|
*/
|
||||||
export function createPort80HandlerOptions(
|
export function createPort80HandlerOptions(
|
||||||
domain: string,
|
domain: string,
|
||||||
forwardConfig: INewForwardConfig
|
forwardConfig: IForwardConfig
|
||||||
): IDomainOptions {
|
): IDomainOptions {
|
||||||
// Determine if we should redirect HTTP to HTTPS
|
// Determine if we should redirect HTTP to HTTPS
|
||||||
let sslRedirect = false;
|
let sslRedirect = false;
|
||||||
|
@ -38,22 +38,30 @@ async function main() {
|
|||||||
|
|
||||||
// Example 1: HTTP-only forwarding
|
// Example 1: HTTP-only forwarding
|
||||||
await domainManager.addDomainConfig(
|
await domainManager.addDomainConfig(
|
||||||
createDomainConfig('example.com', helpers.httpOnly('localhost', 3000))
|
createDomainConfig('example.com', helpers.httpOnly({
|
||||||
|
target: { host: 'localhost', port: 3000 }
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Example 2: HTTPS termination with HTTP backend
|
// Example 2: HTTPS termination with HTTP backend
|
||||||
await domainManager.addDomainConfig(
|
await domainManager.addDomainConfig(
|
||||||
createDomainConfig('secure.example.com', helpers.tlsTerminateToHttp('localhost', 3000))
|
createDomainConfig('secure.example.com', helpers.tlsTerminateToHttp({
|
||||||
|
target: { host: 'localhost', port: 3000 }
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Example 3: HTTPS termination with HTTPS backend
|
// Example 3: HTTPS termination with HTTPS backend
|
||||||
await domainManager.addDomainConfig(
|
await domainManager.addDomainConfig(
|
||||||
createDomainConfig('api.example.com', helpers.tlsTerminateToHttps('localhost', 8443))
|
createDomainConfig('api.example.com', helpers.tlsTerminateToHttps({
|
||||||
|
target: { host: 'localhost', port: 8443 }
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Example 4: SNI passthrough
|
// Example 4: SNI passthrough
|
||||||
await domainManager.addDomainConfig(
|
await domainManager.addDomainConfig(
|
||||||
createDomainConfig('passthrough.example.com', helpers.sniPassthrough('10.0.0.5', 443))
|
createDomainConfig('passthrough.example.com', helpers.sniPassthrough({
|
||||||
|
target: { host: '10.0.0.5', port: 443 }
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Example 5: Custom configuration for a more complex setup
|
// Example 5: Custom configuration for a more complex setup
|
||||||
|
@ -11,7 +11,7 @@ import { TlsManager } from './classes.pp.tlsmanager.js';
|
|||||||
import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
||||||
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
|
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
|
||||||
import { PortRangeManager } from './classes.pp.portrangemanager.js';
|
import { PortRangeManager } from './classes.pp.portrangemanager.js';
|
||||||
import type { IForwardingHandler } from './forwarding/forwarding.handler.js';
|
import type { IForwardingHandler } from './types/forwarding.types.js';
|
||||||
import type { ForwardingType } from './types/forwarding.types.js';
|
import type { ForwardingType } from './types/forwarding.types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -446,9 +446,8 @@ export class ConnectionHandler {
|
|||||||
if (domainConfig) {
|
if (domainConfig) {
|
||||||
const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
|
const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
|
||||||
|
|
||||||
// Skip IP validation if allowedIPs is empty
|
// Perform IP validation using security rules
|
||||||
if (
|
if (
|
||||||
domainConfig.allowedIPs.length > 0 &&
|
|
||||||
!this.securityManager.isIPAuthorized(
|
!this.securityManager.isIPAuthorized(
|
||||||
record.remoteIP,
|
record.remoteIP,
|
||||||
ipRules.allowedIPs,
|
ipRules.allowedIPs,
|
||||||
@ -497,10 +496,31 @@ export class ConnectionHandler {
|
|||||||
// Only apply port-based rules if the incoming port is within one of the global port ranges.
|
// Only apply port-based rules if the incoming port is within one of the global port ranges.
|
||||||
if (this.portRangeManager.isPortInGlobalRanges(localPort)) {
|
if (this.portRangeManager.isPortInGlobalRanges(localPort)) {
|
||||||
if (this.portRangeManager.shouldUseGlobalForwarding(localPort)) {
|
if (this.portRangeManager.shouldUseGlobalForwarding(localPort)) {
|
||||||
|
// Create a virtual domain config for global forwarding with security settings
|
||||||
|
const globalDomainConfig = {
|
||||||
|
domains: ['global'],
|
||||||
|
forwarding: {
|
||||||
|
type: 'http-only' as ForwardingType,
|
||||||
|
target: {
|
||||||
|
host: this.settings.targetIP!,
|
||||||
|
port: this.settings.toPort
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
allowedIps: this.settings.defaultAllowedIPs || [],
|
||||||
|
blockedIps: this.settings.defaultBlockedIPs || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the same IP filtering mechanism as domain-specific configs
|
||||||
|
const ipRules = this.domainConfigManager.getEffectiveIPRules(globalDomainConfig);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.settings.defaultAllowedIPs &&
|
!this.securityManager.isIPAuthorized(
|
||||||
this.settings.defaultAllowedIPs.length > 0 &&
|
record.remoteIP,
|
||||||
!this.securityManager.isIPAuthorized(record.remoteIP, this.settings.defaultAllowedIPs)
|
ipRules.allowedIPs,
|
||||||
|
ipRules.blockedIPs
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Connection from ${record.remoteIP} rejected: IP ${record.remoteIP} not allowed in global default allowed list.`
|
`[${connectionId}] Connection from ${record.remoteIP} rejected: IP ${record.remoteIP} not allowed in global default allowed list.`
|
||||||
@ -508,29 +528,21 @@ export class ConnectionHandler {
|
|||||||
socket.end();
|
socket.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Port-based connection from ${record.remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
|
`[${connectionId}] Port-based connection from ${record.remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setupConnection(
|
|
||||||
'',
|
setupConnection('', undefined, globalDomainConfig, localPort);
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
domains: ['global'],
|
|
||||||
allowedIPs: this.settings.defaultAllowedIPs || [],
|
|
||||||
blockedIPs: this.settings.defaultBlockedIPs || [],
|
|
||||||
targetIPs: [this.settings.targetIP!],
|
|
||||||
portRanges: [],
|
|
||||||
},
|
|
||||||
localPort
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Attempt to find a matching forced domain config based on the local port.
|
// Attempt to find a matching forced domain config based on the local port.
|
||||||
const forcedDomain = this.domainConfigManager.findDomainConfigForPort(localPort);
|
const forcedDomain = this.domainConfigManager.findDomainConfigForPort(localPort);
|
||||||
|
|
||||||
if (forcedDomain) {
|
if (forcedDomain) {
|
||||||
|
// Get effective IP rules from the domain config's forwarding security settings
|
||||||
const ipRules = this.domainConfigManager.getEffectiveIPRules(forcedDomain);
|
const ipRules = this.domainConfigManager.getEffectiveIPRules(forcedDomain);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -690,10 +702,18 @@ export class ConnectionHandler {
|
|||||||
initialDataReceived = true;
|
initialDataReceived = true;
|
||||||
record.hasReceivedInitialData = true;
|
record.hasReceivedInitialData = true;
|
||||||
|
|
||||||
if (
|
// Create default security settings for non-SNI connections
|
||||||
this.settings.defaultAllowedIPs &&
|
const defaultSecurity = {
|
||||||
this.settings.defaultAllowedIPs.length > 0 &&
|
allowedIPs: this.settings.defaultAllowedIPs || [],
|
||||||
!this.securityManager.isIPAuthorized(record.remoteIP, this.settings.defaultAllowedIPs)
|
blockedIPs: this.settings.defaultBlockedIPs || []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (defaultSecurity.allowedIPs.length > 0 &&
|
||||||
|
!this.securityManager.isIPAuthorized(
|
||||||
|
record.remoteIP,
|
||||||
|
defaultSecurity.allowedIPs,
|
||||||
|
defaultSecurity.blockedIPs
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return rejectIncomingConnection(
|
return rejectIncomingConnection(
|
||||||
'rejected',
|
'rejected',
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import type { IDomainConfig, ISmartProxyOptions } from './classes.pp.interfaces.js';
|
import type { IDomainConfig, ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||||
import type { ForwardingType, IForwardConfig } from './types/forwarding.types.js';
|
import type { ForwardingType, IForwardConfig, IForwardingHandler } from './types/forwarding.types.js';
|
||||||
import { ForwardingHandlerFactory } from './forwarding/forwarding.factory.js';
|
import { ForwardingHandlerFactory } from './forwarding/forwarding.factory.js';
|
||||||
import type { IForwardingHandler } from './forwarding/forwarding.handler.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages domain configurations and target selection
|
* Manages domain configurations and target selection
|
||||||
@ -79,10 +78,12 @@ export class DomainConfigManager {
|
|||||||
*/
|
*/
|
||||||
public findDomainConfigForPort(port: number): IDomainConfig | undefined {
|
public findDomainConfigForPort(port: number): IDomainConfig | undefined {
|
||||||
return this.settings.domainConfigs.find(
|
return this.settings.domainConfigs.find(
|
||||||
(domain) =>
|
(domain) => {
|
||||||
domain.portRanges &&
|
const portRanges = domain.forwarding?.advanced?.portRanges;
|
||||||
domain.portRanges.length > 0 &&
|
return portRanges &&
|
||||||
this.isPortInRanges(port, domain.portRanges)
|
portRanges.length > 0 &&
|
||||||
|
this.isPortInRanges(port, portRanges);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +112,14 @@ export class DomainConfigManager {
|
|||||||
return this.settings.targetIP || 'localhost';
|
return this.settings.targetIP || 'localhost';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get target host with round-robin support (for tests)
|
||||||
|
* This is just an alias for getTargetIP for easier test compatibility
|
||||||
|
*/
|
||||||
|
public getTargetHost(domainConfig: IDomainConfig): string {
|
||||||
|
return this.getTargetIP(domainConfig);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get target port from domain config
|
* Get target port from domain config
|
||||||
*/
|
*/
|
||||||
@ -122,16 +131,9 @@ export class DomainConfigManager {
|
|||||||
* Checks if a domain should use NetworkProxy
|
* Checks if a domain should use NetworkProxy
|
||||||
*/
|
*/
|
||||||
public shouldUseNetworkProxy(domainConfig: IDomainConfig): boolean {
|
public shouldUseNetworkProxy(domainConfig: IDomainConfig): boolean {
|
||||||
// Check forwarding type first
|
|
||||||
const forwardingType = this.getForwardingType(domainConfig);
|
const forwardingType = this.getForwardingType(domainConfig);
|
||||||
|
return forwardingType === 'https-terminate-to-http' ||
|
||||||
if (forwardingType === 'https-terminate-to-http' ||
|
forwardingType === 'https-terminate-to-https';
|
||||||
forwardingType === 'https-terminate-to-https') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to legacy setting
|
|
||||||
return !!domainConfig.useNetworkProxy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -143,17 +145,14 @@ export class DomainConfigManager {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check forwarding config first
|
return domainConfig.forwarding.advanced?.networkProxyPort || this.settings.networkProxyPort;
|
||||||
if (domainConfig.forwarding?.advanced?.networkProxyPort) {
|
|
||||||
return domainConfig.forwarding.advanced.networkProxyPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to legacy setting
|
|
||||||
return domainConfig.networkProxyPort || this.settings.networkProxyPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get effective allowed and blocked IPs for a domain
|
* Get effective allowed and blocked IPs for a domain
|
||||||
|
*
|
||||||
|
* This method combines domain-specific security rules from the forwarding configuration
|
||||||
|
* with global security defaults when necessary.
|
||||||
*/
|
*/
|
||||||
public getEffectiveIPRules(domainConfig: IDomainConfig): {
|
public getEffectiveIPRules(domainConfig: IDomainConfig): {
|
||||||
allowedIPs: string[],
|
allowedIPs: string[],
|
||||||
@ -163,31 +162,33 @@ export class DomainConfigManager {
|
|||||||
const allowedIPs: string[] = [];
|
const allowedIPs: string[] = [];
|
||||||
const blockedIPs: string[] = [];
|
const blockedIPs: string[] = [];
|
||||||
|
|
||||||
// Add IPs from forwarding security settings
|
// Add IPs from forwarding security settings if available
|
||||||
if (domainConfig.forwarding?.security?.allowedIps) {
|
if (domainConfig.forwarding?.security?.allowedIps) {
|
||||||
allowedIPs.push(...domainConfig.forwarding.security.allowedIps);
|
allowedIPs.push(...domainConfig.forwarding.security.allowedIps);
|
||||||
|
} else {
|
||||||
|
// If no allowed IPs are specified in forwarding config and global defaults exist, use them
|
||||||
|
if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
||||||
|
allowedIPs.push(...this.settings.defaultAllowedIPs);
|
||||||
|
} else {
|
||||||
|
// Default to allow all if no specific rules
|
||||||
|
allowedIPs.push('*');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add blocked IPs from forwarding security settings if available
|
||||||
if (domainConfig.forwarding?.security?.blockedIps) {
|
if (domainConfig.forwarding?.security?.blockedIps) {
|
||||||
blockedIPs.push(...domainConfig.forwarding.security.blockedIps);
|
blockedIPs.push(...domainConfig.forwarding.security.blockedIps);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add legacy settings
|
// Always add global blocked IPs, even if domain has its own rules
|
||||||
if (domainConfig.allowedIPs.length > 0) {
|
// This ensures that global blocks take precedence
|
||||||
allowedIPs.push(...domainConfig.allowedIPs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domainConfig.blockedIPs && domainConfig.blockedIPs.length > 0) {
|
|
||||||
blockedIPs.push(...domainConfig.blockedIPs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add global defaults
|
|
||||||
if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
||||||
allowedIPs.push(...this.settings.defaultAllowedIPs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.settings.defaultBlockedIPs && this.settings.defaultBlockedIPs.length > 0) {
|
if (this.settings.defaultBlockedIPs && this.settings.defaultBlockedIPs.length > 0) {
|
||||||
blockedIPs.push(...this.settings.defaultBlockedIPs);
|
// Add only unique IPs that aren't already in the list
|
||||||
|
for (const ip of this.settings.defaultBlockedIPs) {
|
||||||
|
if (!blockedIPs.includes(ip)) {
|
||||||
|
blockedIPs.push(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -200,16 +201,10 @@ export class DomainConfigManager {
|
|||||||
* Get connection timeout for a domain
|
* Get connection timeout for a domain
|
||||||
*/
|
*/
|
||||||
public getConnectionTimeout(domainConfig?: IDomainConfig): number {
|
public getConnectionTimeout(domainConfig?: IDomainConfig): number {
|
||||||
// First check forwarding configuration for timeout
|
if (domainConfig?.forwarding.advanced?.timeout) {
|
||||||
if (domainConfig?.forwarding?.advanced?.timeout) {
|
|
||||||
return domainConfig.forwarding.advanced.timeout;
|
return domainConfig.forwarding.advanced.timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to legacy connectionTimeout
|
|
||||||
if (domainConfig?.connectionTimeout) {
|
|
||||||
return domainConfig.connectionTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.settings.maxConnectionLifetime || 86400000; // 24 hours default
|
return this.settings.maxConnectionLifetime || 86400000; // 24 hours default
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,10 +212,6 @@ export class DomainConfigManager {
|
|||||||
* Creates a forwarding handler for a domain configuration
|
* Creates a forwarding handler for a domain configuration
|
||||||
*/
|
*/
|
||||||
private createForwardingHandler(domainConfig: IDomainConfig): IForwardingHandler {
|
private createForwardingHandler(domainConfig: IDomainConfig): IForwardingHandler {
|
||||||
if (!domainConfig.forwarding) {
|
|
||||||
throw new Error(`Domain config for ${domainConfig.domains.join(', ')} has no forwarding configuration`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new handler using the factory
|
// Create a new handler using the factory
|
||||||
const handler = ForwardingHandlerFactory.createHandler(domainConfig.forwarding);
|
const handler = ForwardingHandlerFactory.createHandler(domainConfig.forwarding);
|
||||||
|
|
||||||
|
@ -84,8 +84,10 @@ export class PortRangeManager {
|
|||||||
} | undefined {
|
} | undefined {
|
||||||
for (let i = 0; i < this.settings.domainConfigs.length; i++) {
|
for (let i = 0; i < this.settings.domainConfigs.length; i++) {
|
||||||
const domain = this.settings.domainConfigs[i];
|
const domain = this.settings.domainConfigs[i];
|
||||||
if (domain.portRanges) {
|
// Get port ranges from forwarding.advanced if available
|
||||||
for (const range of domain.portRanges) {
|
const portRanges = domain.forwarding?.advanced?.portRanges;
|
||||||
|
if (portRanges && portRanges.length > 0) {
|
||||||
|
for (const range of portRanges) {
|
||||||
if (port >= range.from && port <= range.to) {
|
if (port >= range.from && port <= range.to) {
|
||||||
return { domainIndex: i, range };
|
return { domainIndex: i, range };
|
||||||
}
|
}
|
||||||
@ -129,17 +131,20 @@ export class PortRangeManager {
|
|||||||
|
|
||||||
// Add domain-specific port ranges
|
// Add domain-specific port ranges
|
||||||
for (const domain of this.settings.domainConfigs) {
|
for (const domain of this.settings.domainConfigs) {
|
||||||
if (domain.portRanges) {
|
// Get port ranges from forwarding.advanced
|
||||||
for (const range of domain.portRanges) {
|
const portRanges = domain.forwarding?.advanced?.portRanges;
|
||||||
|
if (portRanges && portRanges.length > 0) {
|
||||||
|
for (const range of portRanges) {
|
||||||
for (let port = range.from; port <= range.to; port++) {
|
for (let port = range.from; port <= range.to; port++) {
|
||||||
ports.add(port);
|
ports.add(port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add domain-specific NetworkProxy port if configured
|
// Add domain-specific NetworkProxy port if configured in forwarding.advanced
|
||||||
if (domain.useNetworkProxy && domain.networkProxyPort) {
|
const networkProxyPort = domain.forwarding?.advanced?.networkProxyPort;
|
||||||
ports.add(domain.networkProxyPort);
|
if (networkProxyPort) {
|
||||||
|
ports.add(networkProxyPort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,8 +175,10 @@ export class PortRangeManager {
|
|||||||
|
|
||||||
// Track domain-specific port ranges
|
// Track domain-specific port ranges
|
||||||
for (const domain of this.settings.domainConfigs) {
|
for (const domain of this.settings.domainConfigs) {
|
||||||
if (domain.portRanges) {
|
// Get port ranges from forwarding.advanced
|
||||||
for (const range of domain.portRanges) {
|
const portRanges = domain.forwarding?.advanced?.portRanges;
|
||||||
|
if (portRanges && portRanges.length > 0) {
|
||||||
|
for (const range of portRanges) {
|
||||||
for (let port = range.from; port <= range.to; port++) {
|
for (let port = range.from; port <= range.to; port++) {
|
||||||
if (!portMappings.has(port)) {
|
if (!portMappings.has(port)) {
|
||||||
portMappings.set(port, []);
|
portMappings.set(port, []);
|
||||||
|
@ -63,7 +63,17 @@ export class SecurityManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an IP is allowed using glob patterns
|
* Check if an IP is authorized using forwarding security rules
|
||||||
|
*
|
||||||
|
* This method is used to determine if an IP is allowed to connect, based on security
|
||||||
|
* rules configured in the forwarding configuration. The allowed and blocked IPs are
|
||||||
|
* typically derived from domain.forwarding.security.allowedIps and blockedIps through
|
||||||
|
* DomainConfigManager.getEffectiveIPRules().
|
||||||
|
*
|
||||||
|
* @param ip - The IP address to check
|
||||||
|
* @param allowedIPs - Array of allowed IP patterns from forwarding.security.allowedIps
|
||||||
|
* @param blockedIPs - Array of blocked IP patterns from forwarding.security.blockedIps
|
||||||
|
* @returns true if IP is authorized, false if blocked
|
||||||
*/
|
*/
|
||||||
public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean {
|
public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean {
|
||||||
// Skip IP validation if allowedIPs is empty
|
// Skip IP validation if allowedIPs is empty
|
||||||
@ -71,7 +81,7 @@ export class SecurityManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First check if IP is blocked
|
// First check if IP is blocked - blocked IPs take precedence
|
||||||
if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) {
|
if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -81,27 +91,41 @@ export class SecurityManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the IP matches any of the glob patterns
|
* Check if the IP matches any of the glob patterns from security configuration
|
||||||
|
*
|
||||||
|
* This method checks IP addresses against glob patterns and handles IPv4/IPv6 normalization.
|
||||||
|
* It's used to implement IP filtering based on the forwarding.security configuration.
|
||||||
|
*
|
||||||
|
* @param ip - The IP address to check
|
||||||
|
* @param patterns - Array of glob patterns from forwarding.security.allowedIps or blockedIps
|
||||||
|
* @returns true if IP matches any pattern, false otherwise
|
||||||
*/
|
*/
|
||||||
private isGlobIPMatch(ip: string, patterns: string[]): boolean {
|
private isGlobIPMatch(ip: string, patterns: string[]): boolean {
|
||||||
if (!ip || !patterns || patterns.length === 0) return false;
|
if (!ip || !patterns || patterns.length === 0) return false;
|
||||||
|
|
||||||
|
// Handle IPv4/IPv6 normalization for proper matching
|
||||||
const normalizeIP = (ip: string): string[] => {
|
const normalizeIP = (ip: string): string[] => {
|
||||||
if (!ip) return [];
|
if (!ip) return [];
|
||||||
|
// Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1)
|
||||||
if (ip.startsWith('::ffff:')) {
|
if (ip.startsWith('::ffff:')) {
|
||||||
const ipv4 = ip.slice(7);
|
const ipv4 = ip.slice(7);
|
||||||
return [ip, ipv4];
|
return [ip, ipv4];
|
||||||
}
|
}
|
||||||
|
// Handle IPv4 addresses by also checking IPv4-mapped form
|
||||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
||||||
return [ip, `::ffff:${ip}`];
|
return [ip, `::ffff:${ip}`];
|
||||||
}
|
}
|
||||||
return [ip];
|
return [ip];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Normalize the IP being checked
|
||||||
const normalizedIPVariants = normalizeIP(ip);
|
const normalizedIPVariants = normalizeIP(ip);
|
||||||
if (normalizedIPVariants.length === 0) return false;
|
if (normalizedIPVariants.length === 0) return false;
|
||||||
|
|
||||||
|
// Normalize the pattern IPs for consistent comparison
|
||||||
const expandedPatterns = patterns.flatMap(normalizeIP);
|
const expandedPatterns = patterns.flatMap(normalizeIP);
|
||||||
|
|
||||||
|
// Check for any match between normalized IP variants and patterns
|
||||||
return normalizedIPVariants.some((ipVariant) =>
|
return normalizedIPVariants.some((ipVariant) =>
|
||||||
expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
|
expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
|
||||||
);
|
);
|
||||||
|
@ -61,8 +61,8 @@ export class TimeoutManager {
|
|||||||
* Calculate effective max lifetime based on connection type
|
* Calculate effective max lifetime based on connection type
|
||||||
*/
|
*/
|
||||||
public getEffectiveMaxLifetime(record: IConnectionRecord): number {
|
public getEffectiveMaxLifetime(record: IConnectionRecord): number {
|
||||||
// Use domain-specific timeout if available
|
// Use domain-specific timeout from forwarding.advanced if available
|
||||||
const baseTimeout = record.domainConfig?.connectionTimeout ||
|
const baseTimeout = record.domainConfig?.forwarding?.advanced?.timeout ||
|
||||||
this.settings.maxConnectionLifetime ||
|
this.settings.maxConnectionLifetime ||
|
||||||
86400000; // 24 hours default
|
86400000; // 24 hours default
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
|||||||
import { CertProvisioner } from './classes.pp.certprovisioner.js';
|
import { CertProvisioner } from './classes.pp.certprovisioner.js';
|
||||||
import type { ICertificateData } from '../common/types.js';
|
import type { ICertificateData } from '../common/types.js';
|
||||||
import { buildPort80Handler } from '../common/acmeFactory.js';
|
import { buildPort80Handler } from '../common/acmeFactory.js';
|
||||||
import { ensureForwardingConfig } from './forwarding/legacy-converter.js';
|
|
||||||
import type { ForwardingType } from './types/forwarding.types.js';
|
import type { ForwardingType } from './types/forwarding.types.js';
|
||||||
import { createPort80HandlerOptions } from '../common/port80-adapter.js';
|
import { createPort80HandlerOptions } from '../common/port80-adapter.js';
|
||||||
|
|
||||||
@ -159,8 +158,8 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-process domain configs to ensure they all have forwarding configurations
|
// Process domain configs
|
||||||
this.settings.domainConfigs = this.settings.domainConfigs.map(config => ensureForwardingConfig(config));
|
// Note: ensureForwardingConfig is no longer needed since forwarding is now required
|
||||||
|
|
||||||
// Initialize domain config manager with the processed configs
|
// Initialize domain config manager with the processed configs
|
||||||
this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs);
|
this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs);
|
||||||
@ -412,11 +411,8 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> {
|
public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> {
|
||||||
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
|
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
|
||||||
|
|
||||||
// Ensure each domain config has a valid forwarding configuration
|
|
||||||
const processedConfigs = newDomainConfigs.map(config => ensureForwardingConfig(config));
|
|
||||||
|
|
||||||
// Update domain configs in DomainConfigManager
|
// Update domain configs in DomainConfigManager
|
||||||
this.domainConfigManager.updateDomainConfigs(processedConfigs);
|
this.domainConfigManager.updateDomainConfigs(newDomainConfigs);
|
||||||
|
|
||||||
// If NetworkProxy is initialized, resync the configurations
|
// If NetworkProxy is initialized, resync the configurations
|
||||||
if (this.networkProxyBridge.getNetworkProxy()) {
|
if (this.networkProxyBridge.getNetworkProxy()) {
|
||||||
@ -425,15 +421,15 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
|
|
||||||
// If Port80Handler is running, provision certificates based on forwarding type
|
// If Port80Handler is running, provision certificates based on forwarding type
|
||||||
if (this.port80Handler && this.settings.acme?.enabled) {
|
if (this.port80Handler && this.settings.acme?.enabled) {
|
||||||
for (const domainConfig of processedConfigs) {
|
for (const domainConfig of newDomainConfigs) {
|
||||||
// Skip certificate provisioning for http-only or passthrough configs that don't need certs
|
// Skip certificate provisioning for http-only or passthrough configs that don't need certs
|
||||||
const forwardingType = domainConfig.forwarding?.type as ForwardingType;
|
const forwardingType = domainConfig.forwarding.type;
|
||||||
const needsCertificate =
|
const needsCertificate =
|
||||||
forwardingType === 'https-terminate-to-http' ||
|
forwardingType === 'https-terminate-to-http' ||
|
||||||
forwardingType === 'https-terminate-to-https';
|
forwardingType === 'https-terminate-to-https';
|
||||||
|
|
||||||
// Skip certificate provisioning if ACME is explicitly disabled for this domain
|
// Skip certificate provisioning if ACME is explicitly disabled for this domain
|
||||||
const acmeDisabled = domainConfig.forwarding?.acme?.enabled === false;
|
const acmeDisabled = domainConfig.forwarding.acme?.enabled === false;
|
||||||
|
|
||||||
if (!needsCertificate || acmeDisabled) {
|
if (!needsCertificate || acmeDisabled) {
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
@ -447,7 +443,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
let provision: string | plugins.tsclass.network.ICert = 'http01';
|
let provision: string | plugins.tsclass.network.ICert = 'http01';
|
||||||
|
|
||||||
// Check for ACME forwarding configuration in the domain
|
// Check for ACME forwarding configuration in the domain
|
||||||
const forwardAcmeChallenges = domainConfig.forwarding?.acme?.forwardChallenges;
|
const forwardAcmeChallenges = domainConfig.forwarding.acme?.forwardChallenges;
|
||||||
|
|
||||||
if (this.settings.certProvisionFunction) {
|
if (this.settings.certProvisionFunction) {
|
||||||
try {
|
try {
|
||||||
@ -467,7 +463,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create Port80Handler options from the forwarding configuration
|
// Create Port80Handler options from the forwarding configuration
|
||||||
const port80Config = createPort80HandlerOptions(domain, domainConfig.forwarding!);
|
const port80Config = createPort80HandlerOptions(domain, domainConfig.forwarding);
|
||||||
|
|
||||||
this.port80Handler.addDomain(port80Config);
|
this.port80Handler.addDomain(port80Config);
|
||||||
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
|
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { IForwardConfig } from '../types/forwarding.types.js';
|
import type { IForwardConfig } from '../types/forwarding.types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updated domain configuration with unified forwarding configuration
|
* Domain configuration with unified forwarding configuration
|
||||||
*/
|
*/
|
||||||
export interface IDomainConfig {
|
export interface IDomainConfig {
|
||||||
// Core properties - domain patterns
|
// Core properties - domain patterns
|
||||||
@ -9,17 +9,6 @@ export interface IDomainConfig {
|
|||||||
|
|
||||||
// Unified forwarding configuration
|
// Unified forwarding configuration
|
||||||
forwarding: IForwardConfig;
|
forwarding: IForwardConfig;
|
||||||
|
|
||||||
// Legacy security properties that will be migrated to forwarding.security
|
|
||||||
allowedIPs?: string[];
|
|
||||||
blockedIPs?: string[];
|
|
||||||
|
|
||||||
// Legacy NetworkProxy properties
|
|
||||||
useNetworkProxy?: boolean;
|
|
||||||
networkProxyPort?: number;
|
|
||||||
|
|
||||||
// Legacy timeout property
|
|
||||||
connectionTimeout?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,19 +16,13 @@ export interface IDomainConfig {
|
|||||||
*/
|
*/
|
||||||
export function createDomainConfig(
|
export function createDomainConfig(
|
||||||
domains: string | string[],
|
domains: string | string[],
|
||||||
forwarding: IForwardConfig,
|
forwarding: IForwardConfig
|
||||||
security?: {
|
|
||||||
allowedIPs?: string[];
|
|
||||||
blockedIPs?: string[];
|
|
||||||
}
|
|
||||||
): IDomainConfig {
|
): IDomainConfig {
|
||||||
// Normalize domains to an array
|
// Normalize domains to an array
|
||||||
const domainArray = Array.isArray(domains) ? domains : [domains];
|
const domainArray = Array.isArray(domains) ? domains : [domains];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
domains: domainArray,
|
domains: domainArray,
|
||||||
forwarding,
|
forwarding
|
||||||
allowedIPs: security?.allowedIPs || ['*'],
|
|
||||||
blockedIPs: security?.blockedIPs
|
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -17,7 +17,7 @@ export {
|
|||||||
httpOnly,
|
httpOnly,
|
||||||
tlsTerminateToHttp,
|
tlsTerminateToHttp,
|
||||||
tlsTerminateToHttps,
|
tlsTerminateToHttps,
|
||||||
sniPassthrough
|
httpsPassthrough
|
||||||
} from '../types/forwarding.types.js';
|
} from '../types/forwarding.types.js';
|
||||||
|
|
||||||
// Export domain configuration
|
// Export domain configuration
|
||||||
@ -41,12 +41,12 @@ import {
|
|||||||
httpOnly,
|
httpOnly,
|
||||||
tlsTerminateToHttp,
|
tlsTerminateToHttp,
|
||||||
tlsTerminateToHttps,
|
tlsTerminateToHttps,
|
||||||
sniPassthrough
|
httpsPassthrough
|
||||||
} from '../types/forwarding.types.js';
|
} from '../types/forwarding.types.js';
|
||||||
|
|
||||||
export const helpers = {
|
export const helpers = {
|
||||||
httpOnly,
|
httpOnly,
|
||||||
tlsTerminateToHttp,
|
tlsTerminateToHttp,
|
||||||
tlsTerminateToHttps,
|
tlsTerminateToHttps,
|
||||||
sniPassthrough
|
sniPassthrough: httpsPassthrough // Alias for backward compatibility
|
||||||
};
|
};
|
Loading…
x
Reference in New Issue
Block a user