Manage Cloudflare Tunnels, nginx reverse proxies, and DNS records from one place.
One config file → auto-generate nginx + tunnel + DNS records.
- Multi-Account Support: Manage multiple Cloudflare accounts from one dashboard
- One-Command Setup: Initialize with your Cloudflare API token
- Subdomain Management: Add/remove subdomains pointing to any local port
- Auto-Generate: Nginx configs, tunnel ingress rules, DNS records
- Web Dashboard: User-friendly UI at
http://localhost:7070 - REST API: Full CRUD with input validation and pagination
- MCP Server: AI agent integration (OpenClaw, Claude, etc.)
- Multi-Domain: Manage multiple domains from a single API token
- Multi-Language: English, Russian, Chinese, Hindi, Indonesian
- Security: Helmet headers, rate limiting, IP whitelist/blacklist, auth
- Monitoring: Health checks, traffic stats, SSL expiry, port scanner
- Request Logging: Detailed access/error logs with analytics
- Docker Support: Ready-to-use Dockerfile and docker-compose
- PWA Support: Installable as a web app
- Test Suite: Jest unit and integration tests
# Add GitHub Packages registry
echo "@oyi77:registry=https://npm.pkg.github.com" >> ~/.npmrc
# Install
npm install -g @oyi77/cloudflare-routernpm install -g cloudflare-routernpx @oyi77/cloudflare-router dashboardgit clone https://github.com/oyi77/cloudflare-router.git
cd cloudflare-router
docker-compose up -d# Initialize
cloudflare-router init \
--token "your-cf-api-token" \
--zone "your-zone-id" \
--tunnel "your-tunnel-id" \
--domain "example.com" \
--credentials "/path/to/tunnel/credentials.json"
# Add mappings
cloudflare-router add api 3002 -d "Backend API"
cloudflare-router add app 3000 -d "Frontend App"
cloudflare-router add admin 3001 -d "Admin Panel"
# Generate configs
cloudflare-router generate
# Deploy DNS records to Cloudflare
cloudflare-router deploy
# Start dashboard
cloudflare-router dashboardThe file watcher automatically regenerates tunnel configs when mapping files change, eliminating manual regeneration steps.
Start the watcher:
# CLI command
cloudflare-router watcher
# Or npm script
npm run watcher
# Or with PM2 (recommended for production)
npm run pm2:start- Watches
~/.cloudflare-router/mappings/*.ymlfor changes - Detects file modifications (add, change, delete)
- Automatically runs
node src/cli.js generateon change - Logs all activity to
~/.cloudflare-router/logs/watcher.log - Debounces rapid changes (waits 1 second after last change)
# Terminal 1: Start watcher
cloudflare-router watcher
# Terminal 2: Edit mapping file
vim ~/.cloudflare-router/mappings/cf_myzone.yml
# Watcher automatically detects change and regenerates config
# Output: ✓ Config regenerated successfullyThe watcher runs as a separate PM2 app alongside the main server:
# Start both server and watcher
npm run pm2:start
# View logs
pm2 logs cf-router-watcher
# Stop watcher
pm2 stop cf-router-watcher
# Restart watcher
pm2 restart cf-router-watcherTo enable automatic sync to Cloudflare after regeneration, uncomment the sync block in src/watcher.js:
// Optional: Auto-sync to Cloudflare (uncomment to enable)
try {
log('Syncing to Cloudflare...', 'info');
execSync('python3 sync-tunnel-config.py', {
cwd: ROUTER_DIR,
encoding: 'utf-8',
stdio: 'pipe'
});
log('Sync completed', 'success');
} catch (syncErr) {
log(`Sync failed: ${syncErr.message}`, 'warn');
}Watcher activity is logged to ~/.cloudflare-router/logs/watcher.log:
# View recent watcher logs
tail -f ~/.cloudflare-router/logs/watcher.log
# View via PM2
pm2 logs cf-router-watcher┌─────────────────────────────────────────────────────────────┐
│ Cloudflare Router │
├─────────────────────────────────────────────────────────────┤
│ │
│ config.yml ──→ Nginx Generator ──→ nginx.conf │
│ │ + │
│ │ Tunnel Generator ──→ tunnel/config.yml │
│ │ + │
│ mappings.yml ──→ DNS API ──→ Cloudflare DNS Records │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ CLI │ │ Web Dashboard│ │ MCP Server │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌──────────────┐
│ Nginx │
│ :6969 │
└──────┬───────┘
│
▼
┌──────────────┐
│ Cloudflare │
│ Tunnel │
└──────┬───────┘
│
▼
*.example.com ──→ localhost:PORT
cloudflare-router init Initialize configuration
cloudflare-router account:add Add Cloudflare account
cloudflare-router account:list List all accounts
cloudflare-router add Add subdomain mapping
cloudflare-router remove Remove subdomain mapping
cloudflare-router list List all mappings
cloudflare-router generate Generate nginx + tunnel configs
cloudflare-router deploy Deploy DNS records to Cloudflare
cloudflare-router status Show system status
cloudflare-router watcher Start file watcher for auto-regeneration
cloudflare-router dashboard Start web dashboard
# Short alias
cfr dashboard
# Bash
cp completions/cloudflare-router.bash /etc/bash_completion.d/
# Zsh
cp completions/cloudflare-router.zsh /usr/share/zsh/site-functions/_cloudflare-router| Method | Endpoint | Description |
|---|---|---|
| GET | /api/accounts |
List accounts |
| POST | /api/accounts |
Add account |
| DELETE | /api/accounts/:id |
Remove account |
| GET | /api/accounts/:id/verify |
Verify account credentials |
| GET | /api/accounts/:id/discover |
Discover zones |
| POST | /api/accounts/:id/zones |
Add zone to account |
| DELETE | /api/accounts/:id/zones/:zoneId |
Remove zone |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/mappings |
List mappings (supports pagination, filter, sort) |
| POST | /api/mappings |
Add mapping |
| PUT | /api/mappings/:account/:zone/:subdomain |
Update mapping |
| PATCH | /api/mappings/:account/:zone/:subdomain |
Toggle enabled state |
| DELETE | /api/mappings/:account/:zone/:subdomain |
Remove mapping |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/status |
System status |
| GET | /api/stats |
Traffic statistics |
| POST | /api/generate |
Generate nginx configs |
| POST | /api/deploy |
Deploy DNS records |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/dns/all |
List all DNS records |
| GET | /api/ssl/all |
SSL certificates |
| GET | /api/ssl/:domain |
SSL details for domain |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/logs/access?lines=100 |
Access logs |
| GET | /api/logs/errors?lines=100 |
Error logs |
| GET | /api/logs/stats |
Log statistics |
| DELETE | /api/logs |
Clear logs |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/ip/lists |
Get whitelist/blacklist |
| POST | /api/ip/whitelist |
Add IP to whitelist |
| POST | /api/ip/blacklist |
Add IP to blacklist |
| DELETE | /api/ip/whitelist/:ip |
Remove from whitelist |
| DELETE | /api/ip/blacklist/:ip |
Remove from blacklist |
Pagination:
?page=1- Page number (default: 1)?limit=50- Items per page (default: 50, max: 100)
Filtering & Sorting:
?filter=api- Filter by subdomain/domain/account name?sort=subdomain:asc- Sort by field (asc/desc)
All POST/PUT endpoints validate input:
| Field | Validation |
|---|---|
email |
Valid email format |
subdomain |
Alphanumeric + hyphens, 1-63 chars |
port |
Integer 1-65535 |
domain |
Valid FQDN |
ip |
Valid IPv4/IPv6 |
Validation errors return 400 with details:
{
"error": "Validation failed",
"code": "validation_error",
"details": [
{ "field": "subdomain", "message": "Valid subdomain required" }
]
}- Login via
POST /api/auth/loginwith{ password }in the request body - On success, returns a cryptographically random 64-character hex token (32 bytes via
crypto.randomBytes) - Use the token as
Authorization: Bearer <token>on subsequent requests - Tokens are stored in-memory and cleared on server restart
- App names are validated against
/^[a-zA-Z0-9_-]+$/(max 128 characters) - App config updates only accept whitelisted fields:
command,script,cwd,env,mode,port,restartPolicy,enabled,autoStart - Domain names in SSL checks are validated against a strict regex
- Cloudflare config paths must resolve within
~/.cloudflared/
- Helmet.js: Security headers (CSP, HSTS, X-Frame-Options)
- Rate Limiting:
- Auth endpoints: 5 requests per 15 minutes
- API endpoints: 100 requests per minute
- CORS: Configurable via
CORS_ORIGINenv var - Request Tracing:
X-Request-IDheader for debugging
The server handles SIGTERM gracefully:
- Stops accepting new connections
- Waits up to 10 seconds for in-flight requests to complete
- Sends SIGTERM to all managed app processes, then SIGKILL after 5 seconds
- Forces exit after 15 seconds if shutdown hangs
Access logs are written to ~/.cloudflare-router/access.log.
- Rotation triggers when the log file exceeds 10MB
- Up to 5 rotated copies are kept (
.log.1through.log.5)
- Apps with
autoStart: trueinapps.yamlstart automatically on server boot restartPolicy: alwaysrestarts apps on exit (exponential backoff: 1s → 2s → 4s → ... → 30s cap)restartPolicy: on-failurerestarts only on non-zero exit codesrestartPolicy: never(default) does not restart
cloudflare_router_list_mappings- List all mappingscloudflare_router_add_mapping- Add subdomain mappingcloudflare_router_remove_mapping- Remove mappingcloudflare_router_toggle_mapping- Enable/disable mappingcloudflare_router_generate- Generate configscloudflare_router_deploy- Deploy DNS recordscloudflare_router_status- Get statuscloudflare_router_list_dns- List DNS recordscloudflare_router_verify_token- Verify API tokencloudflare_router_get_config- Get config
Config is stored in ~/.cloudflare-router/config.yml:
cloudflare:
api_token: "your-api-token"
zone_id: "your-zone-id"
tunnel_id: "your-tunnel-id"
tunnel_credentials: "/path/to/credentials.json"
domain: "example.com"
nginx:
listen_port: 6969
config_dir: "~/.cloudflare-router/nginx/sites"
server:
port: 7070
host: "0.0.0.0"Manage multiple Cloudflare accounts:
accounts:
- id: "personal"
name: "Personal"
email: "user@gmail.com"
api_key: "your-api-key"
zones:
- zone_id: "zone-id-1"
domain: "example.com"
tunnel_id: "tunnel-id-1"
- id: "work"
name: "Work"
email: "user@company.com"
api_key: "work-api-key"
zones:
- zone_id: "zone-id-2"
domain: "company.com"
tunnel_id: "tunnel-id-2"Access logs are stored in ~/.cloudflare-router/logs/:
access.log- All requests (JSON format)error.log- 4xx/5xx errors only
View logs via API:
curl http://localhost:7070/api/logs/access -H "Authorization: Bearer 123456"
curl http://localhost:7070/api/logs/stats -H "Authorization: Bearer 123456"DASHBOARD_PASSWORD=123456 # Dashboard auth password
AUTH_TOKEN=your-token # API auth token
WEBHOOK_URL=https://... # Webhook for alerts
CORS_ORIGIN=https://example.com # CORS origin (default: all)Run the full test suite:
npm test -- --runInBandThe --runInBand flag prevents flaky failures caused by parallel test suite cleanup of shared fixture directories.
Test files:
tests/api.test.js— API integration teststests/portless.test.js— Portless service module teststests/app-lifecycle.test.js— App lifecycle API teststests/mcp.test.js— MCP tool handler teststests/watchdog.test.js— Watchdog restart logic teststests/auth-flow.test.js— Auth token flow teststests/security.test.js— Input validation unit tests
# Run all tests
npm test
# Watch mode
npm run test:watch
# Coverage report
npm run test:coveragesrc/
├── server.js # Express API server
├── config.js # Configuration management
├── cloudflare.js # Cloudflare API client
├── nginx.js # Nginx config generator
├── tunnel.js # Tunnel config generator
├── middleware.js # Rate limiting & IP filtering
├── logger.js # Request logging
├── backup.js # Backup & restore
├── i18n.js # Internationalization
├── cli.js # CLI commands
├── mcp.js # MCP server
└── dashboard/ # Web UI (static files)
tests/
├── config.test.js # Config unit tests
├── api.test.js # API integration tests
└── validation.test.js # Validation tests
MIT