# @push.rocks/smartproxy 🚀

**A high-performance, Rust-powered proxy toolkit for Node.js** — unified route-based configuration for SSL/TLS termination, HTTP/HTTPS reverse proxying, WebSocket support, UDP/QUIC/HTTP3, load balancing, custom protocol handlers, and kernel-level NFTables forwarding via [`@push.rocks/smartnftables`](https://code.foss.global/push.rocks/smartnftables).

## Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.

## 📦 Installation

```bash
pnpm add @push.rocks/smartproxy @push.rocks/smartchallenge
```

`@push.rocks/smartchallenge` is only required when you use the route challenge example below.

## 🎯 What is SmartProxy?

SmartProxy is a production-ready proxy solution that takes the complexity out of traffic management. Under the hood, all networking — TCP, UDP, TLS, HTTP reverse proxy, QUIC/HTTP3, connection tracking, security enforcement, and NFTables — is handled by a **Rust engine** for maximum performance, while you configure everything through a clean TypeScript API with full type safety.

Whether you're building microservices, deploying edge infrastructure, proxying UDP-based protocols, or need a battle-tested reverse proxy with automatic Let's Encrypt certificates, SmartProxy has you covered.

### ⚡ Key Features

| Feature | Description |
|---------|-------------|
| 🦀 **Rust-Powered Engine** | All networking handled by a high-performance Rust binary via IPC |
| 🔀 **Unified Route-Based Config** | Clean match/action patterns for intuitive traffic routing |
| 🔒 **Automatic SSL/TLS** | Zero-config HTTPS with Let's Encrypt ACME integration |
| 🎯 **Flexible Matching** | Route by port, domain, path, protocol, client IP, TLS version, headers, or custom logic |
| 🚄 **High-Performance** | Choose between user-space or kernel-level (NFTables) forwarding |
| 📡 **UDP & QUIC/HTTP3** | First-class UDP transport, datagram handlers, QUIC tunneling, and HTTP/3 support |
| ⚖️ **Load Balancing** | Round-robin, least-connections, and IP-hash selection across host arrays |
| 🛡️ **Enterprise Security** | IP filtering, rate limiting, basic auth, JWT auth, connection limits |
| 🔌 **WebSocket Support** | First-class WebSocket proxying with ping/pong keep-alive |
| 🎮 **Custom Protocols** | Socket and datagram handlers for implementing any protocol in TypeScript |
| 📊 **Live Metrics** | Real-time throughput, connection counts, UDP sessions, and performance data |
| 🔧 **Dynamic Management** | Add/remove ports and routes at runtime without restarts |
| 🔄 **PROXY Protocol** | Full PROXY protocol v1/v2 support for preserving client information |
| 💾 **Consumer Cert Storage** | Bring your own persistence — SmartProxy never writes certs to disk |

## 🚀 Quick Start

Get up and running in 30 seconds:

```typescript
import { SmartProxy, SocketHandlers } from '@push.rocks/smartproxy';

// Create a proxy with automatic HTTPS
const proxy = new SmartProxy({
  acme: {
    email: 'ssl@yourdomain.com',
    useProduction: true
  },
  routes: [
    // HTTPS route with automatic Let's Encrypt cert
    {
      name: 'https-app',
      match: { ports: 443, domains: 'app.example.com' },
      action: {
        type: 'forward',
        targets: [{ host: 'localhost', port: 3000 }],
        tls: { mode: 'terminate', certificate: 'auto' }
      }
    },
    // HTTP → HTTPS redirect
    {
      name: 'http-redirect',
      match: { ports: 80, domains: 'app.example.com' },
      action: {
        type: 'socket-handler',
        socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301)
      }
    }
  ]
});

await proxy.start();
console.log('🚀 Proxy running with automatic HTTPS!');
```

## 📚 Core Concepts

### 🏗️ Route-Based Architecture

SmartProxy uses a powerful **match/action** pattern that makes routing predictable and maintainable:

```typescript
{
  name: 'api-route',
  match: {
    ports: 443,
    domains: 'api.example.com',
    path: '/v1/*'
  },
  action: {
    type: 'forward',
    targets: [{ host: 'backend', port: 8080 }],
    tls: { mode: 'terminate', certificate: 'auto' }
  }
}
```

Every route consists of:
- **Match** — What traffic to capture (ports, domains, paths, transport, protocol, IPs, headers)
- **Action** — What to do with it (`forward` or `socket-handler`)
- **Security** (optional) — IP allow/block lists, rate limits, authentication
- **Headers** (optional) — Request/response header manipulation with template variables
- **Name/Priority** (optional) — For identification and ordering

### 🔄 TLS Modes

SmartProxy supports three TLS handling modes:

| Mode | Description | Use Case |
|------|-------------|----------|
| `passthrough` | Forward encrypted traffic as-is (SNI-based routing) | Backend handles TLS |
| `terminate` | Decrypt at proxy, forward plain HTTP to backend | Standard reverse proxy |
| `terminate-and-reencrypt` | Decrypt at proxy, re-encrypt to backend. HTTP traffic gets full per-request routing (Host header, path matching) via the HTTP proxy; non-HTTP traffic uses a raw TLS-to-TLS tunnel | Zero-trust / defense-in-depth environments |

## 💡 Common Use Cases

### 🌐 HTTP to HTTPS Redirect

```typescript
import { SmartProxy, SocketHandlers } from '@push.rocks/smartproxy';

const proxy = new SmartProxy({
  routes: [{
    name: 'http-to-https',
    match: { ports: 80, domains: ['example.com', '*.example.com'] },
    action: {
      type: 'socket-handler',
      socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301)
    }
  }]
});
```

### ⚖️ Load Balancer

For equivalent backends, put the backend hosts into one target's `host` array and choose a target-level load-balancing algorithm. Multiple `targets` are for sub-routing with `target.match` and `priority`.

```typescript
import { SmartProxy } from '@push.rocks/smartproxy';

const proxy = new SmartProxy({
  routes: [{
    name: 'load-balancer',
    match: { ports: 443, domains: 'app.example.com' },
    action: {
      type: 'forward',
      targets: [{
        host: ['server1.internal', 'server2.internal', 'server3.internal'],
        port: 8080,
        loadBalancing: { algorithm: 'round-robin' }
      }],
      tls: { mode: 'terminate', certificate: 'auto' }
    }
  }]
});
```

### 🔌 WebSocket Proxy

```typescript
import { SmartProxy } from '@push.rocks/smartproxy';

const proxy = new SmartProxy({
  routes: [{
    name: 'websocket',
    match: { ports: 443, domains: 'ws.example.com', path: '/socket' },
    priority: 100,
    action: {
      type: 'forward',
      targets: [{ host: 'websocket-server', port: 8080 }],
      tls: { mode: 'terminate', certificate: 'auto' },
      websocket: {
        enabled: true,
        pingInterval: 30000,
        pingTimeout: 10000
      }
    }
  }]
});
```

### 🚦 API Gateway with Rate Limiting

```typescript
import { SmartProxy } from '@push.rocks/smartproxy';

const proxy = new SmartProxy({
  routes: [{
    name: 'api-gateway',
    match: { ports: 443, domains: 'api.example.com', path: '/api/*' },
    priority: 100,
    action: {
      type: 'forward',
      targets: [{ host: 'api-backend', port: 8080 }],
      tls: { mode: 'terminate', certificate: 'auto' }
    },
    headers: {
      response: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        'Access-Control-Max-Age': '86400'
      }
    },
    security: {
      rateLimit: {
        enabled: true,
        maxRequests: 100,
        window: 60,
        keyBy: 'ip',
        onExceeded: { type: '429' }
      }
    }
  }]
});
```

Set `rateLimit.onExceeded.type` to `challenge` when browser traffic should complete a challenge instead of immediately receiving `429` after the limit is exceeded:

```typescript
{
  security: {
    rateLimit: {
      enabled: true,
      maxRequests: 120,
      window: 60,
      keyBy: 'ip',
      onExceeded: {
        type: 'challenge',
        challenge: {
          providerId: 'smartchallenge',
          challengeType: 'wait',
          applyTo: { methods: ['GET'], browserNavigationsOnly: true },
          clearance: { ttlSeconds: 300, bindToHost: true, bindToRoute: true },
        },
        clearanceEffect: 'bypass-rate-limit',
      },
    },
  },
}
```

### 🎮 Custom Protocol Handler (TCP)

SmartProxy lets you implement any protocol with full socket control. Routes with JavaScript socket handlers are automatically relayed from the Rust engine back to your TypeScript code:

```typescript
import { SmartProxy, SocketHandlers } from '@push.rocks/smartproxy';

const proxy = new SmartProxy({
  routes: [
    // Use pre-built handlers
    {
      name: 'echo-server',
      match: { ports: 7777, domains: 'echo.example.com' },
      action: { type: 'socket-handler', socketHandler: SocketHandlers.echo }
    },
    // Or create your own custom protocol
    {
      name: 'custom-protocol',
      match: { ports: 9999, domains: 'custom.example.com' },
      action: {
        type: 'socket-handler',
        socketHandler: async (socket) => {
          console.log(`New connection on custom protocol`);
          socket.write('Welcome to my custom protocol!\n');

          socket.on('data', (data) => {
            const command = data.toString().trim();
            switch (command) {
              case 'PING': socket.write('PONG\n'); break;
              case 'TIME': socket.write(`${new Date().toISOString()}\n`); break;
              case 'QUIT': socket.end('Goodbye!\n'); break;
              default: socket.write(`Unknown: ${command}\n`);
            }
          });
        }
      }
    }
  ]
});
```

**Pre-built Socket Handlers:**

| Handler | Description |
|---------|-------------|
| `SocketHandlers.echo` | Echo server — returns everything sent |
| `SocketHandlers.proxy(host, port)` | TCP proxy to another server |
| `SocketHandlers.lineProtocol(handler)` | Line-based text protocol |
| `SocketHandlers.httpResponse(code, body)` | Simple HTTP response |
| `SocketHandlers.httpRedirect(url, code)` | HTTP redirect with template variables (`{domain}`, `{path}`, `{port}`, `{clientIp}`) |
| `SocketHandlers.httpServer(handler)` | Full HTTP request/response handling |
| `SocketHandlers.httpBlock(status, message)` | HTTP block response |
| `SocketHandlers.block(message)` | Block with optional message |

### 📡 UDP Datagram Handler

Handle raw UDP datagrams with custom TypeScript logic — perfect for DNS, game servers, IoT protocols, or any UDP-based service:

```typescript
import { SmartProxy } from '@push.rocks/smartproxy';
import type { IRouteConfig, TDatagramHandler, IDatagramInfo } from '@push.rocks/smartproxy';

// Custom UDP echo handler
const udpHandler: TDatagramHandler = (datagram, info, reply) => {
  console.log(`UDP from ${info.sourceIp}:${info.sourcePort} on port ${info.destPort}`);
  reply(datagram); // Echo it back
};

const proxy = new SmartProxy({
  routes: [{
    name: 'udp-echo',
    match: {
      ports: 5353,
      transport: 'udp'        // 👈 Listen for UDP datagrams
    },
    action: {
      type: 'socket-handler',
      datagramHandler: udpHandler,  // 👈 Process each datagram
      udp: {
        sessionTimeout: 60000,     // Session idle timeout (ms)
        maxSessionsPerIP: 100,
        maxDatagramSize: 65535
      }
    }
  }]
});

await proxy.start();
```

### 📡 QUIC / HTTP3 Forwarding

Forward QUIC traffic to backends with optional protocol translation (e.g., receive QUIC, forward as TCP/HTTP1):

```typescript
import { SmartProxy } from '@push.rocks/smartproxy';
import type { IRouteConfig } from '@push.rocks/smartproxy';

const quicRoute: IRouteConfig = {
  name: 'quic-to-backend',
  match: {
    ports: 443,
    transport: 'udp',
    protocol: 'quic'           // 👈 Match QUIC protocol
  },
  action: {
    type: 'forward',
    targets: [{
      host: 'backend-server',
      port: 8443,
      backendTransport: 'tcp'  // 👈 Translate QUIC → TCP for backend
    }],
    tls: {
      mode: 'terminate',
      certificate: 'auto'     // 👈 QUIC requires TLS 1.3
    },
    udp: {
      quic: {
        enableHttp3: true,
        maxIdleTimeout: 30000,
        maxConcurrentBidiStreams: 100,
        altSvcPort: 443,       // Advertise in Alt-Svc header
        altSvcMaxAge: 86400
      }
    }
  }
};

const proxy = new SmartProxy({
  acme: { email: 'ssl@example.com' },
  routes: [quicRoute]
});
```

### 🚄 Best-Effort Backend Protocol (H3 > H2 > H1)

SmartProxy automatically uses the **highest protocol your backend supports** for HTTP requests. The backend protocol is independent of the client protocol — a client using HTTP/1.1 can be forwarded over HTTP/3 to the backend, and vice versa.

```typescript
const route: IRouteConfig = {
  name: 'auto-protocol',
  match: { ports: 443, domains: 'app.example.com' },
  action: {
    type: 'forward',
    targets: [{ host: 'backend', port: 8443 }],
    tls: { mode: 'terminate', certificate: 'auto' },
    options: {
      backendProtocol: 'auto'  // 👈 Default — best-effort selection
    }
  }
};
```

**How protocol discovery works (browser model):**

1. First request → TLS ALPN probe detects H2 or H1
2. Backend response inspected for `Alt-Svc: h3=":port"` header
3. If H3 advertised → cached and used for subsequent requests via QUIC
4. Graceful fallback: H3 failure → H2 → H1 with automatic cache invalidation

| `backendProtocol` | Behavior |
|---|---|
| `'auto'` (default) | Best-effort: H3 > H2 > H1 with Alt-Svc discovery |
| `'http1'` | Always HTTP/1.1 |
| `'http2'` | Always HTTP/2 (hard-fail if unsupported) |
| `'http3'` | Always HTTP/3 via QUIC (hard-fail if unsupported) |

> **Note:** WebSocket upgrades always use HTTP/1.1 to the backend regardless of `backendProtocol`, since there's no performance benefit from H2/H3 Extended CONNECT for tunneled connections, and backend support is rare.

### 🔁 Dual-Stack TCP + UDP Route

Listen on both TCP and UDP with a single route — handle each transport with its own handler:

```typescript
const dualStackRoute: IRouteConfig = {
  name: 'dual-stack-dns',
  match: {
    ports: 53,
    transport: 'all'  // 👈 Listen on both TCP and UDP
  },
  action: {
    type: 'socket-handler',
    socketHandler: handleTcpDns,      // 👈 TCP connections
    datagramHandler: handleUdpDns,    // 👈 UDP datagrams
  }
};
```

### ⚡ High-Performance NFTables Forwarding

For ultra-low latency on Linux, use kernel-level forwarding via [`@push.rocks/smartnftables`](https://code.foss.global/push.rocks/smartnftables) (requires root):

```typescript
import { SmartProxy } from '@push.rocks/smartproxy';

const proxy = new SmartProxy({
  routes: [{
    name: 'nftables-fast',
    match: { ports: 443, domains: 'fast.example.com' },
    action: {
      type: 'forward',
      forwardingEngine: 'nftables',
      targets: [{ host: 'backend', port: 8080 }],
      tls: { mode: 'terminate', certificate: 'auto' },
      nftables: {
        protocol: 'tcp',
        preserveSourceIP: true  // Backend sees real client IP
      }
    }
  }]
});
```

### 🔒 SNI Passthrough (TLS Passthrough)

Forward encrypted traffic to backends without terminating TLS — the proxy routes based on the SNI hostname alone:

```typescript
import { SmartProxy } from '@push.rocks/smartproxy';

const proxy = new SmartProxy({
  routes: [{
    name: 'sni-passthrough',
    match: { ports: 443, domains: 'secure.example.com' },
    action: {
      type: 'forward',
      targets: [{ host: 'backend-that-handles-tls', port: 8443 }],
      tls: { mode: 'passthrough' }
    }
  }]
});
```

## 🔧 Advanced Features

### 🎯 Dynamic Routing

Route traffic based on runtime conditions using function-based host/port resolution:

```typescript
const proxy = new SmartProxy({
  routes: [{
    name: 'dynamic-backend',
    match: {
      ports: 443,
      domains: 'app.example.com'
    },
    action: {
      type: 'forward',
      targets: [{
        host: (context) => {
          return context.path?.startsWith('/premium')
            ? 'premium-backend'
            : 'standard-backend';
        },
        port: 8080
      }],
      tls: { mode: 'terminate', certificate: 'auto' }
    }
  }]
});
```

> **Note:** Routes with dynamic functions (host/port callbacks) are automatically relayed through the TypeScript socket handler server, since JavaScript functions can't be serialized to Rust.

### 🔀 Protocol-Specific Routing

Restrict routes to specific application-layer protocols. When `protocol` is set, the Rust engine detects the protocol after connection (or after TLS termination) and only matches routes that accept that protocol:

```typescript
// HTTP-only route (rejects raw TCP connections)
const httpOnlyRoute: IRouteConfig = {
  name: 'http-api',
  match: {
    ports: 443,
    domains: 'api.example.com',
    protocol: 'http',   // Only match HTTP/1.1, HTTP/2, and WebSocket upgrades
  },
  action: {
    type: 'forward',
    targets: [{ host: 'api-backend', port: 8080 }],
    tls: { mode: 'terminate', certificate: 'auto' }
  }
};

// Raw TCP route (rejects HTTP traffic)
const tcpOnlyRoute: IRouteConfig = {
  name: 'database-proxy',
  match: {
    ports: 5432,
    protocol: 'tcp',    // Only match non-HTTP TCP streams
  },
  action: {
    type: 'forward',
    targets: [{ host: 'db-server', port: 5432 }]
  }
};
```

> **Note:** Omitting `protocol` (the default) matches any protocol. For TLS routes, protocol detection happens *after* TLS termination — during the initial SNI-based route match, `protocol` is not yet known and the route is allowed to match. The protocol restriction is enforced after the proxy peeks at the decrypted data.

### 🔒 Security Controls

Comprehensive per-route security options:

```typescript
{
  name: 'secure-api',
  match: { ports: 443, domains: 'api.example.com' },
  action: {
    type: 'forward',
    targets: [{ host: 'api-backend', port: 8080 }],
    tls: { mode: 'terminate', certificate: 'auto' }
  },
  security: {
    // IP-based access control
    ipAllowList: ['10.0.0.0/8', '192.168.*'],
    ipBlockList: ['192.168.1.100'],

    // Connection limits
    maxConnections: 1000,

    // Rate limiting
    rateLimit: {
      enabled: true,
      maxRequests: 100,
      window: 60,
      keyBy: 'ip',
      onExceeded: { type: '429' }
    },

    // Authentication
    basicAuth: { users: [{ username: 'admin', password: 'secret' }] },
    jwtAuth: { secret: 'your-jwt-secret', algorithm: 'HS256' }
  }
}
```

Security options are configured directly on each route's `security` property — no separate helpers needed.

### 🛡️ Route Challenge Enforcement

SmartProxy can enforce browser-facing challenges through providers from `@push.rocks/smartchallenge`. Route configs store challenge intent only: `providerId`, `challengeType`, `policyRef`, `settings`, `applyTo`, and `clearance`. Runtime wiring such as provider endpoints, socket paths, ports, deployment IDs, secrets, API tokens, and credentials must stay outside `security.challenge`.

```typescript
import { SmartProxy } from '@push.rocks/smartproxy';
import { SmartChallengeProvider, WaitChallengeType } from '@push.rocks/smartchallenge';

const provider = new SmartChallengeProvider({
  challengeTypes: [new WaitChallengeType()],
});

const proxy = new SmartProxy({
  routes: [{
    name: 'wait-protected-app',
    match: {
      ports: 443,
      domains: 'app.example.com',
      protocol: 'http',
    },
    action: {
      type: 'forward',
      targets: [{ host: 'app-backend', port: 8080 }],
      tls: { mode: 'terminate', certificate: 'auto' },
    },
    security: {
      challenge: {
        providerId: 'smartchallenge',
        challengeType: 'wait',
        settings: { waitSeconds: 5 },
        applyTo: { methods: ['GET'], browserNavigationsOnly: true },
        clearance: { ttlSeconds: 1800, bindToHost: true, bindToRoute: true },
      },
    },
  }],
  challenge: {
    pendingTtlSeconds: 300,
    relayTimeoutMs: 5000,
  },
});

proxy.registerChallengeProvider('smartchallenge', provider);
await proxy.start();
```

Challenge flow: the first matching protected request calls the provider `assess` operation, renders a provider response, and stores signed pending state in an HTTP-only cookie. Verification can happen by returning to the protected URL or by submitting to `/.well-known/smartproxy-challenge/*`; reserved verification requests pass query parameters plus JSON or `application/x-www-form-urlencoded` body fields as provider `payload`. Successful verification sets a signed clearance cookie and redirects to the original protected URL stored in the pending cookie.

Rate-limit-triggered challenges use the same provider pipeline through `security.rateLimit.onExceeded`. With `type: 'challenge'`, SmartProxy challenges over-limit requests that match the nested challenge `applyTo` selector. If the request does not match `applyTo`, SmartProxy falls back to a normal `429` response. `clearanceEffect` defaults to `bypass-rate-limit`, so a valid clearance lets matching requests bypass the rate-limit check until the clearance expires; use `clearanceEffect: 'none'` when clearance should not bypass the limiter.

Security checks run access filters first (`ipAllowList`, `ipBlockList`, VPN requirements), then rate limiting, then authentication. This lets challenged browser traffic resolve rate limits before basic/JWT auth prompts while still keeping source access rules terminal.

Current challenge routes must set a stable `name` or `id`, use `action.type: 'forward'`, use `match.protocol: 'http'`, and use HTTP-visible traffic. TLS passthrough, pure UDP, `match.transport: 'all'` without `action.udp.quic.enableHttp3`, socket-handler routes, dynamic forwarding targets, and nftables forwarding are rejected for challenged routes. HTTP/3-enabled `transport: 'all'` routes are accepted.

### 📊 Runtime Management

Control your proxy without restarts:

```typescript
// Dynamic port management
await proxy.addListeningPort(8443);
await proxy.removeListeningPort(8080);
const ports = await proxy.getListeningPorts();

// Update routes on the fly (atomic, mutex-locked)
await proxy.updateRoutes([...newRoutes]);

// Get real-time metrics
const metrics = proxy.getMetrics();
console.log(`Active connections: ${metrics.connections.active()}`);
console.log(`Bytes in: ${metrics.totals.bytesIn()}`);
console.log(`Requests/sec: ${metrics.requests.perSecond()}`);
console.log(`Throughput in: ${metrics.throughput.instant().in} bytes/sec`);

// UDP metrics
console.log(`UDP sessions: ${metrics.udp.activeSessions()}`);
console.log(`Datagrams in: ${metrics.udp.datagramsIn()}`);

// Get detailed statistics from the Rust engine
const stats = await proxy.getStatistics();

// Certificate management
await proxy.provisionCertificate('my-route-name');
await proxy.renewCertificate('my-route-name');
const certStatus = await proxy.getCertificateStatus('my-route-name');

// NFTables status
const nftStatus = await proxy.getNfTablesStatus();
```

### 🔄 Header Manipulation

Transform requests and responses with template variables:

```typescript
{
  action: {
    type: 'forward',
    targets: [{ host: 'backend', port: 8080 }]
  },
  headers: {
    request: {
      'X-Real-IP': '{clientIp}',
      'X-Request-ID': '{uuid}',
      'X-Forwarded-Proto': 'https'
    },
    response: {
      'Strict-Transport-Security': 'max-age=31536000',
      'X-Frame-Options': 'DENY'
    }
  }
}
```

### 🔀 PROXY Protocol Support

Preserve original client information through proxy chains:

```typescript
const proxy = new SmartProxy({
  // Shared trust list for explicit inbound PROXY listener policies
  trustedProxyIPs: ['10.0.0.1', '10.0.0.2'],

  // Forward PROXY protocol to backends
  sendProxyProtocol: true,

  routes: [{
    name: 'smtp-from-edge',
    match: {
      ports: 25,
      transport: 'tcp',
      inboundProxyProtocol: {
        mode: 'required'
      }
    },
    action: {
      type: 'forward',
      targets: [{ host: '127.0.0.1', port: 10025 }]
    }
  }]
});
```

Inbound PROXY protocol is never enabled by the global trust list alone. Declare `match.inboundProxyProtocol` on the listener route with `mode: 'optional'` or `mode: 'required'`; omit it or use `mode: 'reject'` for direct-client listeners. A policy can define its own `trustedProxyIPs`; otherwise explicit `optional` and `required` policies inherit the global `trustedProxyIPs` list.

### 🏗️ Custom Certificate Provisioning

Supply your own certificates or integrate with external certificate providers:

```typescript
const proxy = new SmartProxy({
  certProvisionFunction: async (domain, eventComms) => {
    eventComms.setSource('custom-acme-provider');

    // Return 'http01' to let the built-in ACME handle it
    if (domain.endsWith('.example.com')) return 'http01';

    // Or return a static certificate object
    return {
      publicKey: myPemCert,
      privateKey: myPemKey,
    };
  },
  certProvisionFallbackToAcme: true,  // Fall back to ACME if callback fails
  routes: [...]
});
```

### 💾 Consumer-Managed Certificate Storage

SmartProxy **never writes certificates to disk**. Instead, you own all persistence through the `certStore` interface. This gives you full control — store certs in a database, cloud KMS, encrypted vault, or wherever makes sense for your infrastructure:

```typescript
const proxy = new SmartProxy({
  routes: [...],

  certProvisionFunction: async (domain, eventComms) => {
    const cert = await myAcme.provision(domain);
    eventComms.setExpiryDate(new Date(cert.validUntil));
    return cert;
  },

  // Your persistence layer — SmartProxy calls these hooks
  certStore: {
    // Called once on startup to pre-load persisted certs
    loadAll: async () => {
      const certs = await myDb.getAllCerts();
      return certs.map(c => ({
        domain: c.domain,
        publicKey: c.certPem,
        privateKey: c.keyPem,
        ca: c.caPem,      // optional
      }));
    },

    // Called after each successful cert provision
    save: async (domain, publicKey, privateKey, ca) => {
      await myDb.upsertCert({ domain, certPem: publicKey, keyPem: privateKey, caPem: ca });
    },

    // Optional: called when a cert should be removed
    remove: async (domain) => {
      await myDb.deleteCert(domain);
    },
  },
});
```

**Startup flow:**
1. Rust engine starts
2. Default self-signed `*` fallback cert is loaded (unless `disableDefaultCert: true`)
3. `certStore.loadAll()` is called → all returned certs are loaded into the Rust TLS stack, and their parsed expiry feeds the skip-valid filter
4. `certProvisionFunction` runs for any remaining `certificate: 'auto'` routes
5. After each successful provision, `certStore.save()` is called

This means your second startup is instant — no re-provisioning needed for domains that already have valid certs in your store.

**Provisioning sweeps are conservative:**
- A sweep skips every domain whose loaded cert is still valid beyond `acme.renewThresholdDays` (default 30), so `updateRoutes()` does not re-provision unchanged domains.
- Domains whose provisioning failed wait out `certProvisionFailureCooldownMs` (default 30 minutes) before the next attempt, so a permanently failing domain cannot generate continuous ACME orders.
- Sweeps coalesce: at most one runs at a time plus one queued follow-up, no matter how often routes are updated.
- A renewal sweep runs every `acme.renewCheckIntervalHours` (default 24h), so near-expiry certs renew even when routes never change.

## 🏛️ Architecture

SmartProxy uses a hybrid **Rust + TypeScript** architecture:

```
┌─────────────────────────────────────────────────────┐
│                  Your Application                    │
│     (TypeScript — routes, config, handlers)          │
└──────────────────┬──────────────────────────────────┘
                   │  IPC (JSON over stdin/stdout)
┌──────────────────▼──────────────────────────────────┐
│              Rust Proxy Engine                        │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐  │
│  │ TCP/TLS │ │  HTTP    │ │  Route  │ │  ACME    │  │
│  │ Listener│ │ Reverse  │ │ Matcher │ │ Cert Mgr │  │
│  │         │ │  Proxy   │ │         │ │          │  │
│  └─────────┘ └─────────┘ └─────────┘ └──────────┘  │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐               │
│  │   UDP   │ │ Security│ │ Metrics  │               │
│  │  QUIC   │ │ Enforce │ │ Collect  │               │
│  │  HTTP/3 │ │         │ │          │               │
│  └─────────┘ └─────────┘ └─────────┘               │
└──────────────────┬──────────────────────────────────┘
                   │  Unix Socket Relay
┌──────────────────▼──────────────────────────────────┐
│   TypeScript Socket & Datagram Handler Servers       │
│  (for JS socket handlers, datagram handlers,         │
│   and dynamic routes)                                │
├─────────────────────────────────────────────────────┤
│   @push.rocks/smartnftables (kernel-level NFTables)  │
│  (DNAT/SNAT, firewall, rate limiting via nft CLI)    │
└─────────────────────────────────────────────────────┘
```

- **Rust Engine** handles all networking: TCP, UDP, TLS, QUIC, HTTP proxying, connection management, security, and metrics
- **TypeScript** provides the npm API, configuration types, validation, and handler callbacks
- **NFTables** managed by [`@push.rocks/smartnftables`](https://code.foss.global/push.rocks/smartnftables) — kernel-level DNAT/SNAT forwarding, firewall rules, and rate limiting via the `nft` CLI
- **IPC** — The TypeScript wrapper uses JSON commands/events over stdin/stdout to communicate with the Rust binary
- **Socket/Datagram Relay** — Unix domain socket servers for routes requiring TypeScript-side handling (socket handlers, datagram handlers, dynamic host/port functions)

## 🎯 Route Configuration Reference

### Match Criteria

```typescript
interface IRouteMatch {
  ports: TPortRange;                     // Required — port(s) to listen on
  transport?: 'tcp' | 'udp' | 'all';    // Transport protocol (default: 'tcp')
  inboundProxyProtocol?: {               // Explicit inbound PROXY policy for this listener
    mode: 'reject' | 'optional' | 'required';
    trustedProxyIPs?: string[];
  };
  domains?: string | string[];           // 'example.com', '*.example.com'
  path?: string;                         // '/api/*', '/users/:id'
  clientIp?: string[];                   // ['10.0.0.0/8', '192.168.*']
  tlsVersion?: string[];                 // ['TLSv1.2', 'TLSv1.3']
  headers?: Record<string, string | RegExp>;  // Match by HTTP headers
  protocol?: 'http' | 'tcp' | 'udp' | 'quic' | 'http3';  // Application-layer protocol
}

// Port range supports single numbers, arrays, and ranges
type TPortRange = number | Array<number | { from: number; to: number }>;
```

### Action Types

| Type | Description |
|------|-------------|
| `forward` | Proxy to one or more backend targets (with optional TLS, WebSocket, load balancing, UDP/QUIC) |
| `socket-handler` | Custom socket/datagram handling function in TypeScript |

`targets` are evaluated as route-internal sub-routes by `target.match` and `target.priority`. For load balancing across equivalent upstreams, use a single target with `host: ['a', 'b', 'c']` and target-level `loadBalancing`.

### Target Options

```typescript
interface IRouteTarget {
  host: string | string[] | ((context: IRouteContext) => string | string[]);
  port: number | 'preserve' | ((context: IRouteContext) => number);
  tls?: IRouteTls;                  // Per-target TLS override
  priority?: number;                // Target priority
  match?: ITargetMatch;             // Sub-match within a route (by port, path, headers, method)
  websocket?: IRouteWebSocket;
  loadBalancing?: IRouteLoadBalancing;
  sendProxyProtocol?: boolean;
  headers?: IRouteHeaders;
  advanced?: IRouteAdvanced;
  backendTransport?: 'tcp' | 'udp'; // Backend transport (e.g., receive QUIC, forward as TCP)
}
```

### TLS Options

```typescript
interface IRouteTls {
  mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
  certificate?: 'auto' | {
    key: string;
    cert: string;
    ca?: string;
    keyFile?: string;
    certFile?: string;
  };
  acme?: {
    email: string;
    useProduction?: boolean;
    challengePort?: number;
    renewBeforeDays?: number;
  };
  versions?: string[];
  ciphers?: string;
  honorCipherOrder?: boolean;
  sessionTimeout?: number;
}
```

### WebSocket Options

```typescript
interface IRouteWebSocket {
  enabled: boolean;
  pingInterval?: number;     // ms between pings
  pingTimeout?: number;      // ms to wait for pong
  maxPayloadSize?: number;   // Maximum frame payload
  subprotocols?: string[];   // Allowed subprotocols
  allowedOrigins?: string[]; // CORS origins
}
```

### Load Balancing Options

```typescript
interface IRouteLoadBalancing {
  algorithm: 'round-robin' | 'least-connections' | 'ip-hash';
  healthCheck?: {
    path: string;
    interval: number;        // ms
    timeout: number;         // ms
    unhealthyThreshold: number;
    healthyThreshold: number;
  };
}
```

Use this on an `IRouteTarget` with `host` as a string array. The `healthCheck` shape is accepted by the type layer, but active backend health polling is not currently performed by the Rust selector.

### Backend Protocol Options

```typescript
// Set on action.options
{
  action: {
    type: 'forward',
    targets: [...],
    options: {
      backendProtocol: 'auto' | 'http1' | 'http2' | 'http3'
    }
  }
}
```

| Value | Backend Behavior |
|-------|-----------------|
| `'auto'` | Best-effort: discovers H3 via Alt-Svc, probes H2 via ALPN, falls back to H1 |
| `'http1'` | Always HTTP/1.1 (no ALPN probe) |
| `'http2'` | Always HTTP/2 (hard-fail if handshake fails) |
| `'http3'` | Always HTTP/3 over QUIC (3s connect timeout, hard-fail if unreachable) |

### Rate Limit Options

```typescript
interface IRouteRateLimit {
  enabled: boolean;
  maxRequests: number;
  window: number; // Time window in seconds
  keyBy?: 'ip' | 'path' | 'header';
  headerName?: string;
  errorMessage?: string;
  onExceeded?: {
    type: '429' | 'challenge';
    challenge?: IRouteChallengeConfig;
    clearanceEffect?: 'bypass-rate-limit' | 'none';
  };
}
```

When `onExceeded` is omitted, over-limit requests use the normal `429` behavior and `errorMessage` if present. `onExceeded.type: 'challenge'` requires a nested challenge config and only challenges requests accepted by that challenge's `applyTo` matcher; other over-limit requests receive `429`.

### UDP & QUIC Options

```typescript
interface IRouteUdp {
  sessionTimeout?: number;       // Idle timeout per UDP session (ms, default: 60000)
  maxSessionsPerIP?: number;     // Max concurrent sessions per IP (default: 1000)
  maxDatagramSize?: number;      // Max datagram size in bytes (default: 65535)
  quic?: IRouteQuic;
}

interface IRouteQuic {
  maxIdleTimeout?: number;                // QUIC idle timeout (ms, default: 30000)
  maxConcurrentBidiStreams?: number;       // Max bidi streams (default: 100)
  maxConcurrentUniStreams?: number;        // Max uni streams (default: 100)
  enableHttp3?: boolean;                  // Enable HTTP/3 (default: false)
  altSvcPort?: number;                    // Port for Alt-Svc header
  altSvcMaxAge?: number;                  // Alt-Svc max age in seconds (default: 86400)
  initialCongestionWindow?: number;       // Initial congestion window (bytes)
}
```

## 🛠️ Exports Reference

```typescript
import {
  // Core
  SmartProxy,                      // Main proxy class
  SocketHandlers,                  // Pre-built socket handlers (echo, proxy, block, httpRedirect, httpServer, etc.)

  // Route Utilities
  mergeRouteConfigs,               // Deep-merge two route configs
  findMatchingRoutes,              // Find routes matching criteria
  findBestMatchingRoute,           // Find best matching route
  cloneRoute,                      // Deep-clone a route
  generateRouteId,                 // Generate deterministic route ID
  RouteValidator,                  // Validate route configurations
} from '@push.rocks/smartproxy';
```

All routes are configured as plain `IRouteConfig` objects with `match` and `action` properties — see the examples throughout this document.

## 📖 API Documentation

### SmartProxy Class

```typescript
class SmartProxy extends EventEmitter {
  constructor(options: ISmartProxyOptions);

  // Lifecycle
  start(): Promise<void>;
  stop(): Promise<void>;

  // Route Management (atomic, mutex-locked)
  updateRoutes(routes: IRouteConfig[]): Promise<void>;
  updateSecurityPolicy(policy: ISmartProxySecurityPolicy): Promise<void>;
  registerChallengeProvider(providerId: string, provider: IChallengeProvider): void;

  // Port Management
  addListeningPort(port: number): Promise<void>;
  removeListeningPort(port: number): Promise<void>;
  getListeningPorts(): Promise<number[]>;

  // Monitoring & Metrics
  getMetrics(): IMetrics;              // Sync — returns cached metrics adapter
  getStatistics(): Promise<IRustStatistics>; // Async — queries Rust engine

  // Certificate Management
  provisionCertificate(routeName: string): Promise<void>;
  renewCertificate(routeName: string): Promise<void>;
  getCertificateStatus(routeName: string): Promise<any>;
  getEligibleDomainsForCertificates(): string[];

  // NFTables (managed by @push.rocks/smartnftables)
  getNfTablesStatus(): INftStatus | null;

  // Events
  on(event: 'error', handler: (err: Error) => void): this;
  on(event: 'certificate-issued', handler: (ev: ICertificateIssuedEvent) => void): this;
  on(event: 'certificate-failed', handler: (ev: ICertificateFailedEvent) => void): this;
}
```

### Configuration Options

```typescript
interface ISmartProxyOptions {
  routes: IRouteConfig[];                   // Required: array of route configs

  // ACME/Let's Encrypt
  acme?: {
    email: string;                          // Contact email for Let's Encrypt
    useProduction?: boolean;                // Use production servers (default: false)
    port?: number;                          // HTTP-01 challenge port (default: 80)
    renewThresholdDays?: number;            // Days before expiry to renew (default: 30)
    autoRenew?: boolean;                    // Enable auto-renewal (default: true)
    renewCheckIntervalHours?: number;       // Renewal check interval (default: 24)
  };

  // Custom certificate provisioning
  certProvisionFunction?: (
    domain: string,
    eventComms: ICertProvisionEventComms
  ) => Promise<TSmartProxyCertProvisionObject>;
  certProvisionFallbackToAcme?: boolean;    // Fall back to ACME on failure (default: true)
  certProvisionTimeout?: number;            // Timeout per provision call (ms)
  certProvisionConcurrency?: number;        // Max concurrent provisions
  certProvisionFailureCooldownMs?: number;  // Per-domain cooldown after a failed provision (default: 1800000)

  // Consumer-managed certificate persistence (see "Consumer-Managed Certificate Storage")
  certStore?: ISmartProxyCertStore;

  // Self-signed fallback
  disableDefaultCert?: boolean;             // Disable '*' self-signed fallback (default: false)

  // Rust binary path override
  rustBinaryPath?: string;                  // Custom path to the Rust proxy binary

  // Global defaults
  defaults?: {
    target?: { host: string; port: number };
    security?: { ipAllowList?: string[]; ipBlockList?: string[]; maxConnections?: number };
  };

  // PROXY protocol
  trustedProxyIPs?: string[];               // Global trusted IP/CIDR list for explicit inbound policies
  sendProxyProtocol?: boolean;              // Send PROXY protocol to targets

  // Timeouts
  connectionTimeout?: number;               // Backend connection timeout (default: 60s)
  initialDataTimeout?: number;              // Initial data/SNI timeout (default: 60s)
  socketTimeout?: number;                   // Socket inactivity timeout (default: 60s)
  maxConnectionLifetime?: number;           // Max connection lifetime (default: 1h)
  inactivityTimeout?: number;               // Inactivity timeout (default: 75s)
  gracefulShutdownTimeout?: number;         // Shutdown grace period (default: 30s)

  // Connection limits
  maxConnectionsPerIP?: number;             // Per-IP connection limit (default: 100)
  connectionRateLimitPerMinute?: number;    // Per-IP rate limit (default: 300/min)
  securityPolicy?: {
    blockedIps?: string[];
    blockedCidrs?: string[];
  };                                        // Global ingress block policy

  // Runtime-only challenge enforcement settings
  challenge?: {
    cookieSigningKey?: string;              // Generated ephemerally when omitted
    pendingCookieName?: string;             // Default: __smartproxy_challenge_pending
    clearanceCookieName?: string;           // Default: __smartproxy_clearance
    reservedPathPrefix?: string;            // Default: /.well-known/smartproxy-challenge
    relaySocketPath?: string;               // Internal Unix socket path; normally generated by SmartProxy
    relayTimeoutMs?: number;                // Provider relay timeout (default: 5000)
    pendingTtlSeconds?: number;             // Pending challenge cookie TTL (default: 300)
  };

  // Keep-alive
  keepAliveTreatment?: 'standard' | 'extended' | 'immortal';
  keepAliveInactivityMultiplier?: number;   // (default: 4)
  extendedKeepAliveLifetime?: number;       // (default: 1h)

  // Metrics
  metrics?: {
    enabled?: boolean;
    sampleIntervalMs?: number;
    retentionSeconds?: number;
  };

  // Behavior
  enableDetailedLogging?: boolean;          // Verbose connection logging
  enableTlsDebugLogging?: boolean;          // TLS handshake debug logging
}
```

### ISmartProxyCertStore Interface

```typescript
interface ISmartProxyCertStore {
  /** Called once on startup to pre-load persisted certs */
  loadAll: () => Promise<Array<{
    domain: string;
    publicKey: string;
    privateKey: string;
    ca?: string;
  }>>;

  /** Called after each successful cert provision */
  save: (domain: string, publicKey: string, privateKey: string, ca?: string) => Promise<void>;

  /** Optional: remove a cert from storage */
  remove?: (domain: string) => Promise<void>;
}
```

### IMetrics Interface

The `getMetrics()` method returns a cached metrics adapter that polls the Rust engine:

```typescript
const metrics = proxy.getMetrics();

// Connection metrics
metrics.connections.active();              // Current active connections
metrics.connections.total();               // Total connections since start
metrics.connections.byRoute();             // Map<routeName, activeCount>
metrics.connections.byIP();                // Map<ip, activeCount>
metrics.connections.topIPs(10);            // Top N IPs by connection count
metrics.connections.domainRequestsByIP();  // Map<ip, Map<domain, requestCount>>
metrics.connections.topDomainRequests(20); // Top IP/domain pairs by request count
metrics.connections.frontendProtocols();   // H1/H2/H3/WS frontend distribution
metrics.connections.backendProtocols();    // H1/H2/H3/WS backend distribution

// Throughput (bytes/sec)
metrics.throughput.instant();              // { in: number, out: number }
metrics.throughput.recent();               // Recent average
metrics.throughput.average();              // Overall average
metrics.throughput.custom(30);             // Custom window, if provided by Rust cache
metrics.throughput.history(60);            // Recent throughput samples
metrics.throughput.byRoute();              // Map<routeName, { in, out }>
metrics.throughput.byIP();                 // Map<ip, { in, out }>

// Request rates
metrics.requests.perSecond();              // Requests per second
metrics.requests.perMinute();              // Requests per minute
metrics.requests.total();                  // Total requests
metrics.requests.byDomain();               // Map<domain, { perSecond, lastMinute }>

// UDP metrics
metrics.udp.activeSessions();              // Current active UDP sessions
metrics.udp.totalSessions();              // Total UDP sessions since start
metrics.udp.datagramsIn();                // Datagrams received
metrics.udp.datagramsOut();               // Datagrams sent

// Cumulative totals
metrics.totals.bytesIn();                  // Total bytes received
metrics.totals.bytesOut();                 // Total bytes sent
metrics.totals.connections();              // Total connections

// Backend metrics
metrics.backends.byBackend();              // Map<backend, IBackendMetrics>
metrics.backends.protocols();              // Map<backend, protocol>
metrics.backends.topByErrors(10);          // Top N error-prone backends
metrics.backends.detectedProtocols();      // Backend protocol discovery cache

// Percentiles
metrics.percentiles.connectionDuration();  // { p50, p95, p99 }
metrics.percentiles.bytesTransferred();    // { in: { p50, p95, p99 }, out: { p50, p95, p99 } }
```

The percentile methods are part of the public metrics shape. In the current Rust adapter they return zeroed values until percentile collection is implemented in the Rust metrics snapshot.

## 🐛 Troubleshooting

### Certificate Issues
- ✅ Ensure domain DNS points to your server
- ✅ Port 80 must be accessible for ACME HTTP-01 challenges
- ✅ Check DNS propagation with `dig` or `nslookup`
- ✅ Verify the email in ACME configuration is valid
- ✅ Use `getCertificateStatus('route-name')` to check cert state

### Connection Problems
- ✅ Check route priorities (higher number = matched first)
- ✅ Verify security rules aren't blocking legitimate traffic
- ✅ Test with `curl -v` for detailed connection output
- ✅ Enable debug logging with `enableDetailedLogging: true`

### Rust Binary Not Found

SmartProxy searches for the Rust binary in this order:
1. `rustBinaryPath` option in `ISmartProxyOptions`
2. `SMARTPROXY_RUST_BINARY` environment variable
3. Platform-specific npm package (`@push.rocks/smartproxy-linux-x64`, etc.)
4. `dist_rust/rustproxy` relative to the package root (built by `tsrust`)
5. Local dev build (`./rust/target/release/rustproxy`)
6. System PATH (`rustproxy`)

### QUIC / HTTP3 Caveats
- **GREASE frames are disabled.** The underlying h3 crate sends [GREASE frames](https://www.rfc-editor.org/rfc/rfc9114.html#frame-reserved) by default to test protocol extensibility. However, some HTTP/3 clients and servers don't properly ignore unknown frame types, causing 400/500 errors or stream hangs ([h3#206](https://github.com/hyperium/h3/issues/206)). SmartProxy disables GREASE on both the server side (for incoming H3 requests) and the client side (for H3 backend connections) to maximize compatibility.
- **HTTP/3 is pre-release.** The h3 ecosystem (h3 0.0.8, h3-quinn 0.0.10, quinn 0.11) is still pre-1.0. Expect rough edges.

### Performance Tuning
- ✅ Use NFTables forwarding for high-traffic routes (Linux only)
- ✅ Enable connection keep-alive where appropriate
- ✅ Use `getMetrics()` and `getStatistics()` to identify bottlenecks
- ✅ Adjust `maxConnectionsPerIP` and `connectionRateLimitPerMinute` based on your workload
- ✅ Use `passthrough` TLS mode when backend can handle TLS directly

## 🏆 Best Practices

1. **📝 Use Helper Functions** — They provide sensible defaults and prevent common mistakes
2. **🎯 Set Route Priorities** — More specific routes should have higher priority values
3. **🔒 Enable Security** — Always use IP filtering and rate limiting for public-facing services
4. **📊 Monitor Metrics** — Use the built-in metrics to catch issues early
5. **🔄 Certificate Monitoring** — Set up alerts before certificates expire
6. **🛑 Graceful Shutdown** — Always call `proxy.stop()` for clean connection termination
7. **✅ Validate Routes** — Use `RouteValidator.validateRoutes()` to catch config errors before deployment
8. **🔀 Atomic Updates** — Use `updateRoutes()` for hot-reloading routes (mutex-locked, no downtime)
9. **🎮 Use Socket Handlers** — For protocols beyond HTTP, implement custom socket handlers instead of fighting the proxy model
10. **💾 Use `certStore`** — Persist certs in your own storage to avoid re-provisioning on every restart

## License and Legal Information

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.

**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

### Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

### Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
