Files
RustProxy/scripts/performance/checks_policy.py
T
2026-10-17 11:57:16 +02:00

185 lines
9.5 KiB
Python
Executable File

"""Live ACL, authentication, atomic reload and logging regressions."""
import base64
import copy
import json
import re
import time
from common import http, write_json
DENY = {"default": "deny", "rules": []}
ALLOW_LOCAL = {"default": "deny", "rules": [{"network": "127.0.0.1/32", "action": "allow"}]}
def run(check, rust, peer):
port = rust.reverse_port
config = copy.deepcopy(rust.base)
config["host_policies"] = [{"hosts": ["fixture.test"], "acl": DENY}]
rust.apply(config, "policy-global-host")
count = peer.count("GET", "/echo")
s, h, b = http(port, "/echo", headers={"Host": "fixture.test"})
check("acl.global_host_without_vhost_acl", s == 403 and peer.count("GET", "/echo") == count,
status=s, upstream_requests=peer.count("GET", "/echo") - count)
s, h, b = http(port, "/echo", headers={"Host": "localhost"})
check("acl.global_other_host_unchanged", s == 200, status=s)
config = copy.deepcopy(rust.base)
config["vhosts"][0]["acl"] = DENY
rust.apply(config, "policy-vhost-deny")
s, h, b = http(port)
check("acl.vhost_deny", s == 403, status=s)
s, h, b = http(rust.static_port)
check("acl.vhost_isolation", s == 200, status=s)
config["vhosts"][0]["acl"] = ALLOW_LOCAL
rust.apply(config, "policy-vhost-allow")
s, h, b = http(port)
check("acl.hot_enable_allow", s == 200, status=s)
config = copy.deepcopy(rust.base)
config["acl_profiles"] = [{"id": "restricted", "name": "Restricted", "acl": DENY}]
config["vhosts"][0]["acl_profile_id"] = "restricted"
config["vhosts"][0]["acl"] = ALLOW_LOCAL
rust.apply(config, "policy-profile-deny")
s, h, b = http(port)
check("acl.profile_and_vhost_stack", s == 403, status=s)
config = copy.deepcopy(rust.base)
config["vhosts"][0]["routes"] = [{"id": "denied", "path_prefix": "/echo", "acl": DENY}]
rust.apply(config, "policy-location-deny")
count = peer.count("GET", "/echo")
s, h, b = http(port, "/echo")
check("acl.location_before_upstream", s == 403 and peer.count("GET", "/echo") == count, status=s)
s, h, b = http(port)
check("acl.location_other_path_unchanged", s == 200, status=s)
config["vhosts"][0]["acl"] = DENY
config["vhosts"][0]["routes"][0]["acl"] = ALLOW_LOCAL
rust.apply(config, "policy-location-allow-vhost-deny")
s, h, b = http(port, "/echo")
check("acl.location_cannot_override_vhost_deny", s == 403, status=s)
config = copy.deepcopy(rust.base)
config["vhosts"][0]["acl"] = {"default": "deny", "rules": [{"network": "203.0.113.22/32", "action": "allow"}]}
rust.apply(config, "policy-untrusted-forwarded")
s, h, b = http(port, headers={"X-Forwarded-For": "203.0.113.22", "X-Real-IP": "203.0.113.22"})
check("acl.spoofed_forwarded_ip_denied", s == 403, status=s)
config["forwarding"] = {"trusted_proxies": ["127.0.0.1/32"]}
rust.apply(config, "policy-trusted-forwarded")
s, h, b = http(port, headers={"X-Forwarded-For": "203.0.113.22"})
check("acl.trusted_forwarded_ip_allowed", s == 200, status=s)
config = copy.deepcopy(rust.base)
config["vhosts"][0]["rate_limit"] = {"country_access": {"mode": "allow", "countries": ["PL"]}}
config["vhosts"][0]["cloudflare_client_ip"] = True
rust.apply(config, "policy-country")
s, h, b = http(port, headers={"CF-IPCountry": "PL", "CF-Connecting-IP": "203.0.113.22"})
check("acl.untrusted_country_header_cannot_allow", s == 403, status=s)
config = copy.deepcopy(rust.base)
config["vhosts"][0]["method_policy"] = {"mode": "allow", "methods": ["GET", "HEAD"]}
rust.apply(config, "policy-method")
s, h, b = http(port, "/echo", "POST", body=b"denied method")
check("policy.method_allowlist", s in (403, 405), status=s)
s, h, b = http(port)
check("policy.allowed_method_survives", s == 200, status=s)
config = copy.deepcopy(rust.base)
config["vhosts"][0]["rate_limit"] = {"enabled": True, "requests": 2, "window_seconds": 60, "burst": 0}
rust.apply(config, "policy-rate-limit")
statuses = [http(port)[0] for _ in range(3)]
check("policy.rate_limit", statuses[:2] == [200, 200] and statuses[2] == 429, statuses=statuses)
rust.reset()
s, h, b = http(port)
check("policy.hot_disable_restores_request", s == 200, status=s)
# Every route check uses the real admin socket and no bearer/backdoor token.
for path in ["/api/config", "/api/status", "/api/logs", "/api/events", "/api/openapi.json", "/assets/app.js"]:
s, h, b = http(rust.admin_port, path)
check("admin.private." + path, s in (401, 403), status=s)
check("admin.login", rust.login() == 200)
s, h, b = rust.admin("/api/config")
envelope = json.loads(b)
check("admin.authenticated_config", s == 200 and "config" in envelope and "revision" in envelope, status=s)
s, h, b = rust.admin("/api/config", "PUT", {"expected_revision": envelope["revision"], "config": rust.base}, csrf=False)
check("admin.csrf_required", s == 403, status=s)
s, h, b = rust.admin("/api/config", origin="http://foreign.example")
check("admin.foreign_origin_denied", s == 403, status=s)
# Generate the disposable Basic Auth hash through the existing secured API.
s, h, b = rust.admin("/api/basic-auth/hash", "POST", {"username": "fixtureuser", "password": rust.password})
hashed = json.loads(b)
check("basic.hash_api", s == 200 and "password_hash" in hashed, status=s)
config = copy.deepcopy(rust.base)
config["vhosts"][0]["basic_auth"] = {"enabled": True, "require_tls": False,
"users": [{"username": "fixtureuser", "password_hash": hashed["password_hash"]}]}
rust.apply(config, "policy-basic-auth")
s, h, b = http(port, "/echo")
check("basic.challenge", s == 401 and "Basic" in h.get("www-authenticate", ""), status=s)
credentials = base64.b64encode(("fixtureuser:" + rust.password).encode()).decode()
s, h, b = http(port, "/echo", headers={"Authorization": "Basic " + credentials})
check("basic.correct_password_and_strip", s == 200 and "authorization" not in json.loads(b)["headers"], status=s)
s, h, b = http(port, "/echo", headers={"Authorization": "Basic Zml4dHVyZXVzZXI6d3Jvbmc="})
check("basic.wrong_password_denied", s == 401, status=s)
config["vhosts"][0]["acl"] = DENY
rust.apply(config, "policy-acl-before-basic")
s, h, b = http(port, "/echo")
check("basic.acl_precedes_auth", s == 403, status=s)
rust.reset()
config = copy.deepcopy(rust.base)
config["vhosts"][0]["frontend"]["listener_ids"] = ["does-not-exist"]
rust.apply(config, "invalid-config-retains-snapshot", expected_success=False)
s, h, b = http(port)
check("reload.invalid_document_retains_previous_snapshot", s == 200, status=s)
s, h, b = http(rust.static_port)
check("reload.invalid_document_keeps_other_vhost", s == 200, status=s)
config = copy.deepcopy(rust.base)
config["vhosts"][0]["backends"][0]["allow_private"] = False
# Explicit private-address rejection is checked through apply, preserving the
# active configuration if compile-time validation rejects the backend.
rust.apply(config, "ssrf-private-origin-disallowed", expected_success=False)
s, h, b = http(port)
check("ssrf.rejected_origin_does_not_replace_snapshot", s == 200, status=s)
query = "/echo?observability=" + rust.label
s, h, b = http(port, query)
request_id = json.loads(b)["headers"]["x-request-id"]
log = None
for _ in range(20):
ls, lh, lb = rust.admin("/api/logs?vhost=reverse&limit=512")
logs = json.loads(lb)
log = next((entry for entry in logs.get("entries", []) if entry.get("request_id") == request_id), None)
if log:
break
time.sleep(.05)
check("observability.log_fields_and_body_bytes", log is not None and log.get("status") == 200
and log.get("response_bytes") == len(b) and log.get("request_bytes") == 0
and log.get("cache_effective_enabled") is False and log.get("transform_effective_enabled") is False
and bool(re.fullmatch("[0-9a-f]{64}", log.get("request_id", ""))),
recorded_status=log.get("status") if log else None,
recorded_response_bytes=log.get("response_bytes") if log else None, response_bytes=len(b))
if log:
write_json(rust.fixture.results / f"{rust.label}-sample-exchange.json", log)
config = copy.deepcopy(rust.base)
config["diagnostic_headers"] = {"request_id": True}
rust.apply(config, "observability-static-eos")
s, h, b = http(rust.static_port)
static_request_id = h.get("x-request-id")
static_log = None
for _ in range(20):
ls, lh, lb = rust.admin("/api/logs?vhost=static&limit=64")
static_log = next((entry for entry in json.loads(lb).get("entries", [])
if entry.get("request_id") == static_request_id), None)
if static_log:
break
time.sleep(.05)
check("observability.static_complete_after_content_length", s == 200 and static_log is not None
and static_log.get("outcome") == "complete" and static_log.get("response_bytes") == len(b),
outcome=static_log.get("outcome") if static_log else None,
recorded_response_bytes=static_log.get("response_bytes") if static_log else None,
response_bytes=len(b))
if static_log:
write_json(rust.fixture.results / f"{rust.label}-sample-static-exchange.json", static_log)
s, h, b = rust.admin("/api/status")
status = json.loads(b)
write_json(rust.fixture.results / f"{rust.label}-runtime-status.json", status)
check("observability.status_available", s == 200, status=s)