Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
98ef91b6ea | |||
1b4d215cd4 | |||
70448af5b4 | |||
33732c2361 | |||
8d821b4e25 | |||
4b381915e1 |
42
changelog.md
42
changelog.md
@ -1,5 +1,47 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-05-19 - 19.3.6 - fix(tests)
|
||||||
|
Fix route configuration property names in tests: replace 'acceptedRoutes' with 'routes' in nftables tests and update 'match: { port: ... }' to 'match: { ports: ... }' in port forwarding tests.
|
||||||
|
|
||||||
|
- Renamed 'acceptedRoutes' to 'routes' in test/nftables-forwarding.ts for alignment with the current SmartProxy API.
|
||||||
|
- Changed port matching in test/port-forwarding-fix.ts from 'match: { port: ... }' to 'match: { ports: ... }' for consistency.
|
||||||
|
|
||||||
|
## 2025-05-19 - 19.3.6 - fix(tests)
|
||||||
|
Update test route config properties: replace 'acceptedRoutes' with 'routes' in nftables tests and change 'match: { port: ... }' to 'match: { ports: ... }' in port forwarding tests
|
||||||
|
|
||||||
|
- In test/nftables-forwarding.ts, renamed property 'acceptedRoutes' to 'routes' to align with current SmartProxy API.
|
||||||
|
- In test/port-forwarding-fix.ts, updated 'match: { port: 9999 }' to 'match: { ports: 9999 }' for consistency.
|
||||||
|
|
||||||
|
## 2025-05-19 - 19.3.5 - fix(smartproxy)
|
||||||
|
Correct NFTables forwarding handling to avoid premature connection termination and add comprehensive tests
|
||||||
|
|
||||||
|
- Removed overly aggressive socket closing for routes using NFTables forwarding in route-connection-handler.ts
|
||||||
|
- Now logs NFTables-handled connections for monitoring while letting kernel-level forwarding operate transparently
|
||||||
|
- Added and updated tests for connection forwarding, NFTables integration and port forwarding fixes
|
||||||
|
- Enhanced logging and error handling in NFTables and TLS handling functions
|
||||||
|
|
||||||
|
## 2025-05-19 - 19.3.4 - fix(docs, tests, acme)
|
||||||
|
fix: update changelog, documentation, examples and tests for v19.4.0 release. Adjust global ACME configuration to use ssl@bleu.de and add non-privileged port examples.
|
||||||
|
|
||||||
|
- Updated changelog with new v19.4.0 entry detailing fixes in tests and docs
|
||||||
|
- Revised README and certificate-management.md to demonstrate global ACME settings (using ssl@bleu.de, non-privileged port support, auto-renewal configuration, and renewCheckIntervalHours)
|
||||||
|
- Added new examples (certificate-management-v19.ts and complete-example-v19.ts) and updated existing examples (dynamic port management, NFTables integration) to reflect v19.4.0 features
|
||||||
|
- Fixed test exports and port mapping issues in several test files (acme-state-manager, port80-management, race-conditions, etc.)
|
||||||
|
- Updated readme.plan.md to reflect completed refactoring and breaking changes from v19.3.3
|
||||||
|
|
||||||
|
## 2025-05-19 - 19.4.0 - fix(tests) & docs
|
||||||
|
Fix failing tests and update documentation for v19+ features
|
||||||
|
|
||||||
|
- Fix ForwardingHandlerFactory.applyDefaults to set port and socket properties correctly
|
||||||
|
- Fix route finding logic in forwarding tests to properly identify redirect routes
|
||||||
|
- Fix test exports in acme-state-manager.node.ts, port80-management.node.ts, and race-conditions.node.ts
|
||||||
|
- Update ACME email configuration to use ssl@bleu.de instead of test domains
|
||||||
|
- Update README with v19.4.0 features including global ACME configuration
|
||||||
|
- Update certificate-management.md documentation to reflect v19+ changes
|
||||||
|
- Add new examples: certificate-management-v19.ts and complete-example-v19.ts
|
||||||
|
- Update existing examples to demonstrate global ACME configuration
|
||||||
|
- Update readme.plan.md to reflect completed refactoring
|
||||||
|
|
||||||
## 2025-05-19 - 19.3.3 - fix(core)
|
## 2025-05-19 - 19.3.3 - fix(core)
|
||||||
No changes detected – project structure and documentation remain unchanged.
|
No changes detected – project structure and documentation remain unchanged.
|
||||||
|
|
||||||
|
@ -208,11 +208,18 @@ const smartproxy = new SmartProxy({
|
|||||||
// Certificate provisioning was automatic or via certProvisionFunction
|
// Certificate provisioning was automatic or via certProvisionFunction
|
||||||
```
|
```
|
||||||
|
|
||||||
### After (v18+)
|
### After (v19+)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// New approach with route-based configuration
|
// New approach with global ACME configuration
|
||||||
const smartproxy = new SmartProxy({
|
const smartproxy = new SmartProxy({
|
||||||
|
// Global ACME defaults (v19+)
|
||||||
|
acme: {
|
||||||
|
email: 'ssl@bleu.de',
|
||||||
|
useProduction: true,
|
||||||
|
port: 80 // Or 8080 for non-privileged
|
||||||
|
},
|
||||||
|
|
||||||
routes: [{
|
routes: [{
|
||||||
match: { ports: 443, domains: 'example.com' },
|
match: { ports: 443, domains: 'example.com' },
|
||||||
action: {
|
action: {
|
||||||
@ -220,11 +227,7 @@ const smartproxy = new SmartProxy({
|
|||||||
target: { host: 'localhost', port: 8080 },
|
target: { host: 'localhost', port: 8080 },
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto' // Uses global ACME settings
|
||||||
acme: {
|
|
||||||
email: 'admin@example.com',
|
|
||||||
useProduction: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
@ -235,9 +238,38 @@ const smartproxy = new SmartProxy({
|
|||||||
|
|
||||||
### Common Issues
|
### Common Issues
|
||||||
|
|
||||||
1. **Certificate not provisioning**: Ensure port 80 is accessible for ACME challenges
|
1. **Certificate not provisioning**: Ensure the ACME challenge port (80 or configured port) is accessible
|
||||||
2. **ACME rate limits**: Use staging environment for testing
|
2. **ACME rate limits**: Use staging environment for testing (`useProduction: false`)
|
||||||
3. **Permission errors**: Ensure the certificate directory is writable
|
3. **Permission errors**: Ensure the certificate directory is writable
|
||||||
|
4. **Invalid email domain**: ACME servers may reject certain email domains (e.g., example.com). Use a real email domain
|
||||||
|
5. **Port binding errors**: Use higher ports (e.g., 8080) if running without root privileges
|
||||||
|
|
||||||
|
### Using Non-Privileged Ports
|
||||||
|
|
||||||
|
For development or non-root environments:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const proxy = new SmartProxy({
|
||||||
|
acme: {
|
||||||
|
email: 'ssl@bleu.de',
|
||||||
|
port: 8080, // Use 8080 instead of 80
|
||||||
|
useProduction: false
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
match: { ports: 8443, domains: 'example.com' },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: { host: 'localhost', port: 3000 },
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: 'auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### Debug Mode
|
### Debug Mode
|
||||||
|
|
||||||
|
119
examples/certificate-management-v19.ts
Normal file
119
examples/certificate-management-v19.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* Certificate Management Example (v19+)
|
||||||
|
*
|
||||||
|
* This example demonstrates the new global ACME configuration introduced in v19+
|
||||||
|
* along with route-level overrides for specific domains.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
SmartProxy,
|
||||||
|
createHttpRoute,
|
||||||
|
createHttpsTerminateRoute,
|
||||||
|
createCompleteHttpsServer
|
||||||
|
} from '../dist_ts/index.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Create a SmartProxy instance with global ACME configuration
|
||||||
|
const proxy = new SmartProxy({
|
||||||
|
// Global ACME configuration (v19+)
|
||||||
|
// These settings apply to all routes with certificate: 'auto'
|
||||||
|
acme: {
|
||||||
|
email: 'ssl@bleu.de', // Global contact email
|
||||||
|
useProduction: false, // Use staging by default
|
||||||
|
port: 8080, // Use non-privileged port
|
||||||
|
renewThresholdDays: 30, // Renew 30 days before expiry
|
||||||
|
autoRenew: true, // Enable automatic renewal
|
||||||
|
renewCheckIntervalHours: 12 // Check twice daily
|
||||||
|
},
|
||||||
|
|
||||||
|
routes: [
|
||||||
|
// Route that uses global ACME settings
|
||||||
|
createHttpsTerminateRoute('app.example.com',
|
||||||
|
{ host: 'localhost', port: 3000 },
|
||||||
|
{ certificate: 'auto' } // Uses global ACME configuration
|
||||||
|
),
|
||||||
|
|
||||||
|
// Route with route-level ACME override
|
||||||
|
{
|
||||||
|
name: 'production-api',
|
||||||
|
match: { ports: 443, domains: 'api.example.com' },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: { host: 'localhost', port: 3001 },
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: 'auto',
|
||||||
|
acme: {
|
||||||
|
email: 'api-certs@example.com', // Override email
|
||||||
|
useProduction: true, // Use production for API
|
||||||
|
renewThresholdDays: 60 // Earlier renewal for critical API
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Complete HTTPS server with automatic redirects
|
||||||
|
...createCompleteHttpsServer('website.example.com',
|
||||||
|
{ host: 'localhost', port: 8080 },
|
||||||
|
{ certificate: 'auto' }
|
||||||
|
),
|
||||||
|
|
||||||
|
// Static certificate (not using ACME)
|
||||||
|
{
|
||||||
|
name: 'internal-service',
|
||||||
|
match: { ports: 8443, domains: 'internal.local' },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: { host: 'localhost', port: 3002 },
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: {
|
||||||
|
cert: '-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----',
|
||||||
|
key: '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor certificate events
|
||||||
|
proxy.on('certificate:issued', (event) => {
|
||||||
|
console.log(`Certificate issued for ${event.domain}`);
|
||||||
|
console.log(`Expires: ${event.expiryDate}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy.on('certificate:renewed', (event) => {
|
||||||
|
console.log(`Certificate renewed for ${event.domain}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy.on('certificate:error', (event) => {
|
||||||
|
console.error(`Certificate error for ${event.domain}: ${event.error}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the proxy
|
||||||
|
await proxy.start();
|
||||||
|
console.log('SmartProxy started with global ACME configuration');
|
||||||
|
|
||||||
|
// Check certificate status programmatically
|
||||||
|
setTimeout(async () => {
|
||||||
|
// Get status for a specific route
|
||||||
|
const status = proxy.getCertificateStatus('app-route');
|
||||||
|
console.log('Certificate status:', status);
|
||||||
|
|
||||||
|
// Manually trigger renewal if needed
|
||||||
|
if (status && status.status === 'expiring') {
|
||||||
|
await proxy.renewCertificate('app-route');
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
// Handle shutdown gracefully
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
console.log('Shutting down proxy...');
|
||||||
|
await proxy.stop();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the example
|
||||||
|
main().catch(console.error);
|
188
examples/complete-example-v19.ts
Normal file
188
examples/complete-example-v19.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* Complete SmartProxy Example (v19+)
|
||||||
|
*
|
||||||
|
* This comprehensive example demonstrates all major features of SmartProxy v19+:
|
||||||
|
* - Global ACME configuration
|
||||||
|
* - Route-based configuration
|
||||||
|
* - Helper functions for common patterns
|
||||||
|
* - Dynamic route management
|
||||||
|
* - Certificate status monitoring
|
||||||
|
* - Error handling and recovery
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
SmartProxy,
|
||||||
|
createHttpRoute,
|
||||||
|
createHttpsTerminateRoute,
|
||||||
|
createHttpsPassthroughRoute,
|
||||||
|
createHttpToHttpsRedirect,
|
||||||
|
createCompleteHttpsServer,
|
||||||
|
createLoadBalancerRoute,
|
||||||
|
createApiRoute,
|
||||||
|
createWebSocketRoute,
|
||||||
|
createStaticFileRoute,
|
||||||
|
createNfTablesRoute
|
||||||
|
} from '../dist_ts/index.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Create SmartProxy with comprehensive configuration
|
||||||
|
const proxy = new SmartProxy({
|
||||||
|
// Global ACME configuration (v19+)
|
||||||
|
acme: {
|
||||||
|
email: 'ssl@bleu.de',
|
||||||
|
useProduction: false, // Use staging for this example
|
||||||
|
port: 8080, // Non-privileged port for development
|
||||||
|
autoRenew: true,
|
||||||
|
renewCheckIntervalHours: 12
|
||||||
|
},
|
||||||
|
|
||||||
|
// Initial routes
|
||||||
|
routes: [
|
||||||
|
// Basic HTTP service
|
||||||
|
createHttpRoute('api.example.com', { host: 'localhost', port: 3000 }),
|
||||||
|
|
||||||
|
// HTTPS with automatic certificates
|
||||||
|
createHttpsTerminateRoute('secure.example.com',
|
||||||
|
{ host: 'localhost', port: 3001 },
|
||||||
|
{ certificate: 'auto' }
|
||||||
|
),
|
||||||
|
|
||||||
|
// Complete HTTPS server with HTTP->HTTPS redirect
|
||||||
|
...createCompleteHttpsServer('www.example.com',
|
||||||
|
{ host: 'localhost', port: 8080 },
|
||||||
|
{ certificate: 'auto' }
|
||||||
|
),
|
||||||
|
|
||||||
|
// Load balancer with multiple backends
|
||||||
|
createLoadBalancerRoute(
|
||||||
|
'app.example.com',
|
||||||
|
['10.0.0.1', '10.0.0.2', '10.0.0.3'],
|
||||||
|
8080,
|
||||||
|
{
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: 'auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
// API route with CORS
|
||||||
|
createApiRoute('api.example.com', '/v1',
|
||||||
|
{ host: 'api-backend', port: 8081 },
|
||||||
|
{
|
||||||
|
useTls: true,
|
||||||
|
certificate: 'auto',
|
||||||
|
addCorsHeaders: true
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
// WebSocket support
|
||||||
|
createWebSocketRoute('ws.example.com', '/socket',
|
||||||
|
{ host: 'websocket-server', port: 8082 },
|
||||||
|
{
|
||||||
|
useTls: true,
|
||||||
|
certificate: 'auto'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
// Static file server
|
||||||
|
createStaticFileRoute(['cdn.example.com', 'static.example.com'],
|
||||||
|
'/var/www/static',
|
||||||
|
{
|
||||||
|
serveOnHttps: true,
|
||||||
|
certificate: 'auto'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
// HTTPS passthrough for services that handle their own TLS
|
||||||
|
createHttpsPassthroughRoute('legacy.example.com',
|
||||||
|
{ host: '192.168.1.100', port: 443 }
|
||||||
|
),
|
||||||
|
|
||||||
|
// HTTP to HTTPS redirects
|
||||||
|
createHttpToHttpsRedirect(['*.example.com', 'example.com'])
|
||||||
|
],
|
||||||
|
|
||||||
|
// Enable detailed logging for debugging
|
||||||
|
enableDetailedLogging: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
proxy.on('connection', (event) => {
|
||||||
|
console.log(`New connection: ${event.source} -> ${event.destination}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy.on('certificate:issued', (event) => {
|
||||||
|
console.log(`Certificate issued for ${event.domain}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy.on('certificate:renewed', (event) => {
|
||||||
|
console.log(`Certificate renewed for ${event.domain}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy.on('error', (error) => {
|
||||||
|
console.error('Proxy error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the proxy
|
||||||
|
await proxy.start();
|
||||||
|
console.log('SmartProxy started successfully');
|
||||||
|
console.log('Listening on ports:', proxy.getListeningPorts());
|
||||||
|
|
||||||
|
// Demonstrate dynamic route management
|
||||||
|
setTimeout(async () => {
|
||||||
|
console.log('Adding new route dynamically...');
|
||||||
|
|
||||||
|
// Get current routes and add a new one
|
||||||
|
const currentRoutes = proxy.settings.routes;
|
||||||
|
const newRoutes = [
|
||||||
|
...currentRoutes,
|
||||||
|
createHttpsTerminateRoute('new-service.example.com',
|
||||||
|
{ host: 'localhost', port: 3003 },
|
||||||
|
{ certificate: 'auto' }
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Update routes
|
||||||
|
await proxy.updateRoutes(newRoutes);
|
||||||
|
console.log('New route added successfully');
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Check certificate status periodically
|
||||||
|
setInterval(async () => {
|
||||||
|
const routes = proxy.settings.routes;
|
||||||
|
for (const route of routes) {
|
||||||
|
if (route.action.tls?.certificate === 'auto') {
|
||||||
|
const status = proxy.getCertificateStatus(route.name);
|
||||||
|
if (status) {
|
||||||
|
console.log(`Certificate status for ${route.name}:`, status);
|
||||||
|
|
||||||
|
// Renew if expiring soon
|
||||||
|
if (status.status === 'expiring') {
|
||||||
|
console.log(`Renewing certificate for ${route.name}...`);
|
||||||
|
await proxy.renewCertificate(route.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 3600000); // Check every hour
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
console.log('Shutting down gracefully...');
|
||||||
|
await proxy.stop();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
console.log('Received SIGTERM, shutting down...');
|
||||||
|
await proxy.stop();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the example
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Failed to start proxy:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
@ -3,27 +3,25 @@
|
|||||||
*
|
*
|
||||||
* This example demonstrates how to dynamically add and remove ports
|
* This example demonstrates how to dynamically add and remove ports
|
||||||
* while SmartProxy is running, without requiring a restart.
|
* while SmartProxy is running, without requiring a restart.
|
||||||
|
* Also shows the new v19+ global ACME configuration.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SmartProxy } from '../dist_ts/index.js';
|
import { SmartProxy, createHttpRoute, createHttpsTerminateRoute } from '../dist_ts/index.js';
|
||||||
import type { IRouteConfig } from '../dist_ts/index.js';
|
import type { IRouteConfig } from '../dist_ts/index.js';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Create a SmartProxy instance with initial routes
|
// Create a SmartProxy instance with initial routes and global ACME config
|
||||||
const proxy = new SmartProxy({
|
const proxy = new SmartProxy({
|
||||||
|
// Global ACME configuration (v19+)
|
||||||
|
acme: {
|
||||||
|
email: 'ssl@bleu.de',
|
||||||
|
useProduction: false,
|
||||||
|
port: 8080 // Using non-privileged port for ACME challenges
|
||||||
|
},
|
||||||
|
|
||||||
routes: [
|
routes: [
|
||||||
// Initial route on port 8080
|
// Initial route on port 8080
|
||||||
{
|
createHttpRoute(['example.com', '*.example.com'], { host: 'localhost', port: 3000 })
|
||||||
match: {
|
|
||||||
ports: 8080,
|
|
||||||
domains: ['example.com', '*.example.com']
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
type: 'forward',
|
|
||||||
target: { host: 'localhost', port: 3000 }
|
|
||||||
},
|
|
||||||
name: 'Initial HTTP Route'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
* for high-performance network routing that operates at the kernel level.
|
* for high-performance network routing that operates at the kernel level.
|
||||||
*
|
*
|
||||||
* NOTE: This requires elevated privileges to run (sudo) as it interacts with nftables.
|
* NOTE: This requires elevated privileges to run (sudo) as it interacts with nftables.
|
||||||
|
* Also shows the new v19+ global ACME configuration.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
@ -50,15 +51,22 @@ async function simpleForwardingExample() {
|
|||||||
async function httpsTerminationExample() {
|
async function httpsTerminationExample() {
|
||||||
console.log('Starting HTTPS termination with NFTables example...');
|
console.log('Starting HTTPS termination with NFTables example...');
|
||||||
|
|
||||||
// Create a SmartProxy instance with an HTTPS termination route using NFTables
|
// Create a SmartProxy instance with global ACME and NFTables HTTPS termination
|
||||||
const proxy = new SmartProxy({
|
const proxy = new SmartProxy({
|
||||||
|
// Global ACME configuration (v19+)
|
||||||
|
acme: {
|
||||||
|
email: 'ssl@bleu.de',
|
||||||
|
useProduction: false,
|
||||||
|
port: 80 // NFTables needs root, so we can use port 80
|
||||||
|
},
|
||||||
|
|
||||||
routes: [
|
routes: [
|
||||||
createNfTablesTerminateRoute('secure.example.com', {
|
createNfTablesTerminateRoute('secure.example.com', {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8443
|
port: 8443
|
||||||
}, {
|
}, {
|
||||||
ports: 443,
|
ports: 443,
|
||||||
certificate: 'auto', // Automatic certificate provisioning
|
certificate: 'auto', // Uses global ACME configuration
|
||||||
tableName: 'smartproxy_https'
|
tableName: 'smartproxy_https'
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "19.3.3",
|
"version": "19.3.6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
|
29
readme.md
29
readme.md
@ -113,7 +113,7 @@ npm install @push.rocks/smartproxy
|
|||||||
|
|
||||||
## Quick Start with SmartProxy
|
## Quick Start with SmartProxy
|
||||||
|
|
||||||
SmartProxy v18.0.0 continues the evolution of the unified route-based configuration system making your proxy setup more flexible and intuitive with improved helper functions and NFTables integration for high-performance kernel-level routing.
|
SmartProxy v19.4.0 provides a unified route-based configuration system with enhanced certificate management, NFTables integration for high-performance kernel-level routing, and improved helper functions for common proxy setups.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import {
|
import {
|
||||||
@ -136,10 +136,12 @@ import {
|
|||||||
const proxy = new SmartProxy({
|
const proxy = new SmartProxy({
|
||||||
// Global ACME settings for all routes with certificate: 'auto'
|
// Global ACME settings for all routes with certificate: 'auto'
|
||||||
acme: {
|
acme: {
|
||||||
email: 'ssl@example.com', // Required for Let's Encrypt
|
email: 'ssl@bleu.de', // Required for Let's Encrypt
|
||||||
useProduction: false, // Use staging by default
|
useProduction: false, // Use staging by default
|
||||||
renewThresholdDays: 30, // Renew 30 days before expiry
|
renewThresholdDays: 30, // Renew 30 days before expiry
|
||||||
port: 80 // Port for HTTP-01 challenges
|
port: 80, // Port for HTTP-01 challenges (use 8080 for non-privileged)
|
||||||
|
autoRenew: true, // Enable automatic renewal
|
||||||
|
renewCheckIntervalHours: 24 // Check for renewals daily
|
||||||
},
|
},
|
||||||
|
|
||||||
// Define all your routing rules in a single array
|
// Define all your routing rules in a single array
|
||||||
@ -216,26 +218,7 @@ const proxy = new SmartProxy({
|
|||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
maxRate: '100mbps'
|
maxRate: '100mbps'
|
||||||
})
|
})
|
||||||
],
|
]
|
||||||
|
|
||||||
// Global settings that apply to all routes
|
|
||||||
defaults: {
|
|
||||||
security: {
|
|
||||||
maxConnections: 500
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Automatic Let's Encrypt integration
|
|
||||||
acme: {
|
|
||||||
enabled: true,
|
|
||||||
contactEmail: 'admin@example.com',
|
|
||||||
useProduction: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for certificate events
|
|
||||||
proxy.on('certificate', evt => {
|
|
||||||
console.log(`Certificate for ${evt.domain} ready, expires: ${evt.expiryDate}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the proxy
|
// Start the proxy
|
||||||
|
@ -1,22 +1,15 @@
|
|||||||
# SmartProxy Architecture Refactoring Plan
|
# SmartProxy v19.4.0 - Completed Refactoring
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Refactor the proxy architecture to provide clearer separation of concerns between HTTP/HTTPS traffic handling and low-level connection routing.
|
SmartProxy has been successfully refactored with clearer separation of concerns between HTTP/HTTPS traffic handling and low-level connection routing. Version 19.4.0 introduces global ACME configuration and enhanced route management.
|
||||||
|
|
||||||
## Current Architecture Problems
|
## Current Architecture (v19.4.0)
|
||||||
|
|
||||||
1. NetworkProxy name doesn't clearly indicate it handles HTTP/HTTPS
|
### HttpProxy (formerly NetworkProxy)
|
||||||
2. HTTP parsing logic is duplicated in RouteConnectionHandler
|
|
||||||
3. Redirect and static route handling is embedded in SmartProxy
|
|
||||||
4. Unclear separation between TCP routing and HTTP processing
|
|
||||||
|
|
||||||
## Proposed Architecture
|
|
||||||
|
|
||||||
### HttpProxy (renamed from NetworkProxy)
|
|
||||||
**Purpose**: Handle all HTTP/HTTPS traffic with TLS termination
|
**Purpose**: Handle all HTTP/HTTPS traffic with TLS termination
|
||||||
|
|
||||||
**Responsibilities**:
|
**Current Responsibilities**:
|
||||||
- TLS termination for HTTPS
|
- TLS termination for HTTPS
|
||||||
- HTTP/1.1 and HTTP/2 protocol handling
|
- HTTP/1.1 and HTTP/2 protocol handling
|
||||||
- HTTP request/response parsing
|
- HTTP request/response parsing
|
||||||
@ -25,29 +18,33 @@ Refactor the proxy architecture to provide clearer separation of concerns betwee
|
|||||||
- Static route handlers
|
- Static route handlers
|
||||||
- WebSocket protocol upgrades
|
- WebSocket protocol upgrades
|
||||||
- Connection pooling for backend servers
|
- Connection pooling for backend servers
|
||||||
- Certificate management (ACME and static)
|
- Certificate management integration
|
||||||
|
|
||||||
### SmartProxy
|
### SmartProxy
|
||||||
**Purpose**: Low-level connection router and port manager
|
**Purpose**: Central API for all proxy needs with route-based configuration
|
||||||
|
|
||||||
**Responsibilities**:
|
**Current Responsibilities**:
|
||||||
- Port management (listen on multiple ports)
|
- Port management (listen on multiple ports)
|
||||||
- Route-based connection routing
|
- Route-based connection routing
|
||||||
- TLS passthrough (SNI-based routing)
|
- TLS passthrough (SNI-based routing)
|
||||||
- NFTables integration
|
- NFTables integration
|
||||||
- Delegate HTTP/HTTPS connections to HttpProxy
|
- Certificate management via SmartCertManager
|
||||||
- Raw TCP proxying
|
- Raw TCP proxying
|
||||||
- Connection lifecycle management
|
- Connection lifecycle management
|
||||||
|
- Global ACME configuration (v19+)
|
||||||
|
|
||||||
## Implementation Plan
|
## Completed Implementation
|
||||||
|
|
||||||
### Phase 1: Rename and Reorganize NetworkProxy ✅
|
### Phase 1: Rename and Reorganize ✅
|
||||||
|
- NetworkProxy renamed to HttpProxy
|
||||||
|
- Directory structure reorganized
|
||||||
|
- All imports and references updated
|
||||||
|
|
||||||
1. **Rename NetworkProxy to HttpProxy**
|
### Phase 2: Certificate Management ✅
|
||||||
- Renamed directory from `network-proxy` to `http-proxy`
|
- Unified certificate management in SmartCertManager
|
||||||
- Updated all imports and references
|
- Global ACME configuration support (v19+)
|
||||||
|
- Route-level certificate overrides
|
||||||
2. **Update class and file names**
|
- Automatic renewal system
|
||||||
- Renamed `network-proxy.ts` to `http-proxy.ts`
|
- Renamed `network-proxy.ts` to `http-proxy.ts`
|
||||||
- Updated `NetworkProxy` class to `HttpProxy` class
|
- Updated `NetworkProxy` class to `HttpProxy` class
|
||||||
- Updated all type definitions and interfaces
|
- Updated all type definitions and interfaces
|
||||||
@ -157,16 +154,26 @@ After this refactoring, we can more easily add:
|
|||||||
4. Protocol-specific optimizations
|
4. Protocol-specific optimizations
|
||||||
5. Better HTTP/2 multiplexing
|
5. Better HTTP/2 multiplexing
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes from v18 to v19
|
||||||
|
|
||||||
1. `NetworkProxy` class renamed to `HttpProxy`
|
1. `NetworkProxy` class renamed to `HttpProxy`
|
||||||
2. Import paths change from `network-proxy` to `http-proxy`
|
2. Import paths change from `network-proxy` to `http-proxy`
|
||||||
3. Some type names may change for consistency
|
3. Global ACME configuration now available at the top level
|
||||||
|
4. Certificate management unified under SmartCertManager
|
||||||
|
|
||||||
## Rollback Plan
|
## Future Enhancements
|
||||||
|
|
||||||
If issues arise:
|
1. HTTP/3 (QUIC) support in HttpProxy
|
||||||
1. Git revert to previous commit
|
2. Advanced HTTP features (compression, caching)
|
||||||
2. Re-deploy previous version
|
3. HTTP middleware system
|
||||||
3. Document lessons learned
|
4. Protocol-specific optimizations
|
||||||
4. Plan incremental changes
|
5. Better HTTP/2 multiplexing
|
||||||
|
6. Enhanced monitoring and metrics
|
||||||
|
|
||||||
|
## Key Features in v19.4.0
|
||||||
|
|
||||||
|
1. **Global ACME Configuration**: Default settings for all routes with `certificate: 'auto'`
|
||||||
|
2. **Enhanced Route Management**: Better separation between routing and certificate management
|
||||||
|
3. **Improved Test Coverage**: Fixed test exports and port bindings
|
||||||
|
4. **Better Error Messages**: Clear guidance for ACME configuration issues
|
||||||
|
5. **Non-Privileged Port Support**: Examples for development environments
|
21
test/helpers/test-cert.pem
Normal file
21
test/helpers/test-cert.pem
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDizCCAnOgAwIBAgIUAzpwtk6k5v/7LfY1KR7PreezvsswDQYJKoZIhvcNAQEL
|
||||||
|
BQAwVTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx
|
||||||
|
DTALBgNVBAoMBFRlc3QxGTAXBgNVBAMMEHRlc3QuZXhhbXBsZS5jb20wHhcNMjUw
|
||||||
|
NTE5MTc1MDM0WhcNMjYwNTE5MTc1MDM0WjBVMQswCQYDVQQGEwJVUzENMAsGA1UE
|
||||||
|
CAwEVGVzdDENMAsGA1UEBwwEVGVzdDENMAsGA1UECgwEVGVzdDEZMBcGA1UEAwwQ
|
||||||
|
dGVzdC5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||||
|
AK9FivUNjXz5q+snqKLCno0i3cYzJ+LTzSf+x+a/G7CA/rtigIvSYEqWC4+/MXPM
|
||||||
|
ifpU/iIRtj7RzoPKH44uJie7mS5kKSHsMnh/qixaxxJph+tVYdNGi9hNvL12T/5n
|
||||||
|
ihXkpMAK8MV6z3Y+ObiaKbCe4w19sLu2IIpff0U0mo6rTKOQwAfGa/N1dtzFaogP
|
||||||
|
f/iO5kcksWUPqZowM3lwXXgy8vg5ZeU7IZk9fRTBfrEJAr9TCQ8ivdluxq59Ax86
|
||||||
|
0AMmlbeu/dUMBcujLiTVjzqD3jz/Hr+iHq2y48NiF3j5oE/1qsD04d+QDWAygdmd
|
||||||
|
bQOy0w/W1X0ppnuPhLILQzcCAwEAAaNTMFEwHQYDVR0OBBYEFID88wvDJXrQyTsx
|
||||||
|
s+zl/wwx5BCMMB8GA1UdIwQYMBaAFID88wvDJXrQyTsxs+zl/wwx5BCMMA8GA1Ud
|
||||||
|
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIRp9bUxAip5s0dx700PPVAd
|
||||||
|
mrS7kDCZ+KFD6UgF/F3ykshh33MfYNLghJCfhcWvUHQgiPKohWcZq1g4oMuDZPFW
|
||||||
|
EHTr2wkX9j6A3KNjgFT5OVkLdjNPYdxMbTvmKbsJPc82C9AFN/Xz97XlZvmE4mKc
|
||||||
|
JCKqTz9hK3JpoayEUrf9g4TJcVwNnl/UnMp2sZX3aId4wD2+jSb40H/5UPFO2stv
|
||||||
|
SvCSdMcq0ZOQ/g/P56xOKV/5RAdIYV+0/3LWNGU/dH0nUfJO9K31e3eR+QZ1Iyn3
|
||||||
|
iGPcaSKPDptVx+2hxcvhFuRgRjfJ0mu6/hnK5wvhrXrSm43FBgvmlo4MaX0HVss=
|
||||||
|
-----END CERTIFICATE-----
|
28
test/helpers/test-key.pem
Normal file
28
test/helpers/test-key.pem
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCvRYr1DY18+avr
|
||||||
|
J6iiwp6NIt3GMyfi080n/sfmvxuwgP67YoCL0mBKlguPvzFzzIn6VP4iEbY+0c6D
|
||||||
|
yh+OLiYnu5kuZCkh7DJ4f6osWscSaYfrVWHTRovYTby9dk/+Z4oV5KTACvDFes92
|
||||||
|
Pjm4mimwnuMNfbC7tiCKX39FNJqOq0yjkMAHxmvzdXbcxWqID3/4juZHJLFlD6ma
|
||||||
|
MDN5cF14MvL4OWXlOyGZPX0UwX6xCQK/UwkPIr3ZbsaufQMfOtADJpW3rv3VDAXL
|
||||||
|
oy4k1Y86g948/x6/oh6tsuPDYhd4+aBP9arA9OHfkA1gMoHZnW0DstMP1tV9KaZ7
|
||||||
|
j4SyC0M3AgMBAAECggEAKfW6ng74C+7TtxDAAPMZtQ0fTcdKabWt/EC1B6tBzEAd
|
||||||
|
e6vJvW+IaOLB8tBhXOkfMSRu0KYv3Jsq1wcpBcdLkCCLu/zzkfDzZkCd809qMCC+
|
||||||
|
jtraeBOAADEgGbV80hlkh/g8btNPr99GUnb0J5sUlvl6vuyTxmSEJsxU8jL1O2km
|
||||||
|
YgK34fS5NS73h138P3UQAGC0dGK8Rt61EsFIKWTyH/r8tlz9nQrYcDG3LwTbFQQf
|
||||||
|
bsRLAjolxTRV6t1CzcjsSGtrAqm/4QNypP5McCyOXAqajb3pNGaJyGg1nAEOZclK
|
||||||
|
oagU7PPwaFmSquwo7Y1Uov72XuLJLVryBl0fOCen7QKBgQDieqvaL9gHsfaZKNoY
|
||||||
|
+0Cnul/Dw0kjuqJIKhar/mfLY7NwYmFSgH17r26g+X7mzuzaN0rnEhjh7L3j6xQJ
|
||||||
|
qhs9zL+/OIa581Ptvb8H/42O+mxnqx7Z8s5JwH0+f5EriNkU3euoAe/W9x4DqJiE
|
||||||
|
2VyvlM1gngxI+vFo+iewmg+vOwKBgQDGHiPKxXWD50tXvvDdRTjH+/4GQuXhEQjl
|
||||||
|
Po59AJ/PLc/AkQkVSzr8Fspf7MHN6vufr3tS45tBuf5Qf2Y9GPBRKR3e+M1CJdoi
|
||||||
|
1RXy0nMsnR0KujxgiIe6WQFumcT81AsIVXtDYk11Sa057tYPeeOmgtmUMJZb6lek
|
||||||
|
wqUxrFw0NQKBgQCs/p7+jsUpO5rt6vKNWn5MoGQ+GJFppUoIbX3b6vxFs+aA1eUZ
|
||||||
|
K+St8ZdDhtCUZUMufEXOs1gmWrvBuPMZXsJoNlnRKtBegat+Ug31ghMTP95GYcOz
|
||||||
|
H3DLjSkd8DtnUaTf95PmRXR6c1CN4t59u7q8s6EdSByCMozsbwiaMVQBuQKBgQCY
|
||||||
|
QxG/BYMLnPeKuHTlmg3JpSHWLhP+pdjwVuOrro8j61F/7ffNJcRvehSPJKbOW4qH
|
||||||
|
b5aYXdU07n1F4KPy0PfhaHhMpWsbK3w6yQnVVWivIRDw7bD5f/TQgxdWqVd7+HuC
|
||||||
|
LDBP2X0uZzF7FNPvkP4lOut9uNnWSoSRXAcZ5h33AQKBgQDWJYKGNoA8/IT9+e8n
|
||||||
|
v1Fy0RNL/SmBfGZW9pFGFT2pcu6TrzVSugQeWY/YFO2X6FqLPbL4p72Ar4rF0Uxl
|
||||||
|
31aYIjy3jDGzMabdIuW7mBogvtNjBG+0UgcLQzbdG6JkvTkQgqUjwIn/+Jo+0sS5
|
||||||
|
dEylNM0zC6zx1f1U1dGGZaNcLg==
|
||||||
|
-----END PRIVATE KEY-----
|
294
test/test.connection-forwarding.ts
Normal file
294
test/test.connection-forwarding.ts
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tapbundle';
|
||||||
|
import * as net from 'net';
|
||||||
|
import * as tls from 'tls';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { SmartProxy } from '../ts/proxies/smart-proxy/smart-proxy.js';
|
||||||
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
|
|
||||||
|
// Setup test infrastructure
|
||||||
|
const testCertPath = path.join(process.cwd(), 'test', 'helpers', 'test-cert.pem');
|
||||||
|
const testKeyPath = path.join(process.cwd(), 'test', 'helpers', 'test-key.pem');
|
||||||
|
|
||||||
|
let testServer: net.Server;
|
||||||
|
let tlsTestServer: tls.Server;
|
||||||
|
let smartProxy: SmartProxy;
|
||||||
|
|
||||||
|
tap.test('setup test servers', async () => {
|
||||||
|
// Create TCP test server
|
||||||
|
testServer = net.createServer((socket) => {
|
||||||
|
socket.write('Connected to TCP test server\n');
|
||||||
|
socket.on('data', (data) => {
|
||||||
|
socket.write(`TCP Echo: ${data}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
testServer.listen(7001, '127.0.0.1', () => {
|
||||||
|
console.log('TCP test server listening on port 7001');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create TLS test server for SNI testing
|
||||||
|
tlsTestServer = tls.createServer(
|
||||||
|
{
|
||||||
|
cert: fs.readFileSync(testCertPath),
|
||||||
|
key: fs.readFileSync(testKeyPath),
|
||||||
|
},
|
||||||
|
(socket) => {
|
||||||
|
socket.write('Connected to TLS test server\n');
|
||||||
|
socket.on('data', (data) => {
|
||||||
|
socket.write(`TLS Echo: ${data}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
tlsTestServer.listen(7002, '127.0.0.1', () => {
|
||||||
|
console.log('TLS test server listening on port 7002');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should forward TCP connections correctly', async () => {
|
||||||
|
// Create SmartProxy with forward route
|
||||||
|
smartProxy = new SmartProxy({
|
||||||
|
enableDetailedLogging: true,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: 'tcp-forward',
|
||||||
|
name: 'TCP Forward Route',
|
||||||
|
match: {
|
||||||
|
port: 8080,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 7001,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await smartProxy.start();
|
||||||
|
|
||||||
|
// Test TCP forwarding
|
||||||
|
const client = await new Promise<net.Socket>((resolve, reject) => {
|
||||||
|
const socket = net.connect(8080, '127.0.0.1', () => {
|
||||||
|
console.log('Connected to proxy');
|
||||||
|
resolve(socket);
|
||||||
|
});
|
||||||
|
socket.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test data transmission
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
client.on('data', (data) => {
|
||||||
|
const response = data.toString();
|
||||||
|
console.log('Received:', response);
|
||||||
|
expect(response).toContain('Connected to TCP test server');
|
||||||
|
client.end();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.write('Hello from client');
|
||||||
|
});
|
||||||
|
|
||||||
|
await smartProxy.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should handle TLS passthrough correctly', async () => {
|
||||||
|
// Create SmartProxy with TLS passthrough route
|
||||||
|
smartProxy = new SmartProxy({
|
||||||
|
enableDetailedLogging: true,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: 'tls-passthrough',
|
||||||
|
name: 'TLS Passthrough Route',
|
||||||
|
match: {
|
||||||
|
port: 8443,
|
||||||
|
domain: 'test.example.com',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
tls: {
|
||||||
|
mode: 'passthrough',
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 7002,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await smartProxy.start();
|
||||||
|
|
||||||
|
// Test TLS passthrough
|
||||||
|
const client = await new Promise<tls.TLSSocket>((resolve, reject) => {
|
||||||
|
const socket = tls.connect(
|
||||||
|
{
|
||||||
|
port: 8443,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
servername: 'test.example.com',
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
console.log('Connected via TLS');
|
||||||
|
resolve(socket);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
socket.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test data transmission over TLS
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
client.on('data', (data) => {
|
||||||
|
const response = data.toString();
|
||||||
|
console.log('TLS Received:', response);
|
||||||
|
expect(response).toContain('Connected to TLS test server');
|
||||||
|
client.end();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.write('Hello from TLS client');
|
||||||
|
});
|
||||||
|
|
||||||
|
await smartProxy.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should handle SNI-based forwarding', async () => {
|
||||||
|
// Create SmartProxy with multiple domain routes
|
||||||
|
smartProxy = new SmartProxy({
|
||||||
|
enableDetailedLogging: true,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: 'domain-a',
|
||||||
|
name: 'Domain A Route',
|
||||||
|
match: {
|
||||||
|
port: 8443,
|
||||||
|
domain: 'a.example.com',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
tls: {
|
||||||
|
mode: 'passthrough',
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 7002,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'domain-b',
|
||||||
|
name: 'Domain B Route',
|
||||||
|
match: {
|
||||||
|
port: 8443,
|
||||||
|
domain: 'b.example.com',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 7001,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await smartProxy.start();
|
||||||
|
|
||||||
|
// Test domain A (TLS passthrough)
|
||||||
|
const clientA = await new Promise<tls.TLSSocket>((resolve, reject) => {
|
||||||
|
const socket = tls.connect(
|
||||||
|
{
|
||||||
|
port: 8443,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
servername: 'a.example.com',
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
console.log('Connected to domain A');
|
||||||
|
resolve(socket);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
socket.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
clientA.on('data', (data) => {
|
||||||
|
const response = data.toString();
|
||||||
|
console.log('Domain A response:', response);
|
||||||
|
expect(response).toContain('Connected to TLS test server');
|
||||||
|
clientA.end();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
clientA.write('Hello from domain A');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test domain B (non-TLS forward)
|
||||||
|
const clientB = await new Promise<net.Socket>((resolve, reject) => {
|
||||||
|
const socket = net.connect(8443, '127.0.0.1', () => {
|
||||||
|
// Send TLS ClientHello with SNI for b.example.com
|
||||||
|
const clientHello = Buffer.from([
|
||||||
|
0x16, 0x03, 0x01, 0x00, 0x4e, // TLS Record header
|
||||||
|
0x01, 0x00, 0x00, 0x4a, // Handshake header
|
||||||
|
0x03, 0x03, // TLS version
|
||||||
|
// Random bytes
|
||||||
|
...Array(32).fill(0),
|
||||||
|
0x00, // Session ID length
|
||||||
|
0x00, 0x02, // Cipher suites length
|
||||||
|
0x00, 0x35, // Cipher suite
|
||||||
|
0x01, 0x00, // Compression methods
|
||||||
|
0x00, 0x1f, // Extensions length
|
||||||
|
0x00, 0x00, // SNI extension
|
||||||
|
0x00, 0x1b, // Extension length
|
||||||
|
0x00, 0x19, // SNI list length
|
||||||
|
0x00, // SNI type (hostname)
|
||||||
|
0x00, 0x16, // SNI length
|
||||||
|
// "b.example.com" in ASCII
|
||||||
|
0x62, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
|
||||||
|
]);
|
||||||
|
|
||||||
|
socket.write(clientHello);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(socket);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
socket.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
clientB.on('data', (data) => {
|
||||||
|
const response = data.toString();
|
||||||
|
console.log('Domain B response:', response);
|
||||||
|
// Should be forwarded to TCP server
|
||||||
|
expect(response).toContain('Connected to TCP test server');
|
||||||
|
clientB.end();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send regular data after initial handshake
|
||||||
|
setTimeout(() => {
|
||||||
|
clientB.write('Hello from domain B');
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
await smartProxy.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('cleanup', async () => {
|
||||||
|
testServer.close();
|
||||||
|
tlsTestServer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
105
test/test.forwarding-regression.ts
Normal file
105
test/test.forwarding-regression.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tapbundle';
|
||||||
|
import * as net from 'net';
|
||||||
|
import { SmartProxy } from '../ts/proxies/smart-proxy/smart-proxy.js';
|
||||||
|
|
||||||
|
// Test to verify port forwarding works correctly
|
||||||
|
tap.test('forward connections should not be immediately closed', async (t) => {
|
||||||
|
// Create a backend server that accepts connections
|
||||||
|
const testServer = net.createServer((socket) => {
|
||||||
|
console.log('Client connected to test server');
|
||||||
|
socket.write('Welcome from test server\n');
|
||||||
|
|
||||||
|
socket.on('data', (data) => {
|
||||||
|
console.log('Test server received:', data.toString());
|
||||||
|
socket.write(`Echo: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', (err) => {
|
||||||
|
console.error('Test server socket error:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen on a non-privileged port
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
testServer.listen(9090, '127.0.0.1', () => {
|
||||||
|
console.log('Test server listening on port 9090');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create SmartProxy with a forward route
|
||||||
|
const smartProxy = new SmartProxy({
|
||||||
|
enableDetailedLogging: true,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: 'forward-test',
|
||||||
|
name: 'Forward Test Route',
|
||||||
|
match: {
|
||||||
|
port: 8080,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 9090,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await smartProxy.start();
|
||||||
|
|
||||||
|
// Create a client connection through the proxy
|
||||||
|
const client = net.createConnection({
|
||||||
|
port: 8080,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
let connectionClosed = false;
|
||||||
|
let dataReceived = false;
|
||||||
|
let welcomeMessage = '';
|
||||||
|
|
||||||
|
client.on('connect', () => {
|
||||||
|
console.log('Client connected to proxy');
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('data', (data) => {
|
||||||
|
console.log('Client received:', data.toString());
|
||||||
|
dataReceived = true;
|
||||||
|
welcomeMessage = data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('close', () => {
|
||||||
|
console.log('Client connection closed');
|
||||||
|
connectionClosed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (err) => {
|
||||||
|
console.error('Client error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the welcome message
|
||||||
|
await t.waitForExpect(() => {
|
||||||
|
return dataReceived;
|
||||||
|
}, 'Data should be received from the server', 2000);
|
||||||
|
|
||||||
|
// Verify we got the welcome message
|
||||||
|
expect(welcomeMessage).toContain('Welcome from test server');
|
||||||
|
|
||||||
|
// Send some data
|
||||||
|
client.write('Hello from client');
|
||||||
|
|
||||||
|
// Wait a bit to make sure connection isn't immediately closed
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// Connection should still be open
|
||||||
|
expect(connectionClosed).toBe(false);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
client.end();
|
||||||
|
await smartProxy.stop();
|
||||||
|
testServer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
116
test/test.nftables-forwarding.ts
Normal file
116
test/test.nftables-forwarding.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tapbundle';
|
||||||
|
import * as net from 'net';
|
||||||
|
import { SmartProxy } from '../ts/proxies/smart-proxy/smart-proxy.js';
|
||||||
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
|
|
||||||
|
// Test to verify NFTables forwarding doesn't terminate connections
|
||||||
|
tap.test('NFTables forwarding should not terminate connections', async () => {
|
||||||
|
// Create a test server that receives connections
|
||||||
|
const testServer = net.createServer((socket) => {
|
||||||
|
socket.write('Connected to test server\n');
|
||||||
|
socket.on('data', (data) => {
|
||||||
|
socket.write(`Echo: ${data}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start test server
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
testServer.listen(8001, '127.0.0.1', () => {
|
||||||
|
console.log('Test server listening on port 8001');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create SmartProxy with NFTables route
|
||||||
|
const smartProxy = new SmartProxy({
|
||||||
|
enableDetailedLogging: true,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
id: 'nftables-test',
|
||||||
|
name: 'NFTables Test Route',
|
||||||
|
match: {
|
||||||
|
port: 8080,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
forwardingEngine: 'nftables',
|
||||||
|
target: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 8001,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Also add regular forwarding route for comparison
|
||||||
|
{
|
||||||
|
id: 'regular-test',
|
||||||
|
name: 'Regular Forward Route',
|
||||||
|
match: {
|
||||||
|
port: 8081,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 8001,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await smartProxy.start();
|
||||||
|
|
||||||
|
// Test NFTables route
|
||||||
|
const nftablesConnection = await new Promise<net.Socket>((resolve, reject) => {
|
||||||
|
const client = net.connect(8080, '127.0.0.1', () => {
|
||||||
|
console.log('Connected to NFTables route');
|
||||||
|
resolve(client);
|
||||||
|
});
|
||||||
|
client.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add timeout to check if connection stays alive
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
let dataReceived = false;
|
||||||
|
nftablesConnection.on('data', (data) => {
|
||||||
|
console.log('NFTables route data:', data.toString());
|
||||||
|
dataReceived = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send test data
|
||||||
|
nftablesConnection.write('Test NFTables');
|
||||||
|
|
||||||
|
// Check connection after 100ms
|
||||||
|
setTimeout(() => {
|
||||||
|
// Connection should still be alive even if app doesn't handle it
|
||||||
|
expect(nftablesConnection.destroyed).toBe(false);
|
||||||
|
nftablesConnection.end();
|
||||||
|
resolve();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test regular forwarding route for comparison
|
||||||
|
const regularConnection = await new Promise<net.Socket>((resolve, reject) => {
|
||||||
|
const client = net.connect(8081, '127.0.0.1', () => {
|
||||||
|
console.log('Connected to regular route');
|
||||||
|
resolve(client);
|
||||||
|
});
|
||||||
|
client.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test regular connection works
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
regularConnection.on('data', (data) => {
|
||||||
|
console.log('Regular route data:', data.toString());
|
||||||
|
expect(data.toString()).toContain('Connected to test server');
|
||||||
|
regularConnection.end();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await smartProxy.stop();
|
||||||
|
testServer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
86
test/test.port-forwarding-fix.ts
Normal file
86
test/test.port-forwarding-fix.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import * as net from 'net';
|
||||||
|
import { SmartProxy } from '../ts/proxies/smart-proxy/smart-proxy.js';
|
||||||
|
|
||||||
|
let echoServer: net.Server;
|
||||||
|
let proxy: SmartProxy;
|
||||||
|
|
||||||
|
tap.test('port forwarding should not immediately close connections', async () => {
|
||||||
|
// Create an echo server
|
||||||
|
echoServer = await new Promise<net.Server>((resolve) => {
|
||||||
|
const server = net.createServer((socket) => {
|
||||||
|
socket.on('data', (data) => {
|
||||||
|
socket.write(`ECHO: ${data}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(8888, () => {
|
||||||
|
console.log('Echo server listening on port 8888');
|
||||||
|
resolve(server);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create proxy with forwarding route
|
||||||
|
proxy = new SmartProxy({
|
||||||
|
routes: [{
|
||||||
|
id: 'test',
|
||||||
|
match: { ports: 9999 },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: { host: 'localhost', port: 8888 }
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
await proxy.start();
|
||||||
|
|
||||||
|
// Test connection through proxy
|
||||||
|
const client = net.createConnection(9999, 'localhost');
|
||||||
|
|
||||||
|
const result = await new Promise<string>((resolve, reject) => {
|
||||||
|
client.on('data', (data) => {
|
||||||
|
resolve(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', reject);
|
||||||
|
|
||||||
|
client.write('Hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual('ECHO: Hello');
|
||||||
|
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('TLS passthrough should work correctly', async () => {
|
||||||
|
// Create proxy with TLS passthrough
|
||||||
|
proxy = new SmartProxy({
|
||||||
|
routes: [{
|
||||||
|
id: 'tls-test',
|
||||||
|
match: { ports: 8443, domains: 'test.example.com' },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
tls: { mode: 'passthrough' },
|
||||||
|
target: { host: 'localhost', port: 443 }
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
await proxy.start();
|
||||||
|
|
||||||
|
// For now just verify the proxy starts correctly with TLS passthrough route
|
||||||
|
expect(proxy).toBeDefined();
|
||||||
|
|
||||||
|
await proxy.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('cleanup', async () => {
|
||||||
|
if (echoServer) {
|
||||||
|
echoServer.close();
|
||||||
|
}
|
||||||
|
if (proxy) {
|
||||||
|
await proxy.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '19.3.3',
|
version: '19.3.6',
|
||||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
@ -339,21 +339,6 @@ export class RouteConnectionHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this route uses NFTables for forwarding
|
|
||||||
if (route.action.forwardingEngine === 'nftables') {
|
|
||||||
// For NFTables routes, we don't need to do anything at the application level
|
|
||||||
// The packet is forwarded at the kernel level
|
|
||||||
|
|
||||||
// Log the connection
|
|
||||||
console.log(
|
|
||||||
`[${connectionId}] Connection forwarded by NFTables: ${record.remoteIP} -> port ${record.localPort}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Just close the socket in our application since it's handled at kernel level
|
|
||||||
socket.end();
|
|
||||||
this.connectionManager.cleanupConnection(record, 'nftables_handled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the route based on its action type
|
// Handle the route based on its action type
|
||||||
switch (route.action.type) {
|
switch (route.action.type) {
|
||||||
@ -391,10 +376,13 @@ export class RouteConnectionHandler {
|
|||||||
|
|
||||||
// Check if this route uses NFTables for forwarding
|
// Check if this route uses NFTables for forwarding
|
||||||
if (action.forwardingEngine === 'nftables') {
|
if (action.forwardingEngine === 'nftables') {
|
||||||
// Log detailed information about NFTables-handled connection
|
// NFTables handles packet forwarding at the kernel level
|
||||||
|
// The application should NOT interfere with these connections
|
||||||
|
|
||||||
|
// Just log the connection for monitoring purposes
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${record.id}] Connection forwarded by NFTables (kernel-level): ` +
|
`[${record.id}] NFTables forwarding (kernel-level): ` +
|
||||||
`${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` +
|
`${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` +
|
||||||
` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})`
|
` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})`
|
||||||
);
|
);
|
||||||
@ -420,14 +408,8 @@ export class RouteConnectionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This connection is handled at the kernel level, no need to process at application level
|
// For NFTables routes, continue processing the connection normally
|
||||||
// Close the socket gracefully in our application layer
|
// since the packet forwarding happens transparently at the kernel level
|
||||||
socket.end();
|
|
||||||
|
|
||||||
// Mark the connection as handled by NFTables for proper cleanup
|
|
||||||
record.nftablesHandled = true;
|
|
||||||
this.connectionManager.initiateCleanupOnce(record, 'nftables_handled');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should have a target configuration for forwarding
|
// We should have a target configuration for forwarding
|
||||||
|
Reference in New Issue
Block a user