195 lines
7.9 KiB
Markdown
195 lines
7.9 KiB
Markdown
# SmartProxy Project Hints
|
||
|
||
## Project Overview
|
||
- Package: `@push.rocks/smartproxy` – high-performance proxy supporting HTTP(S), TCP, WebSocket, and ACME integration.
|
||
- Written in TypeScript, compiled output in `dist_ts/`, uses ESM with NodeNext resolution.
|
||
|
||
## Important: ACME Configuration in v19.0.0
|
||
- **Breaking Change**: ACME configuration must be placed within individual route TLS settings, not at the top level
|
||
- Route-level ACME config is the ONLY way to enable SmartAcme initialization
|
||
- SmartCertManager requires email in route config for certificate acquisition
|
||
- Top-level ACME configuration is ignored in v19.0.0
|
||
|
||
## Repository Structure
|
||
- `ts/` – TypeScript source files:
|
||
- `index.ts` exports main modules.
|
||
- `plugins.ts` centralizes native and third-party imports.
|
||
- Subdirectories: `networkproxy/`, `nftablesproxy/`, `port80handler/`, `redirect/`, `smartproxy/`.
|
||
- Key classes: `ProxyRouter` (`classes.router.ts`), `SmartProxy` (`classes.smartproxy.ts`), plus handlers/managers.
|
||
- `dist_ts/` – transpiled `.js` and `.d.ts` files mirroring `ts/` structure.
|
||
- `test/` – test suites in TypeScript:
|
||
- `test.router.ts` – routing logic (hostname matching, wildcards, path parameters, config management).
|
||
- `test.smartproxy.ts` – proxy behavior tests (TCP forwarding, SNI handling, concurrency, chaining, timeouts).
|
||
- `test/helpers/` – utilities (e.g., certificates).
|
||
- `assets/certs/` – placeholder certificates for ACME and TLS.
|
||
|
||
## Development Setup
|
||
- Requires `pnpm` (v10+).
|
||
- Install dependencies: `pnpm install`.
|
||
- Build: `pnpm build` (runs `tsbuild --web --allowimplicitany`).
|
||
- Test: `pnpm test` (runs `tstest test/`).
|
||
- Format: `pnpm format` (runs `gitzone format`).
|
||
|
||
## Testing Framework
|
||
- Uses `@push.rocks/tapbundle` (`tap`, `expect`, `expactAsync`).
|
||
- Test files: must start with `test.` and use `.ts` extension.
|
||
- Run specific tests via `tsx`, e.g., `tsx test/test.router.ts`.
|
||
|
||
## Coding Conventions
|
||
- Import modules via `plugins.ts`:
|
||
```ts
|
||
import * as plugins from './plugins.ts';
|
||
const server = new plugins.http.Server();
|
||
```
|
||
- Reference plugins with full path: `plugins.acme`, `plugins.smartdelay`, `plugins.minimatch`, etc.
|
||
- Path patterns support globs (`*`) and parameters (`:param`) in `ProxyRouter`.
|
||
- Wildcard hostname matching leverages `minimatch` patterns.
|
||
|
||
## Key Components
|
||
- **ProxyRouter**
|
||
- Methods: `routeReq`, `routeReqWithDetails`.
|
||
- Hostname matching: case-insensitive, strips port, supports exact, wildcard, TLD, complex patterns.
|
||
- Path routing: exact, wildcard, parameter extraction (`pathParams`), returns `pathMatch` and `pathRemainder`.
|
||
- Config API: `setNewProxyConfigs`, `addProxyConfig`, `removeProxyConfig`, `getHostnames`, `getProxyConfigs`.
|
||
- **SmartProxy**
|
||
- Manages one or more `net.Server` instances to forward TCP streams.
|
||
- Options: `preserveSourceIP`, `defaultAllowedIPs`, `globalPortRanges`, `sniEnabled`.
|
||
- DomainConfigManager: round-robin selection for multiple target IPs.
|
||
- Graceful shutdown in `stop()`, ensures no lingering servers or sockets.
|
||
|
||
## Notable Points
|
||
- **TSConfig**: `module: NodeNext`, `verbatimModuleSyntax`, allows `.js` extension imports in TS.
|
||
- Mermaid diagrams and architecture flows in `readme.md` illustrate component interactions and protocol flows.
|
||
- CLI entrypoint (`cli.js`) supports command-line usage (ACME, proxy controls).
|
||
- ACME and certificate handling via `Port80Handler` and `helpers.certificates.ts`.
|
||
|
||
## ACME/Certificate Configuration Example (v19.0.0)
|
||
```typescript
|
||
const proxy = new SmartProxy({
|
||
routes: [{
|
||
name: 'example.com',
|
||
match: { domains: 'example.com', ports: 443 },
|
||
action: {
|
||
type: 'forward',
|
||
target: { host: 'localhost', port: 8080 },
|
||
tls: {
|
||
mode: 'terminate',
|
||
certificate: 'auto',
|
||
acme: { // ACME config MUST be here, not at top level
|
||
email: 'ssl@example.com',
|
||
useProduction: false,
|
||
challengePort: 80
|
||
}
|
||
}
|
||
}
|
||
}]
|
||
});
|
||
```
|
||
|
||
## TODOs / Considerations
|
||
- Ensure import extensions in source match build outputs (`.ts` vs `.js`).
|
||
- Update `plugins.ts` when adding new dependencies.
|
||
- Maintain test coverage for new routing or proxy features.
|
||
- Keep `ts/` and `dist_ts/` in sync after refactors.
|
||
- Consider implementing top-level ACME config support for backward compatibility
|
||
|
||
## HTTP-01 ACME Challenge Fix (v19.3.8)
|
||
|
||
### Issue
|
||
Non-TLS connections on ports configured in `useHttpProxy` were not being forwarded to HttpProxy. This caused ACME HTTP-01 challenges to fail when the ACME port (usually 80) was included in `useHttpProxy`.
|
||
|
||
### Root Cause
|
||
In the `RouteConnectionHandler.handleForwardAction` method, only connections with TLS settings (mode: 'terminate' or 'terminate-and-reencrypt') were being forwarded to HttpProxy. Non-TLS connections were always handled as direct connections, even when the port was configured for HttpProxy.
|
||
|
||
### Solution
|
||
Added a check for non-TLS connections on ports listed in `useHttpProxy`:
|
||
```typescript
|
||
// No TLS settings - check if this port should use HttpProxy
|
||
const isHttpProxyPort = this.settings.useHttpProxy?.includes(record.localPort);
|
||
|
||
if (isHttpProxyPort && this.httpProxyBridge.getHttpProxy()) {
|
||
// Forward non-TLS connections to HttpProxy if configured
|
||
this.httpProxyBridge.forwardToHttpProxy(/*...*/);
|
||
return;
|
||
}
|
||
```
|
||
|
||
### Test Coverage
|
||
- `test/test.http-fix-unit.ts` - Unit tests verifying the fix
|
||
- Tests confirm that non-TLS connections on HttpProxy ports are properly forwarded
|
||
- Tests verify that non-HttpProxy ports still use direct connections
|
||
|
||
### Configuration Example
|
||
```typescript
|
||
const proxy = new SmartProxy({
|
||
useHttpProxy: [80], // Enable HttpProxy for port 80
|
||
httpProxyPort: 8443,
|
||
acme: {
|
||
email: 'ssl@example.com',
|
||
port: 80
|
||
},
|
||
routes: [
|
||
// Your routes here
|
||
]
|
||
});
|
||
```
|
||
|
||
## ACME Certificate Provisioning Timing Fix (v19.3.9)
|
||
|
||
### Issue
|
||
Certificate provisioning would start before ports were listening, causing ACME HTTP-01 challenges to fail with connection refused errors.
|
||
|
||
### Root Cause
|
||
SmartProxy initialization sequence:
|
||
1. Certificate manager initialized → immediately starts provisioning
|
||
2. Ports start listening (too late for ACME challenges)
|
||
|
||
### Solution
|
||
Deferred certificate provisioning until after ports are ready:
|
||
```typescript
|
||
// SmartCertManager.initialize() now skips automatic provisioning
|
||
// SmartProxy.start() calls provisionAllCertificates() directly after ports are listening
|
||
```
|
||
|
||
### Test Coverage
|
||
- `test/test.acme-timing-simple.ts` - Verifies proper timing sequence
|
||
|
||
### Migration
|
||
Update to v19.3.9+, no configuration changes needed.
|
||
|
||
## Socket Handler Race Condition Fix (v19.5.0)
|
||
|
||
### Issue
|
||
Initial data chunks were being emitted before async socket handlers had completed setup, causing data loss when handlers performed async operations before setting up data listeners.
|
||
|
||
### Root Cause
|
||
The `handleSocketHandlerAction` method was using `process.nextTick` to emit initial chunks regardless of whether the handler was sync or async. This created a race condition where async handlers might not have their listeners ready when the initial data was emitted.
|
||
|
||
### Solution
|
||
Differentiated between sync and async handlers:
|
||
```typescript
|
||
const result = route.action.socketHandler(socket);
|
||
|
||
if (result instanceof Promise) {
|
||
// Async handler - wait for completion before emitting initial data
|
||
result.then(() => {
|
||
if (initialChunk && initialChunk.length > 0) {
|
||
socket.emit('data', initialChunk);
|
||
}
|
||
}).catch(/*...*/);
|
||
} else {
|
||
// Sync handler - use process.nextTick as before
|
||
if (initialChunk && initialChunk.length > 0) {
|
||
process.nextTick(() => {
|
||
socket.emit('data', initialChunk);
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### Test Coverage
|
||
- `test/test.socket-handler-race.ts` - Specifically tests async handlers with delayed listener setup
|
||
- Verifies that initial data is received even when handler sets up listeners after async work
|
||
|
||
### Usage Note
|
||
Socket handlers require initial data from the client to trigger routing (not just a TLS handshake). Clients must send at least one byte of data for the handler to be invoked. |