Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
bun-version: latest

- name: Install dependencies
run: bun install
run: bun install --frozen-lockfile

#- name: Linting
# run: bun run format
Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,9 @@ results/

.plan.md

sec-findings.md
sec-findings.md
sec-findings.md*.pem
VERIFICATION.md
verification-certificate.json
*.key
examples/cert.*
13 changes: 12 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ src/
### Path Validation (input-validator.ts + utils.ts)

**Two-pass validation** against double-encoding attacks:

1. **First pass:** Check raw path against `blockedPatterns` (catches null bytes, `../`)
2. **Recursive decode:** `recursiveDecodeURIComponent()` decodes up to 5 layers until stable
3. **Second pass:** Check fully-decoded path (catches `%252f` → `%2f` → `/`)
Expand All @@ -90,6 +91,7 @@ Never validate before decoding — attackers hide behind encoding layers.
### Health Checks (http-load-balancer.ts)

Threshold-based to prevent flapping and cascade failures:

- `failureThreshold` (default 3): consecutive failures before marking unhealthy
- `successThreshold` (default 2): consecutive successes before marking healthy again
- `minHealthyTargets` (default 1): floor check — refuses to mark the last healthy target down
Expand All @@ -105,14 +107,23 @@ secure header priority (`cf-connecting-ip` > `x-real-ip`).
### Error Handling

Global error handler registered on the 0http-bun router. In production:

- Returns sanitized `{"error":"Internal server error"}` with status 500
- Never leaks stack traces or internal file paths
- Logs full error details internally

### Blocked Patterns (config.ts)

```typescript
blockedPatterns: [/\.\./, /%2e%2e/i, /%2f/i, /%5c/i, /%00/, /\0/, /%25%32%[fF]/i]
blockedPatterns: [
/\.\./,
/%2e%2e/i,
/%2f/i,
/%5c/i,
/%00/,
/\0/,
/%25%32%[fF]/i,
]
```

Covers: raw `..`, encoded `../`, encoded `/`, encoded `\`, null byte, double-encoded `/`.
Expand Down
8 changes: 8 additions & 0 deletions benchmark/bungate-gateway.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* ═══════════════════════════════════════════════════════════════════════════
* FOR BENCHMARKING ONLY — NOT FOR PRODUCTION
* This file intentionally disables security features to maximize throughput.
* Do not use this configuration as a template for production deployments.
* ═══════════════════════════════════════════════════════════════════════════
*/

import { BunGateway } from './src'
import { BunGateLogger } from './src'
import { cpus } from 'os'
Expand Down
28 changes: 23 additions & 5 deletions benchmark/echo-server-simple.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
/**
* ═══════════════════════════════════════════════════════════════════════════
* FOR BENCHMARKING ONLY — NOT FOR PRODUCTION
* ═══════════════════════════════════════════════════════════════════════════
*/

const serverId = process.env.SERVER_ID || 'unknown'
const port = parseInt(process.env.SERVER_PORT || '8080')

let requestCount = 0
let startTime = Date.now()

const SENSITIVE_HEADERS = new Set([
'authorization',
'cookie',
'proxy-authorization',
'x-api-key',
'x-metrics-key',
])

const server = Bun.serve({
port,
fetch(req) {
Expand All @@ -19,24 +33,28 @@ const server = Bun.serve({
})
}

// Echo endpoint with server info
// Echo endpoint with server info — redact sensitive headers
const safeHeaders: Record<string, string> = {}
for (const [name, value] of req.headers.entries()) {
safeHeaders[name] = SENSITIVE_HEADERS.has(name.toLowerCase())
? '[REDACTED]'
: value
}

const response = {
server_id: serverId,
request_count: requestCount,
uptime_ms: now - startTime,
timestamp: now,
method: req.method,
url: req.url,
headers: Object.fromEntries(req.headers.entries()),
headers: safeHeaders,
remote_addr: req.headers.get('x-forwarded-for') || 'unknown',
}

return new Response(JSON.stringify(response, null, 2), {
headers: {
'Content-Type': 'application/json',
Server: `echo-server-${serverId}`,
'X-Request-Count': requestCount.toString(),
'X-Server-Id': serverId,
},
})
},
Expand Down
5 changes: 4 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ interface ClusterConfig {
respawnThresholdTime?: number // Default: 60000ms
shutdownTimeout?: number // Default: 30000ms
exitOnShutdown?: boolean // Default: true
allowedEnvVars?: string[] // Allow-list of env vars forwarded to workers
workerScriptAllowlist?: string[] // Allow-list of worker script paths
}
```

Expand Down Expand Up @@ -392,12 +394,17 @@ gateway.addRoute({
```typescript
interface HealthCheckConfig {
enabled: boolean
interval?: number // Default: 30000ms
timeout?: number // Default: 5000ms
interval?: number // Default: 30000ms, clamped to 1000ms–300000ms
timeout?: number // Default: 5000ms, must be less than interval
path?: string // Default: '/health'
method?: 'GET' | 'HEAD' // Default: 'GET'
expectedStatus?: number // Default: 200
unhealthyThreshold?: number // Default: 3
healthyThreshold?: number // Default: 2
expectedBody?: string // Optional body substring required for healthy status
failureThreshold?: number // Default: 3, max 20
successThreshold?: number // Default: 2, max 20
minHealthyTargets?: number // Default: 1
allowedSchemes?: string[] // Default: ['http', 'https']
allowedHosts?: string[] // CIDR or exact hosts allowed for health checks
validator?: (response: Response) => Promise<boolean> | boolean
}
```
Expand Down
8 changes: 8 additions & 0 deletions docs/CLUSTERING.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ interface ClusterConfig {
// Exit master process after shutdown (default: true)
// Set to false for testing or embedded usage
exitOnShutdown: boolean

// Allow-list of environment variable names forwarded to workers.
// If provided, only these variables (plus internal CLUSTER_* vars) are passed.
allowedEnvVars?: string[]

// Allow-list of worker script paths. The worker script must match one of
// these paths to prevent accidental execution of arbitrary files.
workerScriptAllowlist?: string[]
}
```

Expand Down
41 changes: 29 additions & 12 deletions docs/LOAD_BALANCING.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,8 @@ gateway.addRoute({
timeout: 5000, // 5 second timeout
path: '/health', // Health check endpoint
expectedStatus: 200, // Expected status code
unhealthyThreshold: 3, // Failures before marking unhealthy
healthyThreshold: 2, // Successes before marking healthy
failureThreshold: 3, // Failures before marking unhealthy
successThreshold: 2, // Successes before marking healthy
},
},
})
Expand Down Expand Up @@ -594,7 +594,7 @@ gateway.addRoute({
enabled: true,
interval: 10000,
path: '/health',
unhealthyThreshold: 2, // Fast failover
failureThreshold: 2, // Fast failover
},
},
})
Expand Down Expand Up @@ -650,12 +650,29 @@ healthCheck: {
interval: 15000,
timeout: 5000,
path: '/health',
unhealthyThreshold: 3,
healthyThreshold: 2,
failureThreshold: 3,
successThreshold: 2,
}
```

### 2. Use Circuit Breakers for External Services
### 2. Restrict Health Check Targets

For security, limit which schemes and hosts the load balancer may probe.

```typescript
healthCheck: {
enabled: true,
interval: 15000,
timeout: 5000,
path: '/health',
failureThreshold: 3,
successThreshold: 2,
allowedSchemes: ['http', 'https'],
allowedHosts: ['10.0.0.0/8', 'api.internal.example.com'],
}
```

### 3. Use Circuit Breakers for External Services

```typescript
circuitBreaker: {
Expand All @@ -666,7 +683,7 @@ circuitBreaker: {
}
```

### 3. Configure Timeouts
### 4. Configure Timeouts

```typescript
gateway.addRoute({
Expand All @@ -681,7 +698,7 @@ gateway.addRoute({
})
```

### 4. Monitor Target Health
### 5. Monitor Target Health

```typescript
import { PinoLogger } from 'bungate'
Expand All @@ -698,7 +715,7 @@ gateway.on('target-healthy', (target) => {
})
```

### 5. Use Appropriate Strategy
### 6. Use Appropriate Strategy

```typescript
// ❌ DON'T use IP hash for APIs behind NAT
Expand All @@ -714,7 +731,7 @@ loadBalancer: {
}
```

### 6. Plan for Capacity
### 7. Plan for Capacity

```typescript
// Configure weights based on actual capacity
Expand All @@ -728,7 +745,7 @@ loadBalancer: {
}
```

### 7. Test Failover Scenarios
### 8. Test Failover Scenarios

```bash
# Simulate server failure
Expand Down Expand Up @@ -781,7 +798,7 @@ targets: [
healthCheck: {
enabled: true,
timeout: 10000, // Increase from 5000
unhealthyThreshold: 5, // Require more failures
failureThreshold: 5, // Require more failures
}

// 2. Check health endpoint performance
Expand Down
Loading
Loading