feat(dns): global upstream forward rate limit with sinkhole fallback

Adds DNS_FORWARD_BUDGET (default 50) and DNS_FORWARD_WINDOW (default 1.0s)
env vars. _can_forward() maintains a rolling deque of upstream call
timestamps; queries that exceed the budget within the window are answered
with the sinkhole (127.x) instead of being forwarded, making the honeypot
ineligible as a sustained amp vector even when real_recursive is enabled.
Rate limit is global (not per-source) so IP-spoofed amplification floods
hit the ceiling regardless of how many source addresses are rotated.
This commit is contained in:
2026-05-21 20:50:20 -04:00
parent e5847b7e1e
commit da2ad7a82a
3 changed files with 94 additions and 2 deletions

View File

@@ -65,6 +65,20 @@ class DNSService(BaseService):
placeholder="8.8.8.8:53",
help="Upstream DNS resolver used when real_recursive is enabled (host:port).",
),
ServiceConfigField(
key="forward_budget",
label="Forward budget (queries/window)",
type="string",
default="50",
help="Maximum upstream forwarding calls allowed within the rate window. Excess queries fall back to sinkhole.",
),
ServiceConfigField(
key="forward_window",
label="Forward budget window (seconds)",
type="string",
default="1.0",
help="Rolling window in seconds for the forward budget counter.",
),
]
def compose_fragment(
@@ -82,7 +96,9 @@ class DNSService(BaseService):
"DNS_NSID": str(cfg.get("nsid", "")),
"DNS_EXTRA_RECORDS": str(cfg.get("extra_records", "")),
"DNS_REAL_RECURSIVE": "true" if cfg.get("real_recursive") else "false",
"DNS_UPSTREAM": str(cfg.get("upstream", "8.8.8.8:53")),
"DNS_UPSTREAM": str(cfg.get("upstream", "8.8.8.8:53")),
"DNS_FORWARD_BUDGET": str(cfg.get("forward_budget", "50")),
"DNS_FORWARD_WINDOW": str(cfg.get("forward_window", "1.0")),
}
if log_target:
env["LOG_TARGET"] = log_target