OnionGuard is an open-source, sovereign, self-hosted admission-control engine designed specifically for the threat model of anonymous services.
Go
0
6 commits
updated Sep 25, 2026
Production-grade, zero-trust HTTP admission-control engine for anonymous services & Tor Onion Services.
Key Features • Architecture • Installation • Quick Start • Configuration • Production Readiness • Tor Deployment • Examples
Traditional web application firewalls (Cloudflare, AWS WAF, Akamai, reCAPTCHA) rely on client IP reputation, TLS fingerprinting, and heavy JavaScript challenges. In Tor Onion Services (.onion), these mechanisms are completely ineffective and harmful:
OnionGuard is an open-source, sovereign, self-hosted admission-control engine designed specifically for the threat model of anonymous services. It enforces progressive access control, proof-of-patience wait rooms, server-rendered zero-JavaScript CAPTCHAs, cryptographic sessions, and distributed rate limiting without ever relying on client IP addresses.
Authenticated Principal -> API Bearer Token -> Active Session -> New Visitor). Structurally ignores RemoteAddr, X-Forwarded-For, and X-Real-IP.EvaluateFresh), slashing Redis overhead by 66%.<meta http-equiv="refresh"> proof-of-patience wait room. Works flawlessly in Tor Browser Safest mode.crypto/rand), atomic rotation under distributed lock, automatic stale cookie purging (MaxAge: -1), and absolute lifetime ceilings.go-redis/v9). Fiber v2 adapter is isolated in its own sub-module (github.com/ihatemyfcklife/onionguard/middleware/fiber).MaxAttempts) are automatically demoted back to StateWaiting with reset wait timers.MetricsObserver) and Kubernetes readiness probe support (Ping(ctx)). ┌─────────────────────────────────────────────┐
│ HTTP Requests (Tor / Clear) │
└──────────────────────┬──────────────────────┘
│
┌──────────────────────────────┴──────────────────────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ net/http Middleware │ │ Fiber v2 Submodule │
│ - onionguard/middleware │ │ - middleware/fiber │
│ - MaxBytesReader │ │ - Zero fiber.Ctx leak │
│ - Security Headers │ │ - Isolated dependencies │
└────────────┬─────────────┘ └────────────┬─────────────┘
└──────────────────────┬─────────────────────────────────┘
▼
┌──────────────────────────────┐
│ OnionGuard Engine │
│ - ResolveWithSession (1x) │
│ - AuthorizeRequest │
│ - EvaluateFresh │
└──────────────┬───────────────┘
│
┌────────────┬───────────────┼───────────────┬────────────┐
▼ ▼ ▼ ▼ ▼
Identity Session & Wait Room & Rate Limiter Security
Model Admission CAPTCHA Engine (Token Bucket) Headers
(4 Tiers) (7 States) (Zero-JS PNG) (Multi-Scope) (CSP, no-store)
│
▼
┌──────────────────────────────┐
│ Store Interface │
└──────────────┬───────────────┘
│
┌───────────────────┴───────────────────┐
▼ ▼
MemoryStore RedisStore
(Bounded, Janitor) (go-redis/v9, Lua Scripts)
net/http)For pure Go standard library projects:
go get github.com/ihatemyfcklife/onionguard
Zero external runtime dependencies when using MemoryStore.
If your application uses GoFiber v2, import the isolated submodule:
go get github.com/ihatemyfcklife/onionguard
go get github.com/ihatemyfcklife/onionguard/middleware/fiber
Requires Go 1.22+.
net/http)package main
import (
"log"
"net/http"
"time"
og "github.com/ihatemyfcklife/onionguard"
"github.com/ihatemyfcklife/onionguard/middleware"
)
func main() {
cfg := og.DefaultConfig()
cfg.WaitRoom.Enabled = true
cfg.WaitRoom.WaitTime = 5 * time.Second
cfg.Captcha.Enabled = true
engine, err := og.New(cfg)
if err != nil {
log.Fatalf("failed to initialize onionguard: %v", err)
}
defer engine.Close()
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Retrieve resolved identity from context
if id, ok := og.ClientIdentityFromContext(r.Context()); ok {
log.Printf("Admitted client: kind=%s, principal=%s", id.Kind, id.PrincipalID)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte("Access granted: Welcome to OnionGuard protected service!\n"))
})
// Wrap handler with OnionGuard middleware
handler := middleware.Middleware(engine)(mux)
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 16 * 1024,
}
log.Printf("Server listening on http://localhost:8080")
log.Fatal(server.ListenAndServe())
}
package main
import (
"log"
"github.com/gofiber/fiber/v2"
og "github.com/ihatemyfcklife/onionguard"
ogfiber "github.com/ihatemyfcklife/onionguard/middleware/fiber"
)
func main() {
cfg := og.DefaultConfig()
engine, err := og.New(cfg)
if err != nil {
log.Fatal(err)
}
defer engine.Close()
app := fiber.New(fiber.Config{
BodyLimit: int(cfg.MaxBodyBytes),
})
// Attach OnionGuard Fiber middleware
app.Use(ogfiber.Middleware(engine))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("onionguard: admitted\n")
})
log.Fatal(app.Listen(":8081"))
}
og.DefaultConfig() provides secure, battle-tested defaults:
| Setting | Default Value | Description |
|---|---|---|
SessionTTL | 24 * time.Hour | Sliding expiration duration for active sessions |
SessionAbsoluteLifetime | 7 * 24 * time.Hour | Hard ceiling for any session, regardless of activity |
MaxRenewals | 10 | Maximum number of session rotations before re-admission |
MaxConcurrentSessions | 10000 | Global concurrent session capacity bound (anti-resource exhaustion) |
WaitRoom.Enabled | true | Enables proof-of-patience queue |
WaitRoom.WaitTime | 5 * time.Second | Server-enforced delay before challenge or admission |
Captcha.Enabled | true | Enables zero-JS bitmap challenge |
Captcha.Length | 5 | Length of generated challenge string |
Captcha.TTL | 3 * time.Minute | Lifetime of an unconsumed challenge |
Captcha.MaxAttempts | 3 | Maximum failed guesses before expulsion to wait room |
MaxBodyBytes | 102400 (100 KiB) | HTTP request body sanity cap (returns 413 Payload Too Large) |
CookieSecure | false | Set false for native Tor .onion HTTP; true if behind TLS |
CookieHTTPOnly | true | Prevents cookie extraction via client-side scripts |
CookieSameSite | SameSiteLaxMode | Cross-site request protection |
AllowURLToken | false | Forbids API tokens in URL query params (Invariant 22) |
RedisFailClosed | true | Halts traffic (HTTP 503) if Redis becomes unreachable |
StoreTypeMemory)Ideal for standalone servers, testing, and single-instance Onion Services.
sync.RWMutex), zero external dependencies.cfg := og.DefaultConfig()
cfg.StoreConfig.Type = og.StoreTypeMemory
cfg.StoreConfig.MaxEntries = 50000
cfg.StoreConfig.CleanupInterval = 30 * time.Second
StoreTypeRedis)Mandatory for high-availability clusters and load-balanced Onion Services.
github.com/redis/go-redis/v9.rediss://), and Unix sockets (unix://).cfg := og.DefaultConfig()
cfg.StoreConfig.Type = og.StoreTypeRedis
cfg.StoreConfig.RedisAddr = "redis://:securepassword@127.0.0.1:6379/0"
cfg.StoreConfig.RedisFailClosed = true // Secure default: fail closed on outage
OnionGuard enforces an explicit 4-tier identity hierarchy. Rate-limit buckets never use IP addresses:
[ Incoming Request ]
│
├─► Priority 1: Application Principal (e.g., mTLS cert, upstream auth)
│ └── IdentityAuthenticated ──► Rate Limit Key: "auth:<principal>"
│
├─► Priority 2: Authorization Header (Bearer <token>)
│ └── IdentityAPIToken ──► Rate Limit Key: "token:<sha256[:8]>"
│
├─► Priority 3: Valid Session Cookie
│ └── IdentityAnonymous ──► Rate Limit Key: "anon:sid:<sha256[:8]>"
│
└─► Priority 4: No Session / Invalid Cookie
└── IdentityAnonymousNew ──► Rate Limit Key: "anon_new:global"
ScopeFirstContact)To prevent attackers from causing denial-of-service against new legitimate visitors by exhausting token buckets, unassigned visitors share the ScopeFirstContact quota:
cfg.RateLimit.Limits[og.ScopeFirstContact] = og.RateLimitRule{
Rate: 100, // 100 new visitor handshakes / sec
Burst: 200,
Cost: 1,
Window: 1 * time.Minute,
}
OnionGuard generates clean, accessible, zero-JavaScript HTML fallbacks for the Wait Room and CAPTCHA. You can customize them via CSS hooks or replace them entirely.
See full example in examples/dark_theme/main.go.
// 1. Customize CAPTCHA Image Colors & Anti-OCR Noise
cfg.Captcha.Visual = og.CaptchaVisualConfig{
BackgroundColor: color.RGBA{R: 18, G: 18, B: 18, A: 255}, // Dark background
TextColor: color.RGBA{R: 0, G: 255, B: 128, A: 255}, // Neon green glyphs
LineColor: color.RGBA{R: 60, G: 60, B: 80, A: 180}, // Noise lines
NoiseLines: 3,
NoiseRatio: 0.015,
JitterPixels: 3,
}
// 2. Custom Wait Room Template (Zero-JS Meta Refresh)
cfg.CustomWaitRoomHTML = func(r *http.Request, retry time.Duration) string {
return fmt.Sprintf(`<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="%d">
<title>Please Wait</title>
<style>body{background:#121212;color:#00ff80;font-family:monospace;display:flex;justify-content:center;align-items:center;height:100vh;}</style>
</head>
<body>
<div style="border:1px solid #00ff80;padding:2rem;">
<h2>Queue Protection</h2>
<p>Admitting session in %d second(s)...</p>
</div>
</body>
</html>`, int(retry.Seconds()), int(retry.Seconds()))
}
OnionGuard is engineered from the ground up for high-threat anonymous environments. The engine has been audited against data races, illegal state transitions, memory exhaustion, and side-channel timing leaks.
TestChallenger_StaticAST_NoClientIPInProductionCode). No production code path reads or processes RemoteAddr, X-Forwarded-For, or client IP headers.-race with high concurrency (60+ concurrent rotation races, 100+ concurrent admissions).subtle.ConstantTimeCompare) prevents side-channel timing attacks.EvaluateFresh), keeping latency minimal over high-RTT Tor circuits.When deploying OnionGuard in production, configure and size the engine according to your traffic patterns:
StoreTypeMemory is safe, thread-safe, and self-cleaning via a background janitor.StoreTypeRedis. In a multi-replica deployment, in-memory state is isolated per pod, meaning sessions and rate limits cannot be shared across nodes.unix:///var/run/redis/redis.sock instead of TCP loopback (127.0.0.1:6379) to eliminate TCP stack overhead and lower latency.MaxConcurrentSessions)10000. For high-traffic services, scale MaxConcurrentSessions to 50000–200000 depending on available Redis RAM (approximately 1 KiB per active session record).maxmemory) and eviction policies are configured properly, and consider setting shorter wait room durations.ScopeFirstContact)ScopeFirstContact bucket (anon_new:global).cfg.RateLimit.Limits[og.ScopeFirstContact] = og.RateLimitRule{
Rate: 250, // 250 new visitor handshakes / sec
Burst: 500,
Cost: 1,
Window: 1 * time.Minute,
}
anon:<session_hash>) and are completely unaffected by floods on ScopeFirstContact.CookieSecure).onion v3: Set CookieSecure: false (default). Tor v3 circuits are end-to-end encrypted; Tor Browser accepts standard cookies over onion HTTP.CookieSecure: true. If your service is accessible via clearnet or behind HTTPS termination (Nginx, Traefik, Cloudflare), CookieSecure: true is mandatory to prevent session tokens from being exposed over plaintext connections.torrc:
# Enable Tor v3 protocol-level Proof-of-Work defense
HiddenServicePoWDefensesEnabled 1
HiddenServicePoWQueueRate 250
HiddenServicePoWQueueBurst 500
Combining Tor v3 protocol-level PoW (Layer 4) with OnionGuard (Layer 7) provides an impenetrable defense stack for anonymous services.cfg.Captcha.Visual.CustomGenerator = func(answer string, width, height int) ([]byte, error) {
// Return high-entropy vector PNG with custom TTF distortion or proof-of-work puzzle
return renderCustomAdvancedCAPTCHA(answer, width, height)
}
RedisFailClosed: true (default) ensures that an outage in the storage layer will not allow unauthorized traffic to bypass admission controls.CircuitBreaker fast-fails requests in <1µs with 503 Service Unavailable when storage is unreachable, preventing thread pool starvation and socket exhaustion.When deploying OnionGuard behind a Tor daemon as an Onion Service:
torrc ConfigurationConfigure Tor to forward traffic to OnionGuard and enable native PoW defenses:
HiddenServiceDir /var/lib/tor/my_onion_service/
HiddenServicePort 80 127.0.0.1:8080
HiddenServiceVersion 3
# Recommended: Tor v3 Layer-4 Proof-of-Work defense against introduction cell flooding
HiddenServicePoWDefensesEnabled 1
HiddenServicePoWQueueRate 250
HiddenServicePoWQueueBurst 500
.onion.onion v3: Traffic between the Tor client and your server is end-to-end encrypted by Tor. Setting CookieSecure: false is safe and standard.CookieSecure: true.[ Tor Network ]
│
[ Tor Daemon (v3 Onion Service + Layer 4 PoW) ]
│ (127.0.0.1 / unix domain socket)
[ OnionGuard Admission Middleware (Layer 7 FSM & CAPTCHA) ]
│ (Filters DDoS, scrapers, floods, wait-room)
[ Your Backend Application Handlers ]
Ping)http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
if err := engine.Ping(r.Context()); err != nil {
http.Error(w, "store unhealthy", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
Implement og.MetricsObserver to export operational metrics without leaking user sessions or correlation data:
type MetricsCollector struct{}
func (m *MetricsCollector) OnRequestAdmitted(k og.IdentityKind) { /* prometheus counter */ }
func (m *MetricsCollector) OnWaitRoomQueued(d time.Duration) { /* prometheus histogram */ }
func (m *MetricsCollector) OnChallengeIssued() { /* prometheus counter */ }
func (m *MetricsCollector) OnChallengeSolved() { /* prometheus counter */ }
func (m *MetricsCollector) OnChallengeFailed() { /* prometheus counter */ }
func (m *MetricsCollector) OnRateLimited(k og.IdentityKind) { /* prometheus counter */ }
engine, err := og.New(cfg, og.WithMetricsObserver(&MetricsCollector{}))
OnionGuard maps internal states to RFC-compliant HTTP status codes. All client responses are stripped of internal errors, stack traces, and database connection details:
| Status Code | Error Code | Client Message / Meaning |
|---|---|---|
400 Bad Request | INVALID_INPUT / INVALID_TRANSITION | Malformed request body or illegal state transition |
401 Unauthorized | UNAUTHORIZED | API Token validation failed (no fallback to anonymous) |
403 Forbidden | CHALLENGE_REQUIRED / SESSION_INVALID | Session expired, revoked, or CAPTCHA required |
413 Payload Too Large | PAYLOAD_TOO_LARGE | Request payload exceeds MaxBodyBytes |
429 Too Many Requests | RATE_LIMITED / WAIT_ROOM | Rate limit quota exceeded or wait room in progress (Retry-After header sent) |
503 Unavailable | SERVICE_UNAVAILABLE | Storage failure in RedisFailClosed mode |
The codebase undergoes rigorous verification including unit testing, data-race detection, transition matrix fuzzing, and Redis outage simulation:
# 1. Run all tests with Go Race Detector
go test -count=1 -race ./...
# 2. Run isolated Fiber middleware tests
(cd middleware/fiber && go test -count=1 -race ./...)
# 3. Execute Native Go Fuzz Targets
go test -run=^$ -fuzz=FuzzValidateSessionID -fuzztime=30s .
go test -run=^$ -fuzz=FuzzTokenExtraction -fuzztime=30s .
go test -run=^$ -fuzz=FuzzChallengeAnswerHash -fuzztime=30s .
# 4. Redis Integration Tests (requires running Redis instance)
export ONIONGUARD_REDIS_ADDR=127.0.0.1:6379
go test -v -run TestRedis ./store
Looking for a complete, production-ready implementation? Check out the OnionGuard Filehost Template:
Explore the runnable server examples included directly in this repository:
examples/std_server/ — Standard net/http server with default settings.examples/fiber_server/ — High-performance GoFiber v2 integration.examples/dark_theme/ — Terminal dark-theme UI with custom neon CAPTCHA styling.Contributions, issues, and security suggestions are welcome on GitHub!
git checkout -b feature/defense-enhancement).go test -race ./...).git commit -m 'Add defense enhancement').git push origin feature/defense-enhancement).OnionGuard is open-source software licensed under the Apache License, Version 2.0.
6 commits
Go
100.0%
OnionGuard is an open-source, sovereign, self-hosted admission-control engine designed specifically for the threat model of anonymous services.
Go
0
6 commits
updated Sep 25, 2026
Production-grade, zero-trust HTTP admission-control engine for anonymous services & Tor Onion Services.
Key Features • Architecture • Installation • Quick Start • Configuration • Production Readiness • Tor Deployment • Examples
Traditional web application firewalls (Cloudflare, AWS WAF, Akamai, reCAPTCHA) rely on client IP reputation, TLS fingerprinting, and heavy JavaScript challenges. In Tor Onion Services (.onion), these mechanisms are completely ineffective and harmful:
OnionGuard is an open-source, sovereign, self-hosted admission-control engine designed specifically for the threat model of anonymous services. It enforces progressive access control, proof-of-patience wait rooms, server-rendered zero-JavaScript CAPTCHAs, cryptographic sessions, and distributed rate limiting without ever relying on client IP addresses.
Authenticated Principal -> API Bearer Token -> Active Session -> New Visitor). Structurally ignores RemoteAddr, X-Forwarded-For, and X-Real-IP.EvaluateFresh), slashing Redis overhead by 66%.<meta http-equiv="refresh"> proof-of-patience wait room. Works flawlessly in Tor Browser Safest mode.crypto/rand), atomic rotation under distributed lock, automatic stale cookie purging (MaxAge: -1), and absolute lifetime ceilings.go-redis/v9). Fiber v2 adapter is isolated in its own sub-module (github.com/ihatemyfcklife/onionguard/middleware/fiber).MaxAttempts) are automatically demoted back to StateWaiting with reset wait timers.MetricsObserver) and Kubernetes readiness probe support (Ping(ctx)). ┌─────────────────────────────────────────────┐
│ HTTP Requests (Tor / Clear) │
└──────────────────────┬──────────────────────┘
│
┌──────────────────────────────┴──────────────────────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ net/http Middleware │ │ Fiber v2 Submodule │
│ - onionguard/middleware │ │ - middleware/fiber │
│ - MaxBytesReader │ │ - Zero fiber.Ctx leak │
│ - Security Headers │ │ - Isolated dependencies │
└────────────┬─────────────┘ └────────────┬─────────────┘
└──────────────────────┬─────────────────────────────────┘
▼
┌──────────────────────────────┐
│ OnionGuard Engine │
│ - ResolveWithSession (1x) │
│ - AuthorizeRequest │
│ - EvaluateFresh │
└──────────────┬───────────────┘
│
┌────────────┬───────────────┼───────────────┬────────────┐
▼ ▼ ▼ ▼ ▼
Identity Session & Wait Room & Rate Limiter Security
Model Admission CAPTCHA Engine (Token Bucket) Headers
(4 Tiers) (7 States) (Zero-JS PNG) (Multi-Scope) (CSP, no-store)
│
▼
┌──────────────────────────────┐
│ Store Interface │
└──────────────┬───────────────┘
│
┌───────────────────┴───────────────────┐
▼ ▼
MemoryStore RedisStore
(Bounded, Janitor) (go-redis/v9, Lua Scripts)
net/http)For pure Go standard library projects:
go get github.com/ihatemyfcklife/onionguard
Zero external runtime dependencies when using MemoryStore.
If your application uses GoFiber v2, import the isolated submodule:
go get github.com/ihatemyfcklife/onionguard
go get github.com/ihatemyfcklife/onionguard/middleware/fiber
Requires Go 1.22+.
net/http)package main
import (
"log"
"net/http"
"time"
og "github.com/ihatemyfcklife/onionguard"
"github.com/ihatemyfcklife/onionguard/middleware"
)
func main() {
cfg := og.DefaultConfig()
cfg.WaitRoom.Enabled = true
cfg.WaitRoom.WaitTime = 5 * time.Second
cfg.Captcha.Enabled = true
engine, err := og.New(cfg)
if err != nil {
log.Fatalf("failed to initialize onionguard: %v", err)
}
defer engine.Close()
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Retrieve resolved identity from context
if id, ok := og.ClientIdentityFromContext(r.Context()); ok {
log.Printf("Admitted client: kind=%s, principal=%s", id.Kind, id.PrincipalID)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte("Access granted: Welcome to OnionGuard protected service!\n"))
})
// Wrap handler with OnionGuard middleware
handler := middleware.Middleware(engine)(mux)
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 16 * 1024,
}
log.Printf("Server listening on http://localhost:8080")
log.Fatal(server.ListenAndServe())
}
package main
import (
"log"
"github.com/gofiber/fiber/v2"
og "github.com/ihatemyfcklife/onionguard"
ogfiber "github.com/ihatemyfcklife/onionguard/middleware/fiber"
)
func main() {
cfg := og.DefaultConfig()
engine, err := og.New(cfg)
if err != nil {
log.Fatal(err)
}
defer engine.Close()
app := fiber.New(fiber.Config{
BodyLimit: int(cfg.MaxBodyBytes),
})
// Attach OnionGuard Fiber middleware
app.Use(ogfiber.Middleware(engine))
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("onionguard: admitted\n")
})
log.Fatal(app.Listen(":8081"))
}
og.DefaultConfig() provides secure, battle-tested defaults:
| Setting | Default Value | Description |
|---|---|---|
SessionTTL | 24 * time.Hour | Sliding expiration duration for active sessions |
SessionAbsoluteLifetime | 7 * 24 * time.Hour | Hard ceiling for any session, regardless of activity |
MaxRenewals | 10 | Maximum number of session rotations before re-admission |
MaxConcurrentSessions | 10000 | Global concurrent session capacity bound (anti-resource exhaustion) |
WaitRoom.Enabled | true | Enables proof-of-patience queue |
WaitRoom.WaitTime | 5 * time.Second | Server-enforced delay before challenge or admission |
Captcha.Enabled | true | Enables zero-JS bitmap challenge |
Captcha.Length | 5 | Length of generated challenge string |
Captcha.TTL | 3 * time.Minute | Lifetime of an unconsumed challenge |
Captcha.MaxAttempts | 3 | Maximum failed guesses before expulsion to wait room |
MaxBodyBytes | 102400 (100 KiB) | HTTP request body sanity cap (returns 413 Payload Too Large) |
CookieSecure | false | Set false for native Tor .onion HTTP; true if behind TLS |
CookieHTTPOnly | true | Prevents cookie extraction via client-side scripts |
CookieSameSite | SameSiteLaxMode | Cross-site request protection |
AllowURLToken | false | Forbids API tokens in URL query params (Invariant 22) |
RedisFailClosed | true | Halts traffic (HTTP 503) if Redis becomes unreachable |
StoreTypeMemory)Ideal for standalone servers, testing, and single-instance Onion Services.
sync.RWMutex), zero external dependencies.cfg := og.DefaultConfig()
cfg.StoreConfig.Type = og.StoreTypeMemory
cfg.StoreConfig.MaxEntries = 50000
cfg.StoreConfig.CleanupInterval = 30 * time.Second
StoreTypeRedis)Mandatory for high-availability clusters and load-balanced Onion Services.
github.com/redis/go-redis/v9.rediss://), and Unix sockets (unix://).cfg := og.DefaultConfig()
cfg.StoreConfig.Type = og.StoreTypeRedis
cfg.StoreConfig.RedisAddr = "redis://:securepassword@127.0.0.1:6379/0"
cfg.StoreConfig.RedisFailClosed = true // Secure default: fail closed on outage
OnionGuard enforces an explicit 4-tier identity hierarchy. Rate-limit buckets never use IP addresses:
[ Incoming Request ]
│
├─► Priority 1: Application Principal (e.g., mTLS cert, upstream auth)
│ └── IdentityAuthenticated ──► Rate Limit Key: "auth:<principal>"
│
├─► Priority 2: Authorization Header (Bearer <token>)
│ └── IdentityAPIToken ──► Rate Limit Key: "token:<sha256[:8]>"
│
├─► Priority 3: Valid Session Cookie
│ └── IdentityAnonymous ──► Rate Limit Key: "anon:sid:<sha256[:8]>"
│
└─► Priority 4: No Session / Invalid Cookie
└── IdentityAnonymousNew ──► Rate Limit Key: "anon_new:global"
ScopeFirstContact)To prevent attackers from causing denial-of-service against new legitimate visitors by exhausting token buckets, unassigned visitors share the ScopeFirstContact quota:
cfg.RateLimit.Limits[og.ScopeFirstContact] = og.RateLimitRule{
Rate: 100, // 100 new visitor handshakes / sec
Burst: 200,
Cost: 1,
Window: 1 * time.Minute,
}
OnionGuard generates clean, accessible, zero-JavaScript HTML fallbacks for the Wait Room and CAPTCHA. You can customize them via CSS hooks or replace them entirely.
See full example in examples/dark_theme/main.go.
// 1. Customize CAPTCHA Image Colors & Anti-OCR Noise
cfg.Captcha.Visual = og.CaptchaVisualConfig{
BackgroundColor: color.RGBA{R: 18, G: 18, B: 18, A: 255}, // Dark background
TextColor: color.RGBA{R: 0, G: 255, B: 128, A: 255}, // Neon green glyphs
LineColor: color.RGBA{R: 60, G: 60, B: 80, A: 180}, // Noise lines
NoiseLines: 3,
NoiseRatio: 0.015,
JitterPixels: 3,
}
// 2. Custom Wait Room Template (Zero-JS Meta Refresh)
cfg.CustomWaitRoomHTML = func(r *http.Request, retry time.Duration) string {
return fmt.Sprintf(`<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="%d">
<title>Please Wait</title>
<style>body{background:#121212;color:#00ff80;font-family:monospace;display:flex;justify-content:center;align-items:center;height:100vh;}</style>
</head>
<body>
<div style="border:1px solid #00ff80;padding:2rem;">
<h2>Queue Protection</h2>
<p>Admitting session in %d second(s)...</p>
</div>
</body>
</html>`, int(retry.Seconds()), int(retry.Seconds()))
}
OnionGuard is engineered from the ground up for high-threat anonymous environments. The engine has been audited against data races, illegal state transitions, memory exhaustion, and side-channel timing leaks.
TestChallenger_StaticAST_NoClientIPInProductionCode). No production code path reads or processes RemoteAddr, X-Forwarded-For, or client IP headers.-race with high concurrency (60+ concurrent rotation races, 100+ concurrent admissions).subtle.ConstantTimeCompare) prevents side-channel timing attacks.EvaluateFresh), keeping latency minimal over high-RTT Tor circuits.When deploying OnionGuard in production, configure and size the engine according to your traffic patterns:
StoreTypeMemory is safe, thread-safe, and self-cleaning via a background janitor.StoreTypeRedis. In a multi-replica deployment, in-memory state is isolated per pod, meaning sessions and rate limits cannot be shared across nodes.unix:///var/run/redis/redis.sock instead of TCP loopback (127.0.0.1:6379) to eliminate TCP stack overhead and lower latency.MaxConcurrentSessions)10000. For high-traffic services, scale MaxConcurrentSessions to 50000–200000 depending on available Redis RAM (approximately 1 KiB per active session record).maxmemory) and eviction policies are configured properly, and consider setting shorter wait room durations.ScopeFirstContact)ScopeFirstContact bucket (anon_new:global).cfg.RateLimit.Limits[og.ScopeFirstContact] = og.RateLimitRule{
Rate: 250, // 250 new visitor handshakes / sec
Burst: 500,
Cost: 1,
Window: 1 * time.Minute,
}
anon:<session_hash>) and are completely unaffected by floods on ScopeFirstContact.CookieSecure).onion v3: Set CookieSecure: false (default). Tor v3 circuits are end-to-end encrypted; Tor Browser accepts standard cookies over onion HTTP.CookieSecure: true. If your service is accessible via clearnet or behind HTTPS termination (Nginx, Traefik, Cloudflare), CookieSecure: true is mandatory to prevent session tokens from being exposed over plaintext connections.torrc:
# Enable Tor v3 protocol-level Proof-of-Work defense
HiddenServicePoWDefensesEnabled 1
HiddenServicePoWQueueRate 250
HiddenServicePoWQueueBurst 500
Combining Tor v3 protocol-level PoW (Layer 4) with OnionGuard (Layer 7) provides an impenetrable defense stack for anonymous services.cfg.Captcha.Visual.CustomGenerator = func(answer string, width, height int) ([]byte, error) {
// Return high-entropy vector PNG with custom TTF distortion or proof-of-work puzzle
return renderCustomAdvancedCAPTCHA(answer, width, height)
}
RedisFailClosed: true (default) ensures that an outage in the storage layer will not allow unauthorized traffic to bypass admission controls.CircuitBreaker fast-fails requests in <1µs with 503 Service Unavailable when storage is unreachable, preventing thread pool starvation and socket exhaustion.When deploying OnionGuard behind a Tor daemon as an Onion Service:
torrc ConfigurationConfigure Tor to forward traffic to OnionGuard and enable native PoW defenses:
HiddenServiceDir /var/lib/tor/my_onion_service/
HiddenServicePort 80 127.0.0.1:8080
HiddenServiceVersion 3
# Recommended: Tor v3 Layer-4 Proof-of-Work defense against introduction cell flooding
HiddenServicePoWDefensesEnabled 1
HiddenServicePoWQueueRate 250
HiddenServicePoWQueueBurst 500
.onion.onion v3: Traffic between the Tor client and your server is end-to-end encrypted by Tor. Setting CookieSecure: false is safe and standard.CookieSecure: true.[ Tor Network ]
│
[ Tor Daemon (v3 Onion Service + Layer 4 PoW) ]
│ (127.0.0.1 / unix domain socket)
[ OnionGuard Admission Middleware (Layer 7 FSM & CAPTCHA) ]
│ (Filters DDoS, scrapers, floods, wait-room)
[ Your Backend Application Handlers ]
Ping)http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
if err := engine.Ping(r.Context()); err != nil {
http.Error(w, "store unhealthy", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
Implement og.MetricsObserver to export operational metrics without leaking user sessions or correlation data:
type MetricsCollector struct{}
func (m *MetricsCollector) OnRequestAdmitted(k og.IdentityKind) { /* prometheus counter */ }
func (m *MetricsCollector) OnWaitRoomQueued(d time.Duration) { /* prometheus histogram */ }
func (m *MetricsCollector) OnChallengeIssued() { /* prometheus counter */ }
func (m *MetricsCollector) OnChallengeSolved() { /* prometheus counter */ }
func (m *MetricsCollector) OnChallengeFailed() { /* prometheus counter */ }
func (m *MetricsCollector) OnRateLimited(k og.IdentityKind) { /* prometheus counter */ }
engine, err := og.New(cfg, og.WithMetricsObserver(&MetricsCollector{}))
OnionGuard maps internal states to RFC-compliant HTTP status codes. All client responses are stripped of internal errors, stack traces, and database connection details:
| Status Code | Error Code | Client Message / Meaning |
|---|---|---|
400 Bad Request | INVALID_INPUT / INVALID_TRANSITION | Malformed request body or illegal state transition |
401 Unauthorized | UNAUTHORIZED | API Token validation failed (no fallback to anonymous) |
403 Forbidden | CHALLENGE_REQUIRED / SESSION_INVALID | Session expired, revoked, or CAPTCHA required |
413 Payload Too Large | PAYLOAD_TOO_LARGE | Request payload exceeds MaxBodyBytes |
429 Too Many Requests | RATE_LIMITED / WAIT_ROOM | Rate limit quota exceeded or wait room in progress (Retry-After header sent) |
503 Unavailable | SERVICE_UNAVAILABLE | Storage failure in RedisFailClosed mode |
The codebase undergoes rigorous verification including unit testing, data-race detection, transition matrix fuzzing, and Redis outage simulation:
# 1. Run all tests with Go Race Detector
go test -count=1 -race ./...
# 2. Run isolated Fiber middleware tests
(cd middleware/fiber && go test -count=1 -race ./...)
# 3. Execute Native Go Fuzz Targets
go test -run=^$ -fuzz=FuzzValidateSessionID -fuzztime=30s .
go test -run=^$ -fuzz=FuzzTokenExtraction -fuzztime=30s .
go test -run=^$ -fuzz=FuzzChallengeAnswerHash -fuzztime=30s .
# 4. Redis Integration Tests (requires running Redis instance)
export ONIONGUARD_REDIS_ADDR=127.0.0.1:6379
go test -v -run TestRedis ./store
Looking for a complete, production-ready implementation? Check out the OnionGuard Filehost Template:
Explore the runnable server examples included directly in this repository:
examples/std_server/ — Standard net/http server with default settings.examples/fiber_server/ — High-performance GoFiber v2 integration.examples/dark_theme/ — Terminal dark-theme UI with custom neon CAPTCHA styling.Contributions, issues, and security suggestions are welcome on GitHub!
git checkout -b feature/defense-enhancement).go test -race ./...).git commit -m 'Add defense enhancement').git push origin feature/defense-enhancement).OnionGuard is open-source software licensed under the Apache License, Version 2.0.
6 commits
Go
100.0%