The safest way to expose self-hosted TCP services to the public with a tiny footprint.
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Clients │────▶│ SPF │────▶│ Backends │
│ │◀────│ TLS + LB │◀────│ │
└─────────────┘ └──────────────┘ └─────────────┘
│
┌──────┴──────┐
│ SIEM Engine │
│ Health Chks │
│ Metrics │
└─────────────┘
graph TB
subgraph "Client Layer"
C1[Client 1]
C2[Client 2]
C3[Client N]
end
subgraph "SPF Core"
TLS[TLS Termination]
AUTH[Auth Layer]
LB[Load Balancer]
HC[Health Checker]
SIEM[SIEM Engine]
LOG[Audit Logger]
end
subgraph "Backend Pool"
B1[Backend 1]
B2[Backend 2]
B3[Backend N]
end
C1 --> TLS
C2 --> TLS
C3 --> TLS
TLS --> AUTH
AUTH --> LB
LB --> B1
LB --> B2
LB --> B3
HC --> B1
HC --> B2
HC --> B3
AUTH --> SIEM
SIEM --> LOG
The strongest painpoint SPF targets is this:
"I need to expose my app publicly, but I do not want to run a massive reverse-proxy stack or accidentally expose an unauthenticated admin/control plane."
SPF is built for operators who want:
- dynamic L4 forwarding
- strict defaults around control-plane safety
- small binary, simple deployment, and high signal observability
- TCP Port Forwarding - High-performance L4 proxy
- TLS Termination - OpenSSL with TLS 1.2+
- Load Balancing - Round-robin, least-conn, IP-hash, weighted
- Health Checks - Auto-detect backend failures
- Rate Limiting - Per-IP and global token bucket
- Audit Logging - JSON structured events
- IP Blocking - Manual and automatic (brute-force)
- Geo-IP Blocking - Block by country
- Threat Intelligence - External blocklist feeds
- Anomaly Detection - Traffic pattern analysis
- PROXY Protocol v2 - Preserve client IPs
- Webhook Alerts - Slack/Discord/PagerDuty
- Admin Lockout & Rate Limiting - Brute-force and abuse protection on control plane
- Readonly Control Sessions - Separate readonly token for safe ops access
- Tamper-Evident Audit Chain - JSON audit entries with hash chaining
- Dual-Control Config Apply - Stage/apply/rollback workflow for admin config changes
- Prometheus Metrics - Full observability
- Live Control - TCP control protocol
- Hot Reload - Change rules without restart
- Daemon Mode - Background service
- Cross-Platform - Linux, macOS, Windows, ESP32
- Config Rules Start on Boot - Persisted rules are actively started at launch
- Safer Forwarding Path - Handles partial socket writes to avoid data truncation
- Hardened Input Parsing - Strict numeric parsing for ports/rates in config and CLI
- Control-Plane Guardrail - Refuses non-loopback admin bind without token
# build
make
# run with auth token
./bin/spf --token mysecret
# connect control
nc localhost 8081
> AUTH mysecret
> ADD 8080 10.0.0.1:80,10.0.0.2:80 rr
> STATUSRun validation quickly:
make testUse the hardened config template:
cp spf.conf.example spf.confUse this checklist before exposing SPF on a public or shared network:
- Always set
--token(oradmin.tokenin config). - Keep admin bind on loopback unless absolutely required.
- If exposing admin remotely, require TLS/mTLS and firewall-restrict source IPs.
- Enable
security.enabled = trueand set reasonable rate limits. - Scrape metrics and alert on
spf_blocked_total, backend down events, and active connection spikes.
SPF enforces one guardrail by default now: if admin bind is non-loopback and no token is set, startup fails.
# debian/ubuntu
make install-deps-debian
make
sudo make install
sudo make install-service
# arch
make install-deps-arch
make
sudo make install
# fedora/rhel/centos
make install-deps-fedora
make
sudo make install
# opensuse
make install-deps-suse
make
sudo make install
# alpine
make install-deps-alpine
make
sudo make install
# macos
make install-deps-macos
make
sudo make installSPF ships distro-friendly packaging targets:
# local Debian package
make package-deb VERSION=2.0.0
# local RPM package
make package-rpm VERSION=2.0.0
# build both
make package-all VERSION=2.0.0Install from package:
sudo apt install ./spf_2.0.0_amd64.deb
sudo dnf install ./spf-2.0.0-1.x86_64.rpmFor packagers, DESTDIR is supported:
make DESTDIR="$(pwd)/build/stage" installMore details: docs/packaging.md
AUTH <token> # authenticate first
STATUS # system overview
RULES # list all rules
BACKENDS <id> # show backends for rule
ADD <port> <backends> [algo] # add forwarding rule
DEL <id> # delete rule
PAUSE <id> # stop accepting new conns for rule
RESUME <id> # resume accepting for rule
DRAIN <id> <idx> [sec] # gracefully drain backend index
SETWEIGHT <id> <idx> <w> # set backend weight
SETSTATE <id> <idx> <UP|DOWN|DRAIN> # force backend state
HEALTH <id> # backend health snapshot
ADMINALLOWLIST # list admin allowlist IPs
ADMINALLOW <ip> # add admin allowlist IP
ADMINDENY <ip> # remove admin allowlist IP
ADMINSET <ip1,ip2,...> # replace admin allowlist
SAVE # persist runtime config to disk
RELOAD # reload config from disk
READONLY ON|OFF # toggle global readonly mode
STAGE <key> <value> # stage admin config change
APPLY # apply staged admin config changes
ROLLBACK # rollback most recent APPLY
TOKENADD <label> <ro|rw> <ttl_sec> [max_uses] <token> # register scoped service token
TOKENLIST # list active service tokens (without secret)
TOKENDEL <id> # revoke service token
ACCESSGRANT <ip> [ttl] # temporary allowlist grant for admin plane
ACCESSGRANTS # list temporary allowlist grants
ACCESSREVOKE <ip> # revoke temporary allowlist grant
SETRATE <id> <bps> # set per-rule byte rate limit
SETMAXCONNS <id> <n> # set per-rule concurrent connection cap
SETGLOBALMAXCONNS <n> # set global concurrent connection cap
EMERGENCY ON|OFF # immediate kill-switch for new dataplane accepts
AUDITVERIFY # verify tamper-evident audit chain
TLSINFO # show TLS + admin security posture
BLOCK <ip> [seconds] # block IP
UNBLOCK <ip> # unblock IP
LOGS [n] # recent security events
METRICS # prometheus format
QUIT # close connection
# add rule with 3 backends, round-robin
ADD 443 10.0.0.1:8080,10.0.0.2:8080,10.0.0.3:8080 rr
# add rule with least-connections
ADD 80 web1:8080,web2:8080 lc
# add sticky sessions (IP hash)
ADD 3000 app1:3000,app2:3000 ip
# block abusive IP for 1 hour
BLOCK 1.2.3.4 3600
# drain backend 1 on rule 12345 with 20s timeout
DRAIN 12345 1 20
# pause and resume rule traffic admission
PAUSE 12345
RESUME 12345-b, --admin-bind <ip> Control bind address (default: 127.0.0.1)
-p, --admin-port <n> Control port (default: 8081)
-t, --token <str> Auth token (recommended)
-r, --readonly Start in readonly admin mode
-a, --admin-allow <ips> Comma-separated admin IP allowlist
-m, --mtls Require admin client certificate
-A, --ca <path> Client CA bundle for mTLS
-c, --cert <path> TLS certificate
-k, --key <path> TLS private key
-d, --daemon Run as background daemon
-h, --help Show help
- Admin API supports IP allowlist (
admin.allowlist/--admin-allow). - Admin API supports TLS and optional client certificate enforcement (mTLS, optional
admin.ca/--ca). - Admin API supports readonly sessions (
readonly_token) and global readonly mode. - Admin API supports brute-force lockout and command rate-limits (
auth_fail_threshold,auth_lockout_sec,max_cmds_per_min). - Admin API supports dual-control flow:
STAGE->APPLYwithROLLBACKsafety. - Audit events are persisted as JSON lines with
prev_hashandhashfor tamper-evident chaining (admin.audit_log). - Admin API supports short-lived service tokens (
TOKENADD/TOKENDEL) with scoped role (ro|rw), TTL, and optional max-uses. - Admin API supports temporary just-in-time access grants by source IP (
ACCESSGRANT/ACCESSREVOKE) for least-privilege operations. - Admin config supports
service_token_max_ttl_secandtemp_grant_max_ttl_secto enforce hard caps on temporary credentials. - Unknown or invalid allowlist IPs are rejected from CLI and ignored with warnings in config parsing.
From self-hosted community pain points (cost/lock-in, repeated MFA prompts, and temporary collaboration access), SPF now includes:
- Short-lived machine/service credentials (service tokens with expiry + use caps).
- JIT operator access grants (temporary IP grants with explicit TTL).
- Operator-tunable session windows via staged config and safer rollback controls.
This gives teams several capabilities typically bundled in paid tunnel/control platforms, while keeping deployment self-hostable.
- Emergency kill-switch (
EMERGENCY ON|OFF) for instant containment. - Per-rule and global connection caps (
SETMAXCONNS,SETGLOBALMAXCONNS) for blast-radius control. - Live rule rate tuning (
SETRATE) for abuse throttling without restarts. - Audit chain verification (
AUDITVERIFY) to detect tampering. - Expanded security telemetry counters for failed/sensitive/unknown commands and backend TLS failure classes.
Per-backend TLS to upstream targets supports:
backend_tls = truefor encrypted upstream transport.backend_tls_verify = truefor certificate validation + hostname checks.backend_tls_cato set trust roots for upstream verification.backend_tls_snifor explicit SNI/hostname validation target.backend_tls_pin_sha256for SHA-256 DER pin validation.
Use this to enforce zero-trust upstream identity checks when forwarding to remote/private backends.
# address sanitizer build
make asan
# undefined behavior sanitizer build
make ubsan
# both sanitizer builds
make sanitizers
# build control parser fuzz harness
make fuzz-ctrlCI runs sanitizer jobs and fuzz harness build in .github/workflows/sanitizers.yml.
| Algo | Flag | Description |
|---|---|---|
| Round Robin | rr |
Default, rotate through backends |
| Least Connections | lc |
Route to least busy backend |
| IP Hash | ip |
Sticky sessions by client IP |
| Weighted | w |
Weighted distribution |
SPF logs these security events:
| Event | Description |
|---|---|
CONN_OPEN |
New connection established |
CONN_CLOSE |
Connection closed |
AUTH_FAIL |
Failed authentication attempt |
BLOCKED |
IP blocked (rate limit) |
RATE_LIMITED |
Request rate limited |
HEALTH_DOWN |
Backend failed health check |
HEALTH_UP |
Backend recovered |
GEOBLOCK |
Blocked by geo-IP |
THREAT_MATCH |
IP matched threat intel |
ANOMALY |
Unusual traffic pattern |
DDOS |
Potential DDoS detected |
spf_connections_active # current connections
spf_connections_total # total since start
spf_bytes_in_total # bytes received
spf_bytes_out_total # bytes sent
spf_blocked_total # blocked IPs
spf_rules_active # active rules
spf_admin_service_token_auth_success_total
spf_admin_service_token_auth_fail_total
spf_admin_temp_grants_created_total
spf_admin_failed_commands_total
spf_admin_unknown_commands_total
spf_admin_sensitive_commands_total
spf_backend_tls_handshake_failures_total
spf_backend_tls_pin_failures_total
spf_backend_connect_timeouts_total
spf_conn_reject_emergency_total
spf_conn_reject_rule_max_total
spf_conn_reject_global_max_total
spf_audit_verify_failures_total
spf_global_max_conns
spf_emergency_mode
SPF runs on ESP32 for edge/IoT scenarios:
# configure via serial first boot
SETUP YourSSID YourPassword YourAuthToken
# then control via network
nc 192.168.1.x 8081Credentials stored in NVS flash - no hardcoded secrets.
src/
├── common.h # shared types and limits
├── core.c # state, blocking, load balancing
├── server.cpp # main server (linux/mac/win)
└── esp32.cpp # embedded variant
| Feature | SPF | socat | rinetd | HAProxy | nginx |
|---|---|---|---|---|---|
| Dynamic rules | ✅ | ❌ | ❌ | ✅ | |
| Load balancing | ✅ | ❌ | ❌ | ✅ | ✅ |
| Health checks | ✅ | ❌ | ❌ | ✅ | ✅ |
| TLS | ✅ | ✅ | ❌ | ✅ | ✅ |
| SIEM/Security | ✅ | ❌ | ❌ | ||
| ESP32/IoT | ✅ | ❌ | ❌ | ❌ | ❌ |
| Binary size | ~50KB | ~500KB | ~20KB | ~2MB | ~5MB |
- minimal footprint and setup time for dynamic L4 forwarding
- strong control-plane guardrails for small teams/self-hosters
- practical security + observability in one binary
- massive plugin/ecosystem depth
- advanced L7 traffic policy and enterprise integrations
- long-standing production adoption at hyperscale
# release
make
# debug with sanitizers
make debug
# cross compile
make cross-arm
make cross-aarch64
make cross-windows
# info
make info
# build + smoke integration tests
make test
# run documented CLI workflow tests explicitly
make test-cliGPL-2.0