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:
Philipp Kunz 2025-05-09 15:39:15 +00:00
parent f00bae4631
commit 1a902a04fb
20 changed files with 441 additions and 434 deletions

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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> => ({

View File

@ -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',

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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');
}); });

View File

@ -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.'
} }

View File

@ -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;

View File

@ -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

View File

@ -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',

View File

@ -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);

View File

@ -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, []);

View File

@ -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))
); );

View File

@ -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

View File

@ -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`);

View File

@ -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
}; };
} }

View File

@ -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
}; };