20 KiB
SmartProxy Fully Unified Configuration Plan
Project Goal
Redesign SmartProxy's configuration for a more elegant, unified, and comprehensible approach by:
- Creating a single, unified configuration model that covers both "where to listen" and "how to forward"
- Eliminating the confusion between domain configs and port forwarding
- Providing a clear, declarative API that makes the intent obvious
- Enhancing maintainability and extensibility for future features
Current Issues
The current approach has several issues:
-
Dual Configuration Systems:
- Port configuration (
fromPort
,toPort
,globalPortRanges
) for "where to listen" - Domain configuration (
domainConfigs
) for "how to forward" - Unclear relationship between these two systems
- Port configuration (
-
Mixed Concerns:
- Port management is mixed with forwarding logic
- Domain routing is separated from port listening
- Security settings defined in multiple places
-
Complex Logic:
- PortRangeManager must coordinate with domain configuration
- Implicit rules for handling connections based on port and domain
-
Difficult to Understand and Configure:
- Two separate configuration hierarchies that must work together
- Unclear which settings take precedence
Proposed Solution: Fully Unified Routing Configuration
Replace both port and domain configuration with a single, unified configuration:
// The core unified configuration interface
interface IRouteConfig {
// What to match
match: {
// Listen on these ports (required)
ports: number | number[] | Array<{ from: number, to: number }>;
// Optional domain patterns to match (default: all domains)
domains?: string | string[];
// Advanced matching criteria
path?: string; // Match specific paths
clientIp?: string[]; // Match specific client IPs
tlsVersion?: string[]; // Match specific TLS versions
};
// What to do with matched traffic
action: {
// Basic routing
type: 'forward' | 'redirect' | 'block';
// Target for forwarding
target?: {
host: string | string[]; // Support single host or round-robin
port: number;
preservePort?: boolean; // Use incoming port as target port
};
// TLS handling
tls?: {
mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
certificate?: 'auto' | { // Auto = use ACME
key: string;
cert: string;
};
};
// For redirects
redirect?: {
to: string; // URL or template with {domain}, {port}, etc.
status: 301 | 302 | 307 | 308;
};
// Security options
security?: {
allowedIps?: string[];
blockedIps?: string[];
maxConnections?: number;
authentication?: {
type: 'basic' | 'digest' | 'oauth';
// Auth-specific options
};
};
// Advanced options
advanced?: {
timeout?: number;
headers?: Record<string, string>;
keepAlive?: boolean;
// etc.
};
};
// Optional metadata
name?: string; // Human-readable name for this route
description?: string; // Description of the route's purpose
priority?: number; // Controls matching order (higher = matched first)
tags?: string[]; // Arbitrary tags for categorization
}
// Main SmartProxy options
interface ISmartProxyOptions {
// The unified configuration array (required)
routes: IRouteConfig[];
// Global/default settings
defaults?: {
target?: {
host: string;
port: number;
};
security?: {
// Global security defaults
};
tls?: {
// Global TLS defaults
};
// ...other defaults
};
// Other global settings remain (acme, etc.)
acme?: IAcmeOptions;
// Advanced settings remain as well
// ...
}
Example Configuration
const proxy = new SmartProxy({
// All routing is configured in a single array
routes: [
// Basic HTTPS server with automatic certificates
{
match: {
ports: 443,
domains: ['example.com', '*.example.com']
},
action: {
type: 'forward',
target: {
host: 'localhost',
port: 8080
},
tls: {
mode: 'terminate',
certificate: 'auto' // Use ACME
}
},
name: 'Main HTTPS Server'
},
// HTTP to HTTPS redirect
{
match: {
ports: 80,
domains: ['example.com', '*.example.com']
},
action: {
type: 'redirect',
redirect: {
to: 'https://{domain}{path}',
status: 301
}
},
name: 'HTTP to HTTPS Redirect'
},
// Admin portal with IP restriction
{
match: {
ports: 8443,
domains: 'admin.example.com'
},
action: {
type: 'forward',
target: {
host: 'admin-backend',
port: 3000
},
tls: {
mode: 'terminate',
certificate: 'auto'
},
security: {
allowedIps: ['192.168.1.*', '10.0.0.*'],
maxConnections: 10
}
},
priority: 100, // Higher priority than default rules
name: 'Admin Portal'
},
// Port range for direct forwarding
{
match: {
ports: [{ from: 10000, to: 10010 }],
// No domains = all domains or direct IP
},
action: {
type: 'forward',
target: {
host: 'backend-server',
port: 10000,
preservePort: true // Use same port number as incoming
},
tls: {
mode: 'passthrough' // Direct TCP forwarding
}
},
name: 'Dynamic Port Range'
},
// Path-based routing
{
match: {
ports: 443,
domains: 'api.example.com',
path: '/v1/*'
},
action: {
type: 'forward',
target: {
host: 'api-v1-service',
port: 8001
},
tls: {
mode: 'terminate'
}
},
name: 'API v1 Endpoints'
},
// Load balanced backend
{
match: {
ports: 443,
domains: 'app.example.com'
},
action: {
type: 'forward',
target: {
// Round-robin load balancing
host: [
'app-server-1',
'app-server-2',
'app-server-3'
],
port: 8080
},
tls: {
mode: 'terminate'
},
advanced: {
headers: {
'X-Served-By': '{server}',
'X-Client-IP': '{clientIp}'
}
}
},
name: 'Load Balanced App Servers'
}
],
// Global defaults
defaults: {
target: {
host: 'localhost',
port: 8080
},
security: {
maxConnections: 1000,
// Global security defaults
}
},
// ACME configuration for auto certificates
acme: {
enabled: true,
email: 'admin@example.com',
production: true,
renewThresholdDays: 30
},
// Other global settings
// ...
});
Implementation Plan
Phase 1: Core Design & Interface Definition
-
Define New Core Interfaces:
- Create
IRouteConfig
interface withmatch
andaction
branches - Define all sub-interfaces for matching and actions
- Update
ISmartProxyOptions
to useroutes
array - Define template variable system for dynamic values
- Create
-
Create Helper Functions:
createRoute()
- Basic route creation with reasonable defaultscreateHttpRoute()
,createHttpsRoute()
,createRedirect()
- Common scenarioscreateLoadBalancer()
- For multi-target setupsmergeSecurity()
,mergeDefaults()
- For combining configs
-
Design Router:
- Decision tree for route matching algorithm
- Priority system for route ordering
- Optimized lookup strategy for fast routing
Phase 2: Core Implementation
-
Create RouteManager:
- Replaces both PortRangeManager and DomainConfigManager
- Handles all routing decisions in one place
- Provides fast lookup from port+domain+path to action
- Manages server instances for different ports
-
Update ConnectionHandler:
- Simplify to work with the unified route system
- Implement templating system for dynamic values
- Support more sophisticated routing rules
-
Implement New SmartProxy Core:
- Rewrite initialization to use route-based configuration
- Create network servers based on port specifications
- Manage TLS contexts and certificates
Phase 3: Feature Implementation
-
Path-Based Routing:
- Implement HTTP path matching
- Add wildcard and regex support for paths
- Create route differentiation based on HTTP method
-
Enhanced Security:
- Implement per-route security rules
- Add authentication methods (basic, digest, etc.)
- Support for IP-based access control
-
TLS Management:
- Support multiple certificate types (auto, manual, wildcard)
- Implement certificate selection based on SNI
- Support different TLS modes per route
-
Metrics & Monitoring:
- Per-route statistics
- Named route tracking for better visibility
- Tag-based grouping of metrics
Phase 4: Backward Compatibility
-
Legacy Adapter Layer:
- Convert old configuration format to route-based format
- Map fromPort/toPort/domainConfigs to unified routes
- Preserve all functionality during migration
-
API Compatibility:
- Support both old and new configuration methods
- Add deprecation warnings when using legacy properties
- Provide migration utilities
-
Documentation:
- Clear migration guide for existing users
- Examples mapping old config to new config
- Timeline for deprecation
Phase 5: Documentation & Examples
-
Update Core Documentation:
- Rewrite README.md with a focus on route-based configuration
- Create interface reference documentation
- Document all template variables
-
Create Example Library:
- Common configuration patterns
- Complex use cases for advanced features
- Infrastructure-as-code examples
-
Add Validation Tooling:
- Configuration validator to check for issues
- Schema definitions for IDE autocomplete
- Runtime validation helpers
Phase 6: Testing
-
Unit Tests:
- Test route matching logic
- Validate priority handling
- Test template processing
-
Integration Tests:
- Verify full proxy flows
- Test complex routing scenarios
- Check backward compatibility
-
Performance Testing:
- Benchmark routing performance
- Evaluate memory usage
- Test with large numbers of routes
Benefits of the New Approach
-
Truly Unified Configuration:
- One "source of truth" for all routing
- Entire routing flow visible in a single configuration
- No overlapping or conflicting configuration systems
-
Declarative Intent:
- Configuration clearly states what to match and what action to take
- Metadata provides context and documentation inline
- Easy to understand the purpose of each route
-
Advanced Routing Capabilities:
- Path-based routing with pattern matching
- Client IP-based conditional routing
- Fine-grained security controls
-
Composable and Extensible:
- Each route is a self-contained unit of configuration
- Routes can be grouped by tags or priority
- New match criteria or actions can be added without breaking changes
-
Better Developer Experience:
- Clear, consistent configuration pattern
- Helper functions for common scenarios
- Better error messages and validation
Example Use Cases
1. Complete Reverse Proxy with Auto SSL
const proxy = new SmartProxy({
routes: [
// HTTPS server for all domains
{
match: { ports: 443 },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: { mode: 'terminate', certificate: 'auto' }
}
},
// HTTP to HTTPS redirect
{
match: { ports: 80 },
action: {
type: 'redirect',
redirect: { to: 'https://{domain}{path}', status: 301 }
}
}
],
acme: {
enabled: true,
email: 'admin@example.com',
production: true
}
});
2. Microservices API Gateway
const apiGateway = new SmartProxy({
routes: [
// Users API
{
match: {
ports: 443,
domains: 'api.example.com',
path: '/users/*'
},
action: {
type: 'forward',
target: { host: 'users-service', port: 8001 },
tls: { mode: 'terminate', certificate: 'auto' }
},
name: 'Users Service'
},
// Products API
{
match: {
ports: 443,
domains: 'api.example.com',
path: '/products/*'
},
action: {
type: 'forward',
target: { host: 'products-service', port: 8002 },
tls: { mode: 'terminate', certificate: 'auto' }
},
name: 'Products Service'
},
// Orders API with authentication
{
match: {
ports: 443,
domains: 'api.example.com',
path: '/orders/*'
},
action: {
type: 'forward',
target: { host: 'orders-service', port: 8003 },
tls: { mode: 'terminate', certificate: 'auto' },
security: {
authentication: {
type: 'basic',
users: { 'admin': 'password' }
}
}
},
name: 'Orders Service (Protected)'
}
],
acme: {
enabled: true,
email: 'admin@example.com'
}
});
3. Multi-Environment Setup
const environments = {
production: {
target: { host: 'prod-backend', port: 8080 },
security: { maxConnections: 1000 }
},
staging: {
target: { host: 'staging-backend', port: 8080 },
security: {
allowedIps: ['10.0.0.*', '192.168.1.*'],
maxConnections: 100
}
},
development: {
target: { host: 'localhost', port: 3000 },
security: {
allowedIps: ['127.0.0.1'],
maxConnections: 10
}
}
};
// Select environment based on hostname
const proxy = new SmartProxy({
routes: [
// Production environment
{
match: {
ports: [80, 443],
domains: ['app.example.com', 'www.example.com']
},
action: {
type: 'forward',
target: environments.production.target,
tls: { mode: 'terminate', certificate: 'auto' },
security: environments.production.security
},
name: 'Production Environment'
},
// Staging environment
{
match: {
ports: [80, 443],
domains: 'staging.example.com'
},
action: {
type: 'forward',
target: environments.staging.target,
tls: { mode: 'terminate', certificate: 'auto' },
security: environments.staging.security
},
name: 'Staging Environment'
},
// Development environment
{
match: {
ports: [80, 443],
domains: 'dev.example.com'
},
action: {
type: 'forward',
target: environments.development.target,
tls: { mode: 'terminate', certificate: 'auto' },
security: environments.development.security
},
name: 'Development Environment'
}
],
acme: { enabled: true, email: 'admin@example.com' }
});
Implementation Strategy
Code Organization
-
New Files:
route-manager.ts
(core routing engine)route-types.ts
(interface definitions)route-helpers.ts
(helper functions)route-matcher.ts
(matching logic)template-engine.ts
(for variable substitution)
-
Modified Files:
smart-proxy.ts
(update to use route-based configuration)connection-handler.ts
(simplify using route-based approach)- Replace
port-range-manager.ts
anddomain-config-manager.ts
Backward Compatibility
The backward compatibility layer will convert the legacy configuration to the new format:
function convertLegacyConfig(legacy: ILegacySmartProxyOptions): ISmartProxyOptions {
const routes: IRouteConfig[] = [];
// Convert main port configuration
if (legacy.fromPort) {
// Add main listener for fromPort
routes.push({
match: { ports: legacy.fromPort },
action: {
type: 'forward',
target: { host: legacy.targetIP || 'localhost', port: legacy.toPort },
tls: { mode: legacy.sniEnabled ? 'passthrough' : 'terminate' }
},
name: 'Main Listener (Legacy)'
});
// If ACME is enabled, add HTTP listener for challenges
if (legacy.acme?.enabled) {
routes.push({
match: { ports: 80 },
action: {
type: 'forward',
target: { host: 'localhost', port: 80 },
// Special flag for ACME handler
acmeEnabled: true
},
name: 'ACME Challenge Handler (Legacy)'
});
}
}
// Convert domain configs
if (legacy.domainConfigs) {
for (const domainConfig of legacy.domainConfigs) {
const { domains, forwarding } = domainConfig;
// Determine action based on forwarding type
let action: Partial<IRouteAction> = {
type: 'forward',
target: {
host: forwarding.target.host,
port: forwarding.target.port
}
};
// Set TLS mode based on forwarding type
switch (forwarding.type) {
case 'http-only':
// No TLS
break;
case 'https-passthrough':
action.tls = { mode: 'passthrough' };
break;
case 'https-terminate-to-http':
action.tls = {
mode: 'terminate',
certificate: forwarding.https?.customCert || 'auto'
};
break;
case 'https-terminate-to-https':
action.tls = {
mode: 'terminate-and-reencrypt',
certificate: forwarding.https?.customCert || 'auto'
};
break;
}
// Security settings
if (forwarding.security) {
action.security = forwarding.security;
}
// Add HTTP redirect if needed
if (forwarding.http?.redirectToHttps) {
routes.push({
match: { ports: 80, domains },
action: {
type: 'redirect',
redirect: { to: 'https://{domain}{path}', status: 301 }
},
name: `HTTP Redirect for ${domains.join(', ')} (Legacy)`
});
}
// Add main route
routes.push({
match: {
ports: forwarding.type.startsWith('https') ? 443 : 80,
domains
},
action: action as IRouteAction,
name: `Route for ${domains.join(', ')} (Legacy)`
});
// Add port ranges if specified
if (forwarding.advanced?.portRanges) {
for (const range of forwarding.advanced.portRanges) {
routes.push({
match: {
ports: { from: range.from, to: range.to },
domains
},
action: action as IRouteAction,
name: `Port Range ${range.from}-${range.to} for ${domains.join(', ')} (Legacy)`
});
}
}
}
}
// Global port ranges
if (legacy.globalPortRanges) {
for (const range of legacy.globalPortRanges) {
routes.push({
match: { ports: { from: range.from, to: range.to } },
action: {
type: 'forward',
target: {
host: legacy.targetIP || 'localhost',
port: legacy.forwardAllGlobalRanges ? 0 : legacy.toPort,
preservePort: !!legacy.forwardAllGlobalRanges
},
tls: { mode: 'passthrough' }
},
name: `Global Port Range ${range.from}-${range.to} (Legacy)`
});
}
}
return {
routes,
defaults: {
target: {
host: legacy.targetIP || 'localhost',
port: legacy.toPort
}
},
acme: legacy.acme
};
}
Success Criteria
- All existing functionality works with the new route-based configuration
- Performance is equal or better than the current implementation
- Configuration is more intuitive and easier to understand
- New features can be added without breaking existing code
- Code is more maintainable with clear separation of concerns
- Migration from old configuration to new is straightforward