Examples

Configs for common deployments, from examples/. Run one with dvarapala serve --config examples/<file>. Names, paths and addresses are placeholders. Every key is in Configuration.

dvarapala.toml

Minimal reverse proxy with the default rate limit. Every key can be overridden by DVARAPALA__<SECTION>__<KEY>, for example DVARAPALA__RATE_LIMIT__RATE=5.

# Minimal reverse proxy with the default rate limit.
# Every key can be overridden by DVARAPALA__<SECTION>__<KEY>, for example
# DVARAPALA__RATE_LIMIT__RATE=5.

# "forward" runs an egress forward proxy instead, see forward-proxy.toml.
# mode = "reverse"
listen = "0.0.0.0:8080"
upstream = "http://127.0.0.1:8081"
# "http2" for gRPC upstreams (h2c, or ALPN h2 over https://); "auto" lets
# ALPN choose for https://. Top-level key: keep it above the first section.
# upstream_protocol = "http1"

# Only peers in these blocks may set X-Forwarded-For. Top-level key: keep it
# above the first [section].
# trusted_proxies = ["192.0.2.0/24"]

[metrics]
listen = "127.0.0.1:9090"

[rate_limit]
enabled = true
rate = 20
burst = 40
max_clients = 100000

# Adaptive detection (RFC 0002). Starts in observe mode: logs and metrics
# only. Watch dvarapala_detection_* settle before switching to "enforce".
[detection]
enabled = true
mode = "observe"
window = "1s"
# Group sources by prefix; shorten to 24 / 48 to catch floods spread over
# a subnet, at the cost of dropping its neighbors too.
prefix_v4 = 32
prefix_v6 = 64

# HTTPS on the listener (RFC 0003): set both to turn TLS on. SIGHUP reloads
# the pair; a bad file keeps the old one.
# [tls]
# cert = "/etc/dvarapala/cert.pem"
# key = "/etc/dvarapala/key.pem"
# handshake_timeout = "10s"
# max_handshakes = 1024

# Verification of https:// upstreams. Default: the system roots and the
# upstream host name. There is no switch to turn verification off.
# [upstream_tls]
# ca_file = "/etc/dvarapala/upstream-ca.pem"
# server_name = "app.internal"

# HTTP/2 listener limits (RFC 0004), on by default. Each bounds what one
# client connection can hold or burn.
# [http2]
# max_concurrent_streams = 100
# max_pending_reset_streams = 20
# max_local_error_resets = 1024
# max_header_list_size = 16384
# keepalive_interval = "30s"
# keepalive_timeout = "10s"

# WebSocket upgrades (RFC 0005), on by default. Each tunnel holds about
# 19 KB; protocols = [] turns upgrades off.
# [upgrade]
# protocols = ["websocket"]
# max_tunnels = 10000
# idle_timeout = "300s"
# max_lifetime = "24h"

forward-proxy.toml

Egress rate compliance (RFC 0007): our services call third-party APIs through this proxy, which keeps each provider’s agreed rate and concurrency for all callers together. Callers set HTTP_PROXY (or their client’s proxy setting) to the listen address.

Why these values: provider-a allows 50 requests per second with 20 in flight, so we queue up to 3 s instead of failing fast; provider-b is a slow batch API with 1 request per second and no queue: callers get a 429 at once and retry on their own schedule.

# Egress rate compliance (RFC 0007): our services call third-party APIs
# through this proxy, which keeps each provider's agreed rate and
# concurrency for all callers together. Callers set HTTP_PROXY (or their
# client's proxy setting) to the listen address.
#
# Why these values: provider-a allows 50 requests per second with 20 in
# flight, so we queue up to 3 s instead of failing fast; provider-b is a
# slow batch API with 1 request per second and no queue: callers get a
# 429 at once and retry on their own schedule.

mode = "forward"
# Only reachable from the services' network: the proxy can reach listed
# destinations only, but it is still an exit to them.
listen = "0.0.0.0:3128"

[metrics]
listen = "127.0.0.1:9090"

[forward]
clients = ["192.0.2.0/24"]
# Clients send https:// URLs as CONNECT tunnels: one slot per tunnel. Turn
# it off to require absolute-form requests, which are shaped per request.
connect = true

[[forward.destinations]]
name = "provider-a"
origin = "https://api.provider-a.example"
rate = 50
burst = 10
max_concurrent = 20
max_queue = 500
max_wait = "3s"
# Honor the provider's Retry-After for at most a minute.
max_pause = "60s"

[[forward.destinations]]
name = "provider-b"
origin = "https://batch.provider-b.example"
rate = 1
max_queue = 0

# CONNECT tunnels use the [upgrade] limits.
[upgrade]
max_tunnels = 1000
idle_timeout = "60s"

grpc.toml

Reverse proxy for a gRPC service.

Use case: gRPC clients (mobile apps, partner integrations, internal services) calling a public API. gRPC needs HTTP/2 to the upstream and trailers end to end. Local errors come back in gRPC form, so a throttled client sees RESOURCE_EXHAUSTED (8) with a retry pushback instead of an HTTP 429 its stub cannot read, and a dead upstream is UNAVAILABLE (14).

The HTTP/2 listener limits are the defense against rapid reset and header floods; tighten max_concurrent_streams when each call is expensive.

# Reverse proxy for a gRPC service.
#
# Use case: gRPC clients (mobile apps, partner integrations, internal
# services) calling a public API. gRPC needs HTTP/2 to the upstream and
# trailers end to end. Local errors come back in gRPC form, so a throttled
# client sees RESOURCE_EXHAUSTED (8) with a retry pushback instead of an
# HTTP 429 its stub cannot read, and a dead upstream is UNAVAILABLE (14).
#
# The HTTP/2 listener limits are the defense against rapid reset and header
# floods; tighten max_concurrent_streams when each call is expensive.

listen = "0.0.0.0:8443"
upstream = "http://127.0.0.1:50051"
# h2c with prior knowledge to a cleartext gRPC server; with an https://
# upstream, ALPN h2.
upstream_protocol = "http2"

[metrics]
listen = "127.0.0.1:9090"

# Streams count as requests: a busy streaming client needs a larger burst
# than a browser.
[rate_limit]
rate = 50
burst = 200

[detection]
mode = "observe"

[tls]
cert = "/etc/dvarapala/fullchain.pem"
key = "/etc/dvarapala/privkey.pem"

[http2]
max_concurrent_streams = 50
max_pending_reset_streams = 20
keepalive_interval = "30s"
keepalive_timeout = "10s"

reverse-proxy-tls.toml

Reverse proxy with TLS on both sides.

Use case: a public HTTPS endpoint in front of a web app or API. dvarapala terminates TLS, so the rate limit and detection see every request in the clear, then opens a new verified TLS connection to the upstream. Bounded handshakes keep a TLS flood (the most expensive thing a client can ask for) from eating the CPU before any request is read.

Reload the certificate after a renewal with SIGHUP; a bad pair keeps the old one. Placeholders: <cert dir>, app.example.

# Reverse proxy with TLS on both sides.
#
# Use case: a public HTTPS endpoint in front of a web app or API. dvarapala
# terminates TLS, so the rate limit and detection see every request in the
# clear, then opens a new verified TLS connection to the upstream. Bounded
# handshakes keep a TLS flood (the most expensive thing a client can ask
# for) from eating the CPU before any request is read.
#
# Reload the certificate after a renewal with SIGHUP; a bad pair keeps the
# old one. Placeholders: <cert dir>, app.example.

listen = "0.0.0.0:443"
upstream = "https://app.example:8443"
# Clients arrive through a load balancer in this block: trust its
# X-Forwarded-For, key everyone else by their own address.
trusted_proxies = ["192.0.2.0/24"]

[metrics]
listen = "127.0.0.1:9090"

[rate_limit]
rate = 20
burst = 40

[detection]
mode = "observe"

[tls]
cert = "/etc/dvarapala/<cert dir>/fullchain.pem"
key = "/etc/dvarapala/<cert dir>/privkey.pem"
handshake_timeout = "5s"
max_handshakes = 512

# The upstream uses a private CA: trust it instead of the system roots.
# Verification cannot be turned off.
[upstream_tls]
ca_file = "/etc/dvarapala/upstream-ca.pem"
server_name = "app.example"

websocket.toml

Reverse proxy for WebSocket traffic.

Use case: chat, live dashboards, notifications. The upgrade request goes through the rate limit and detection like any request, so a reconnect storm is throttled before it reaches the app. Once the upstream answers 101, bytes are copied both ways; the tunnel limits bound what each admitted client can hold: how many tunnels in total, how long one may idle, and how long one may live, so clients reconnect through the current limits after an attack starts.

Each idle tunnel costs about 19 KB: 10000 tunnels is about 200 MB.

# Reverse proxy for WebSocket traffic.
#
# Use case: chat, live dashboards, notifications. The upgrade request goes
# through the rate limit and detection like any request, so a reconnect
# storm is throttled before it reaches the app. Once the upstream answers
# 101, bytes are copied both ways; the tunnel limits bound what each
# admitted client can hold: how many tunnels in total, how long one may
# idle, and how long one may live, so clients reconnect through the
# current limits after an attack starts.
#
# Each idle tunnel costs about 19 KB: 10000 tunnels is about 200 MB.

listen = "0.0.0.0:8080"
# WebSocket needs HTTP/1.1 to the upstream (the default).
upstream = "http://127.0.0.1:3000"

[metrics]
listen = "127.0.0.1:9090"

# One upgrade per reconnect: a low rate is enough, and slows a storm.
[rate_limit]
rate = 5
burst = 10

[detection]
mode = "observe"

[upgrade]
protocols = ["websocket"]
max_tunnels = 10000
# Clients that send pings every 30 s stay well under this.
idle_timeout = "120s"
max_lifetime = "12h"