v0.32.5
This commit is contained in:
1 parent
8b9ca4db44
commit
00218a72df
35 files changed
+784
-864
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
|
||||
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
|
||||
compatibility. Current source version: 0.32.4.
|
||||
compatibility. Current source version: 0.32.5.
|
||||
|
||||
## Delivery
|
||||
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
> Operator-facing release history. Historical engineering notes are kept in
|
||||
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
## 0.32.5
|
||||
|
||||
- Add bounded DNS/DDNS hostnames as **allow-only** client ACL rules in reusable
|
||||
profiles, host policies, vhosts, locations and Docker ACL assignments.
|
||||
- Refresh A/AAAA answers asynchronously every 60 seconds and after config
|
||||
publication; cache immutable, expiring address sets for lock-free request
|
||||
checks. Fail closed on lookup failure and initial unresolved state.
|
||||
- Add IP/CIDR vs DNS/DDNS selection in the ACL profile editor and localized
|
||||
operator guidance; preserve the JSON ACL rule format and previous policies.
|
||||
- Add DNS ACL validation/regression tests. Cargo and real rotation testing
|
||||
remain required on a Rust-enabled staging host.
|
||||
|
||||
## 0.32.4
|
||||
|
||||
- Fix Rust `E0382` in retry construction: clone the upstream URI before
|
||||
|
||||
Generated
+1
-1
@@ -1881,7 +1881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.32.4"
|
||||
version = "0.32.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.32.4"
|
||||
version = "0.32.5"
|
||||
edition = "2021"
|
||||
rust-version = "1.99"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RustProxy 0.32.4
|
||||
# RustProxy 0.32.5
|
||||
|
||||
**0.31.0 panel/cache additions:** browser-negotiated Zstd/Brotli/gzip for HTML, JS and CSS (Settings → Administration); optional HTTP BAN/PURGE restricted to explicit IP/CIDR allowlist (Settings → Cache, disabled by default); System information Refresh button beside the API badge. See [Settings](docs/SETTINGS.md) and [Cache](docs/CACHE.md).
|
||||
|
||||
@@ -7,7 +7,32 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
|
||||
|
||||
**0.31.9 Certificate API management: inspect and edit credential settings without exposing/rotating the current token, or regenerate one existing token with one-time display. Rust/Cargo and live testing required.**
|
||||
|
||||
## What changed in 0.32.4
|
||||
## What changed in 0.32.5
|
||||
|
||||
- **Dynamic DNS/DDNS ACL allow rules:** reusable ACL profiles, host policies,
|
||||
inline vhost/location ACLs, and Docker ACL assignments now accept `allow
|
||||
office.example.ddns.net` in place of a static client IP/CIDR. Existing IP,
|
||||
CIDR and country-based controls retain their previous precedence.
|
||||
- DNS answers (A and AAAA) are refreshed every **60 seconds**, with up to
|
||||
**128 distinct hostnames**, **32 unique addresses per hostname**, **8 parallel
|
||||
lookups** and a **4-second timeout**. A failed/empty/invalid resolution is
|
||||
removed from the allowlist (fail closed); successful entries expire within
|
||||
180 seconds if refresh tasks stall. Initial access is denied until the first
|
||||
successful lookup. No DNS lookup or locks on the proxy request path.
|
||||
- The ACL profile editor now offers an IP/CIDR vs DNS/DDNS selector and
|
||||
explains the allow-only semantics. Existing JSON documents keep the `network`
|
||||
string format; no database migration, extra public route or backend resolver
|
||||
behavior change. Name resolution follows the machine's DNS configuration.
|
||||
- DNS names are **allow-only**: dynamic deny rules are rejected because a DNS
|
||||
outage could otherwise silently remove a security deny. Hostnames must be
|
||||
ASCII FQDNs (punycode for IDNs, no wildcards, URL or ports).
|
||||
- **Security note:** allowing a DDNS hostname delegates address authorization
|
||||
to whoever controls its DNS records. Use trusted domains and keep a
|
||||
default-deny ACL profile; old IPs may remain allowed until the next DNS refresh.
|
||||
- Rust/Cargo compilation and real DNS/address-rotation testing are **pending**
|
||||
before production deployment.
|
||||
|
||||
## Previously in 0.32.4
|
||||
|
||||
- **Follow-up build correction:** a retry request now passes a cloned URI to
|
||||
the HTTP request builder, retaining the original for backend-specific
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# RustProxy roadmap
|
||||
|
||||
## 0.32.5 - Dynamic DNS client ACLs (runtime acceptance pending)
|
||||
|
||||
- [x] Preserve CIDR/IP rules and introduce allow-only FQDNs for client ACLs.
|
||||
- [x] Add bounded A/AAAA periodic resolver, immutable cache, expiration and
|
||||
immediate refresh on published configuration changes.
|
||||
- [x] Add DNS/DDNS editor selector, PL/EN guidance and source/regression tests.
|
||||
- [ ] Compile and test Rust with Cargo; stage live DDNS rotation, DNS outage,
|
||||
mixed IPv4/IPv6 answers, vhosts and trusted-forwarded-client-IP paths.
|
||||
|
||||
## 0.32.4 - Upstream Host/authority parity and Overview gutters (runtime acceptance pending)
|
||||
|
||||
- [x] Remove `preserve_host` H1-only validation and the Auto/H2 UI forced-uncheck.
|
||||
|
||||
+34
-31
@@ -1,18 +1,18 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
4c6c3590caca877484ca256335bb1c9fcfa9736999f9c74d0a46ba8255894f49 AGENTS.md
|
||||
e055e50b5a46ba97e4bcddbac7f5f0635efb10240ac8039ac5d01c70672c5372 CHANGELOG.md
|
||||
d66bffcc09f1e3c351e639ed46c0b8c4ff4d814b81c410a96a5ace9f78cdd34a Cargo.lock
|
||||
d9f6c09426dbfbd0bd876efb57722e252dfce1090b489a06c2086852307a6583 Cargo.toml
|
||||
ad0382153576411fb19861072635c02bdf33de740d6f122d8f27bd9c52ce112a AGENTS.md
|
||||
91c6942b3a0d24d61bcfd43f9ee9fc8f3f5d889aeaae63dde118849d64a283c5 CHANGELOG.md
|
||||
2f1de87bae58761fc8690ec7225520e3a16213c77cbbb5981e34f12a95810668 Cargo.lock
|
||||
8c76ffaf4adba93df26e30f3dd163e0e5e201d069d1ae69c071ca42d7fc7264e Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
f99952e718c956a34bc2c4e81a2ccccaef767c7567fe76a29e05e05beaa9e2fa README.md
|
||||
3aa4c5b581dd179d3926676c65870815cbfd47bda7b8e60741e5ac9712f8fbbb ROADMAP.md
|
||||
31e13d43d5432731ca7f1bfd29d73eb2b62b8d23fe8678205f712f0daf9908c6 README.md
|
||||
fc03a8d19055642313f0b34bcd22e1cce17efc4d91be7fe7e4819e33b91c531d ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
||||
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
|
||||
7b14a253256617d7a0618f84964ff0ced0a5d5a20e99c7e2c2581b4147dc829d dev.sh
|
||||
3e709b09ddf4e2c17497c66b416d07e1423eeeb5f29a9100b06c8d3d5cacc36a dev.sh
|
||||
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
||||
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
|
||||
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
|
||||
@@ -61,9 +61,9 @@ d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.m
|
||||
ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.md
|
||||
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
||||
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
|
||||
c90409f4dc979e72998983f4c470c10a07e9ae1f9a551d4c06d6b77400ca9831 docs/SETTINGS.md
|
||||
25a06e7ddbb95d286e849b2fc5ba4adeedbddba0bad948e55bd91d962fd606de docs/SETTINGS.md
|
||||
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
|
||||
464862ebc9f53615361b910dac0e936f61bf0588f0c3ca03b1626886b5c3befb docs/TEST_REPORT.md
|
||||
e79adbe06155b024adfe8d072abba36394b3c6ef2007920627c217e290c08a88 docs/TEST_REPORT.md
|
||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
|
||||
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
|
||||
@@ -95,8 +95,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
|
||||
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
|
||||
fa53054d2108a8b0935a5c595c609a224f2a03694fcdee27137e197d15b8921c langs/en_US.json
|
||||
776eb519bea38465945a13fd2ebe6e4b24682d39d7fd6a4d6f58be01abd41a70 langs/pl_pl.json
|
||||
1c3bdb963bc4fa4eef48439721d7fee54375458f3a1d44f9e31c0c4ed7aca4f0 langs/en_US.json
|
||||
70d8ad7d503118356721ff7953b829049da793934dd73f4abbbbd7c7f0b4fc63 langs/pl_pl.json
|
||||
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
@@ -131,8 +131,10 @@ f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_s
|
||||
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/tests/test_stream_tcp.py
|
||||
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/tests/test_stream_udp.py
|
||||
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
|
||||
ad62f73bb42906d617cc1b479f02d4648c7324cb89fc11d2c6aa6bf13774eec9 src/acl.rs
|
||||
ea272433e7f92e983509a8ab1aa54242440abef21df526c2f99944ef279e09b9 src/acl/tests.rs
|
||||
55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs
|
||||
a0690d0d48bb92e53f4b94ce430ac434736ec2860c03ac4d50bd94df49706458 src/acl/dynamic.rs
|
||||
2b452b2819f9af4f60c984f98b5fb1e4e79353dee4fad1add6ecb540b7be1c74 src/acl/dynamic/tests.rs
|
||||
a22e5ea6381a1eacafe650be0f5cd6e653773e31f6491a562fe0459a309fb7cc src/acl/tests.rs
|
||||
6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs
|
||||
587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs
|
||||
3f158f46e80b403b658b4e9b1e3710e38c70e47b2d476b8223d9c394ffeeba95 src/acme/model.rs
|
||||
@@ -154,7 +156,7 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file
|
||||
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
|
||||
7de7da489a367658b700f00776fa43604d3554500b5fbef4da2764e402e68afa src/admin/logging_api.rs
|
||||
33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs
|
||||
eeb447366ceb666ce6e9ddef5f47eeb4052c58872f5a2543b1c9ed536a1989b5 src/admin/openapi.json
|
||||
c1b8ab15455a8199fbe011c6a7dd203572c4d4772c107d30531a6fb38a92ace2 src/admin/openapi.json
|
||||
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
|
||||
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
|
||||
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
|
||||
@@ -215,12 +217,12 @@ c24411dd62afaecfc631682554d4a037c7dd27100d817e9901ccdfd505c2ed5b src/admin/web/
|
||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
|
||||
c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/login.js
|
||||
36af1f8f83af7e6f9a60b4d9524714e02c077e539a90aac0ce990d0587c0c3ed src/admin/web/management.js
|
||||
0d0ec350d713aaf32a33607b2ddf0a8bb1858f2fd31c8396042cec4c84aee6ef src/admin/web/management.js
|
||||
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
|
||||
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
|
||||
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
||||
e3075af59644e42fa5735574d8e9caa1bcebfe8de184b78aff9f4026b91915f1 src/admin/web/streams.js
|
||||
478898f011c49862675b65e974727c7e900e6562554f3ae7fb376d598943ea5d src/admin/web/style.css
|
||||
065a377512c146254a19ce03c299f9d07f54e4f2cb0bccae965ac63e3a96063f src/admin/web/style.css
|
||||
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
||||
a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/transform-panel.js
|
||||
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
|
||||
@@ -293,7 +295,7 @@ a8a0ce446eb53ffdb8f8d97efa3e1344410dc3798b25f62b8817e8e160d28940 src/cache/tick
|
||||
e2f58279499b7c3b47c65d3a7814bb5c7cd6506544d939875fbb0f91fa9aa59d src/compression/tests.rs
|
||||
7bc8285cb2012506fd1a38a71dffa561f885f4ab65a9b2a7c8344319e3cd1cf6 src/config.rs
|
||||
34be288c858a037c7174f6c4dee23178b01b565aa9d65f0951f351e1685847df src/config/backend.rs
|
||||
a3d6a9d01dd9b3b602ea2f3dd5559503ea82129544c8944c26ad39445aaa3ad3 src/config/compile.rs
|
||||
beb831a208c4e5f7ebe8327686b1e0edc66608c7032f895cb4fc3366b189aae0 src/config/compile.rs
|
||||
490d05d84b2fe349ddde54eae44880993fab70811652484dd9ba38b5069d603b src/config/internal_routing.rs
|
||||
a19fc675720c7c2dbf4bba706e1b1c30c83c6df1da3f29e3cbb3b82442e7246a src/config/model.rs
|
||||
f66c2388f31b19142c12469df23d43451e0f7c48b158d4996920282c06d32076 src/config/routing.rs
|
||||
@@ -335,7 +337,7 @@ f5815d778cc1785723faed4abb15f9880f22b6524b5289fe1e7158905aac0986 src/logging/mo
|
||||
42fef80af50735cd566c6801dad86ba81be98cf4a8ec7d45c2ea26f53f556a22 src/logging/runtime.rs
|
||||
03c50bd14fecd421b0901a5f0af891dc277e35420e32068781e3a6d78c88c24b src/logging/tests.rs
|
||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||
788cdeb96e887f85dc24798ec69b56619ee35fabeac5a40f8fa071bbf6cf24c6 src/main/admin_runtime.rs
|
||||
fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin_runtime.rs
|
||||
42a9197475823f67c1ca762951ede5d7a2f10124bd6ed4d1a3365f4297be894c src/main/bootstrap.rs
|
||||
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
|
||||
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
|
||||
@@ -391,12 +393,12 @@ e7e3a898e2c769b1ef11e58a13c1053104ada93dd694f8a03ea4b647c44c7d69 src/rate_limit
|
||||
d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs
|
||||
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
|
||||
d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
|
||||
4e0b25c2b86eefddd76c64b39dc1c1cd6d243addc32cdea99e6c3ac84ed879a6 src/runtime/certificates.rs
|
||||
8d0d7b01871c8c588c40a54330a1053c85024655fbe38516ca6c94c3c19a8956 src/runtime/certificates.rs
|
||||
8d2443df8dab1ce6f11878e991b4928046b7362c417c5703f6d1f3a977646cbe src/runtime/constructors.rs
|
||||
31dc5cdea00efd63594dc6becd5a9103594696145cbc8e7b7594fb407214ad9d src/runtime/docker.rs
|
||||
92b4b284f1b69dca58e4e3f76199f0aecc397eed26d10123b427af1bb8a4a57e src/runtime/docker.rs
|
||||
6b2b57888863a396461fff202bfdaa6d8e869b2628cc171c0823f31f388e5178 src/runtime/tests.rs
|
||||
4036b48925f5b25de70e21dfbb278488c20b39248dcda4cad4d19d5d7aed2b8d src/runtime/types.rs
|
||||
88bc546fdda20d3f1f8f5e7d7872ede1fa334b8bb5102c729bd72f56e3fb94b9 src/runtime/updates.rs
|
||||
e1d172acd2b230b8f4dd671cae8862a76f28416cbf0e43f86214a796edd90fab src/runtime/updates.rs
|
||||
208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs
|
||||
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
|
||||
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
|
||||
@@ -500,19 +502,20 @@ c9d0bef4b8ed7a2f49b0fad3e116cace62d3e6e1ca31e02a3073d1fdde5157d8 tools/checks/a
|
||||
f3dba206c60cd2e6fcc5f4a2c8d652ccf47362694448312cc1f8dc4112d93e7e tools/checks/backend_cache_0297_ui_test.mjs
|
||||
f34f0f78edc4b6c6226006337090c72d1f8e57ddd77939b37cf00538a05abfa8 tools/checks/backend_dashboard_0323_ui_test.mjs
|
||||
45cd21192248ffe6f66c20bab03da1a5ce299f040883764d09a640859e19924d tools/checks/backend_health_0300_ui_test.mjs
|
||||
03492edbaa3aa67d81a0b0ce71f849a870c695c8ea436f02ce270701f5476738 tools/checks/certificate_api_0319.py
|
||||
48359559ca415890acbef0f2717bb4b7acacdc26b6cf5f78135a9d9637f83547 tools/checks/certificate_api_0319.py
|
||||
526d16db9de7be414a64483a1e9276d4a4402e3c7efd2479f79d01cc5b388f46 tools/checks/certificate_api_0319_ui_test.mjs
|
||||
334771ffcca95c18e91d10d1ef053d2085d6addcff4ebd0da779c30800cce9fc tools/checks/check_source.py
|
||||
d48c5080b34d2df18d0f3bf16cf7b58a36efbe4f6f2b96724581dea2b1a91544 tools/checks/check_source.py
|
||||
dc4df51fc15d4b6345c6625890ddbe014103851ebfa16abc583fbb0d809f4015 tools/checks/dashboard_boot_0311_ui_test.mjs
|
||||
7508f232a9e48049002dd4c9f34f9737bc92d304c51219f50430578dbc721855 tools/checks/dashboard_inline_layout_0323_ui_test.mjs
|
||||
dba768a42247cdd01863a032cf7ba223a52240285e70b0f7a0abd4a2e68c5fa1 tools/checks/dashboard_layout_editor_0323_ui_test.mjs
|
||||
259d4b0985c82b631e151a07e8ef3b4c760019cb99bb2af458ce577497f7df58 tools/checks/dashboard_startup_0310_ui_test.mjs
|
||||
29d2c6e7473cffd9d7e486e1dc377a47c25e786fd173e697a882606273601cdf tools/checks/dependency_upgrade_0312.py
|
||||
f6a6d12275bc8ba627b4616a546d075511094bb064cd9058231cc86ca489743e tools/checks/dependency_upgrade_0312.py
|
||||
38e35bed4faf984932b39bb5a00819fadc664387e8050a803ff4b5348b31369f tools/checks/docker_startup_0318_ui_test.mjs
|
||||
e233c4644b7c85459aafb576c59fe7d6cea184db4778204edd9ab269724d2c62 tools/checks/docs_check.py
|
||||
3396e5a510787e70e2ad8d1fad90356af461a88fd84429525d56fa82113da0f3 tools/checks/docs_check.py
|
||||
2d44f85ec083dedfee60dc15e65e040c6c208614411458b36a2fca8b5762f4f7 tools/checks/dynamic_acl_0325.py
|
||||
5bf26d9c883704e5ac85670bbcb7fb0bbad5d01779ab50407ff4fd5f3022a9cb tools/checks/file_logs_tabs_0315_ui_test.mjs
|
||||
72f3c4e3274d33e02d46646395bc0cd18a8f723f8747969fc81997c7a7259e76 tools/checks/h3_cache_mobile_0315_test.mjs
|
||||
0ff6b762ad2b727c4354e0f0710651a51282f14466072960f958a116551246e9 tools/checks/h3_navigation_0316.py
|
||||
1b5ead59bb2ea0e1ff32bf085f791018149cb9cc29b071fae4fb98ea80010824 tools/checks/h3_navigation_0316.py
|
||||
e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py
|
||||
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
|
||||
a8bda904a92f9436761db1460f14da3c0c9b3db9edeeee581497700242784d64 tools/checks/memory_contracts.py
|
||||
@@ -526,26 +529,26 @@ f450e97dccf75c24e228e696f21ed4ae826a823ea4edde2429894a6b6b36eaab tools/checks/o
|
||||
a1e68daf3a72b65545b0f5346094cbc0357c80d88504603090b776ef770f08de tools/checks/overview_wide_masonry_0323_ui_test.mjs
|
||||
1ac812929c2a8c0699ae3bdebd0d7e05b5bcaf86c87533bb669b5ba7955e3245 tools/checks/panel_cache_http_0310_ui_test.mjs
|
||||
eea7cdc9c7d8dd0f5fb079cfff657a6feadfc0b856f63265bfbd8257f1beca19 tools/checks/preflight.py
|
||||
c5163e47b07393f4101c0bacb78d6041b3738bb2ab01c547d2b7511abbbe54b1 tools/checks/preserve_host_0324.py
|
||||
200e86cb2598bd3b422187c6421d10017f0b39afb137aaf7614858e1b3924592 tools/checks/preserve_host_0324.py
|
||||
3352941648add6d8a8044c0b5cad6bf264546d38c884a5beed64cd95addd850c tools/checks/preserve_host_0324_ui_test.mjs
|
||||
021a97bd6763b59f3f7eca15f51efdf88d0c863f1f81a2e42ba80a9a09ca3fec tools/checks/refresh_0310_ui_test.mjs
|
||||
c11583dad0899f8b945cae6e2f6cb0bdb55f5526bd5f42c248ac41badec83679 tools/checks/release_0317.py
|
||||
b84b66a6f8f63cd0a354ef607b96ff60c77ef37f80f49ea9a9036b33147c0b6a tools/checks/release_0317_ui_test.mjs
|
||||
af42355fc7aeca631e3e24dcaadc75a721f9031d1c339d248d649b89c572ec54 tools/checks/release_0318.py
|
||||
56f8f6839df0e65a1c9a4b64bdd5d8ce7bd3dcd76280f85181d2a6c333308e13 tools/checks/release_0318.py
|
||||
e73afc62214e9092c180730f624a2039ac3fc850dcd865b2b117c11f7caaf88b tools/checks/release_0318_ui_test.mjs
|
||||
689ec03d97e89af1704a3e4fe04a4cec495676da589d2ff5565ae051a2de54c6 tools/checks/release_smoke.py
|
||||
62350e7fb5d4c4cfbe1b68ada1176c655922706dc2ebabc70913f5189ffa4155 tools/checks/release_smoke.py
|
||||
31b31e976962e2c53e0f1f71bf67f7d2686d30cb26f638defa1bd7636cf1c1e8 tools/checks/rust_module_contracts.py
|
||||
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py
|
||||
adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py
|
||||
23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py
|
||||
0087a7d7b766ab9183cc45a8a93396d05c004f5899a193fc36b5e1151d7fc93f tools/checks/security_logs_0315_ui_test.mjs
|
||||
c8bdf822bfc972614b47caf3d6bfb392f1c4c96b43fc0113b963a8def73b7e7d tools/checks/security_logs_0315_ui_test.mjs
|
||||
011bbe65dae072999a3951aba3d74a520926abf836b784cdb2395023d2942867 tools/checks/source_module_contracts.py
|
||||
37eebe90c0a60f7d7f4fbdd09986171b7dbd50c4d3760d94c31595dd93a2e680 tools/checks/source_pruning_0317.py
|
||||
bb862620104f925488e36904e258b7c934a097362ef879ecb275c64008cebb83 tools/checks/stream_0320_ui_test.mjs
|
||||
19cb347c87b0fa462fc2f3f2f5bef62e93628334ab4b8a1776df417e47c91ef0 tools/checks/stream_0320_visibility_regression.mjs
|
||||
8542f5c668155aa9f70de11e6fdaf64c5a80629a15ecc5f1d8c40f3d655f9b55 tools/checks/system_info_0301_ui_test.mjs
|
||||
543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.py
|
||||
3b703261c71c0f128a385ea9996da89abbe2cfbbf1a4b381184653cc28aaf84d tools/checks/upstream_protocols_0323.py
|
||||
760188db7aaae178c5f4570c1bcfac8b6cffe2e2e0522bb317331dc44515cbe5 tools/checks/upstream_protocols_0323.py
|
||||
a5105ab53de67e6b1114974c92b019014e1aa93acc445bc5dd6c82af0b0d3a16 tools/checks/version_contracts.py
|
||||
161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs
|
||||
ea98e8ce2bdc841814e486ea4c13f94e284b2c50a5887598b7a288f75a14deaf tools/checks/vhost_www_ui_test.mjs
|
||||
|
||||
@@ -100,6 +100,7 @@ if [[ "$mode" == "test" ]]; then
|
||||
python3 tools/checks/version_contracts.py
|
||||
python3 tools/checks/upstream_protocols_0323.py
|
||||
python3 tools/checks/preserve_host_0324.py
|
||||
python3 tools/checks/dynamic_acl_0325.py
|
||||
python3 tools/checks/check_source.py
|
||||
python3 tools/checks/release_smoke.py
|
||||
python3 tools/checks/memory_contracts.py
|
||||
|
||||
@@ -607,3 +607,30 @@ The sample charts are browser-local, bounded and reset on a changed config
|
||||
revision, logout or source unavailability. They are not computed while hidden.
|
||||
No additional polling, SQLite schema migration or new administrator endpoint
|
||||
is introduced. Existing Stream widgets remain independent.
|
||||
|
||||
|
||||
## Dynamic DNS / DDNS client ACL (0.32.5)
|
||||
|
||||
In **ACL profiles**, add an **Allow** rule and select **DNS / DDNS** instead of
|
||||
IP/CIDR. Enter the hostname without a URL, port or wildcard, for example:
|
||||
|
||||
```text
|
||||
allow myoffice.example.ddns.net
|
||||
```
|
||||
|
||||
Apply the profile to the desired vhost. Use the **Allow exceptions** preset
|
||||
(default deny) to admit only resolved addresses (and any other explicitly
|
||||
allowed IP/country exceptions). Host policies and inline vhost/location ACLs
|
||||
accept the same rule format. A/AAAA records are refreshed every 60 seconds,
|
||||
with immediate refresh on published configuration changes. No request-path DNS
|
||||
lookup is performed. The initial state and failed, empty, oversized, or expired
|
||||
DNS results **do not authorize any client IP** for that DNS rule. Only
|
||||
**allow** rules may contain DNS names; `deny example.com` is rejected.
|
||||
|
||||
Limits: 128 distinct names across active ACLs, 32 returned IPs/name, four-
|
||||
second lookup timeout and eight parallel DNS lookups. Successful answers expire
|
||||
after 180 seconds if the refresh task stalls. DNS changes may take up to the
|
||||
next poll interval to take effect. The chosen DDNS provider controls which IPs
|
||||
receive access; always trust the provider and protect its credentials. Existing
|
||||
IP/CIDR ACLs and country rules are unchanged. No new admin access path or
|
||||
backend origin DNS exceptions are created by this feature.
|
||||
@@ -1,63 +0,0 @@
|
||||
# Verification report - RustProxy 0.32.4
|
||||
|
||||
## Scope
|
||||
|
||||
Review of the supplied RustProxy 0.32.3 source and patch to 0.32.4:
|
||||
HTTP Host / HTTP/2 authority parity, explicit TCP/DNS destination and TLS/SNI
|
||||
isolation, relaxed HTTPS-by-IP troubleshooting hint, and uniform Overview gutters.
|
||||
Follow-up patch to the first 0.32.4 archive:
|
||||
- Fix reported `E0382` in `src/proxy/types.rs`: `Request::builder().uri(uri)`
|
||||
moved `uri` before `upstream_request_target(&uri, ...)` could borrow it.
|
||||
`.uri(uri.clone())` preserves the original URI for authority selection.
|
||||
- Make normal Overview gutters uniformly 4px (dense 3px, comfortable 12px).
|
||||
Resolve the higher-specificity `html[data-dashboard-density] .stats-grid`
|
||||
rule which otherwise overrode the aligned grid and kept the larger gaps.
|
||||
Existing database/configuration schemas and retry/transport policies are
|
||||
unchanged. Only compiling Rust and live protocol tests prove runtime behavior.
|
||||
|
||||
## Available checks
|
||||
|
||||
- `./dev.sh test`: **all pre-Cargo checks passed**, including source/security,
|
||||
SQLite migration/upgrade/backup simulations, memory-model scripts and **35
|
||||
Node.js UI/behavior test scripts**. The command then exited **127** because
|
||||
Rust/Cargo is not installed; this is **not** a passing Rust test run.
|
||||
- New `preserve_host_0324.py` and `preserve_host_0324_ui_test.mjs` passed:
|
||||
authority/destination split source contracts, four protocol selectors,
|
||||
verified/unencrypted origin warnings, explicit diagnostics preset.
|
||||
- Expanded Chromium CSS fixture `tools/checks/overview_gutters_0324_browser.py`:
|
||||
**27 checks passed** (3 container widths x 3 UI density modes x 3 Dashboard
|
||||
densities), checking horizontal/vertical gutters, equally high rows and
|
||||
numeric-to-large-card transition. Normal/dense/comfortable gaps measured
|
||||
**4/3/12px** and the outer grid bottom margin remained `0px`.
|
||||
The older `overview_grid_spacing_0323_browser.py` also passed with all
|
||||
62 blocks, responsive breakpoints and browser zoom 90%-130%.
|
||||
These are simulated layout tests, not a live administrator session.
|
||||
- `preserve_host_0324.py` now guards against moving `uri` before its final
|
||||
authority adjustment, in addition to the backend/transport source contracts.
|
||||
The existing Rust unit test for retry authority remains in the source.
|
||||
- Version consistency: Cargo manifest, package root in existing lockfile,
|
||||
OpenAPI, README, release notes and static assertion fixtures updated to 0.32.4.
|
||||
External Cargo dependency resolutions have **not** been regenerated.
|
||||
|
||||
## Rust validation not performed (blocking for production)
|
||||
|
||||
The user's Rust compiler reported `error[E0382]: borrow of moved value: uri`
|
||||
from the first 0.32.4 archive. This has been corrected in source, but **the
|
||||
corrected code has not been compiled here**: no Cargo or Rust compiler is
|
||||
available in the source-editing environment.
|
||||
Do not treat static tests or the simulated TLS-peer tests added to Rust source
|
||||
as evidence that RustProxy builds or that an actual Angie backend accepts the
|
||||
corrected requests. Run, on a Rust 1.99+ host:
|
||||
|
||||
```sh
|
||||
cargo fmt --all -- --check
|
||||
cargo check --locked --all-targets
|
||||
cargo test --locked
|
||||
```
|
||||
|
||||
Then validate on staging with HTTPS-by-IP and `preserve_host: true` for
|
||||
HTTP/1.1, Auto (H2 and H1 ALPN), compatible HTTP/2 and strict H2; check
|
||||
`:authority` vs `Host`, SNI, certificate verification, WebSockets, H2C and
|
||||
HTTP health probe `host` overrides. Inspect Angie response/error logs for
|
||||
400 responses and verify backend connectivity through pool rotation/retry.
|
||||
No claim of universal backend compatibility or measured throughput is made.
|
||||
+10
-5
@@ -783,7 +783,7 @@
|
||||
"locationHelp": "The longest matching prefix wins. The original path and query are retained. Select a backend from this host or add a dedicated backend. Host authentication and ACL always apply.",
|
||||
"aclUse": "Enable IP access list",
|
||||
"aclAllow": "Allow",
|
||||
"aclNetwork": "IP network (CIDR)",
|
||||
"aclNetwork": "IP/CIDR or DNS name",
|
||||
"aclAction": "Action",
|
||||
"aclAdd": "Add IP rule",
|
||||
"authMode": "Authentication",
|
||||
@@ -1000,7 +1000,7 @@
|
||||
"aclProfileId": "Profile ID",
|
||||
"aclProfileName": "Display name",
|
||||
"aclProfileIpEnabled": "Enable IP ACL",
|
||||
"aclProfileRules": "IP rules — one “allow|deny CIDR” per line",
|
||||
"aclProfileRules": "ACL rules — allow IP/CIDR or DNS hostname; deny IP/CIDR only (one per line)",
|
||||
"aclProfileCombineHelp": "Country and IP exceptions are combined with OR inside this profile. Explicit deny rules and blocked countries win; attached profile, vhost and location policies must still all pass.",
|
||||
"aclProfilePolicyMode": "Choose the base policy",
|
||||
"aclProfilePolicyModeHelp": "Use a guided allow-list or block-list for normal cases. Custom mode keeps full control for mixed policies.",
|
||||
@@ -1028,7 +1028,7 @@
|
||||
"aclProfilePreviewDefaultAllow": "DEFAULT: ALLOW",
|
||||
"aclProfilePreviewAllowedCountries": "ALLOW COUNTRY",
|
||||
"aclProfilePreviewBlockedCountries": "BLOCK COUNTRY",
|
||||
"aclProfilePreviewAllowedIps": "ALLOW IP",
|
||||
"aclProfilePreviewAllowedIps": "ALLOW IP / DNS",
|
||||
"aclProfilePreviewBlockedIps": "BLOCK IP",
|
||||
"aclProfilePreviewIpRules": "IP RULES",
|
||||
"aclProfilePreviewNoExceptions": "No exceptions configured",
|
||||
@@ -1042,7 +1042,7 @@
|
||||
"aclProfileCountriesShortBlock": "Blocked countries",
|
||||
"aclProfileIpsShortAllow": "Allowed IPs",
|
||||
"aclProfileIpsShortBlock": "Blocked IPs",
|
||||
"aclProfileIpRulesShort": "IP rules",
|
||||
"aclProfileIpRulesShort": "IP/DNS rules",
|
||||
"aclProfileIpOff": "IP ACL disabled",
|
||||
"aclProfileCountriesOff": "Country filtering disabled",
|
||||
"aclProfileAttached": "Attached to",
|
||||
@@ -2624,6 +2624,11 @@
|
||||
"cornerStyleSubtle": "Subtle",
|
||||
"cornerStyleRounded": "Rounded",
|
||||
"cornerStyleExtra": "Extra rounded",
|
||||
"cornerStyleHelp": "Change corners across the interface independently of your color theme. Original preserves the existing shape."
|
||||
"cornerStyleHelp": "Change corners across the interface independently of your color theme. Original preserves the existing shape.",
|
||||
"aclRuleType": "Address type",
|
||||
"aclRuleIp": "IP / CIDR",
|
||||
"aclRuleDns": "DNS / DDNS",
|
||||
"aclDnsAllowOnly": "DNS/DDNS addresses only support allow rules. Use IP/CIDR for deny.",
|
||||
"aclDnsHelp": "DNS/DDNS allow rules resolve A/AAAA every 60 seconds. DNS errors remove access (fail closed). Enter a hostname such as office.example.ddns.net; no URL or wildcard. Select Allow exceptions with default Deny. Up to 128 unique hostnames, 32 addresses per hostname."
|
||||
}
|
||||
}
|
||||
+10
-5
@@ -783,7 +783,7 @@
|
||||
"locationHelp": "Wygrywa najdłuższy pasujący prefiks. Ścieżka i query pozostają bez zmian. Wybierz backend hosta lub dodaj osobny. Uwierzytelnianie i ACL hosta obowiązują zawsze.",
|
||||
"aclUse": "Włącz listę dostępu IP",
|
||||
"aclAllow": "Zezwól",
|
||||
"aclNetwork": "Sieć IP (CIDR)",
|
||||
"aclNetwork": "Adres IP/CIDR lub nazwa DNS",
|
||||
"aclAction": "Akcja",
|
||||
"aclAdd": "Dodaj regułę IP",
|
||||
"authMode": "Uwierzytelnianie",
|
||||
@@ -1000,7 +1000,7 @@
|
||||
"aclProfileId": "ID profilu",
|
||||
"aclProfileName": "Nazwa wyświetlana",
|
||||
"aclProfileIpEnabled": "Włącz ACL IP",
|
||||
"aclProfileRules": "Reguły IP — po jednej „allow|deny CIDR” w wierszu",
|
||||
"aclProfileRules": "Reguły ACL — allow IP/CIDR lub nazwa DNS; deny tylko IP/CIDR (po jednej na wiersz)",
|
||||
"aclProfileCombineHelp": "Wyjątki krajów i IP są łączone operatorem LUB w obrębie tego profilu. Jawne blokady IP i zablokowane kraje wygrywają; profil, vhost i lokalizacja nadal muszą przejść wszystkie swoje polityki.",
|
||||
"aclProfilePolicyMode": "Wybierz bazową politykę",
|
||||
"aclProfilePolicyModeHelp": "Do typowych przypadków użyj prostego trybu zezwoleń albo blokad. Tryb niestandardowy daje pełną kontrolę nad mieszanymi regułami.",
|
||||
@@ -1028,7 +1028,7 @@
|
||||
"aclProfilePreviewDefaultAllow": "DOMYŚLNIE: ZEZWALAJ",
|
||||
"aclProfilePreviewAllowedCountries": "ZEZWÓL KRAJ",
|
||||
"aclProfilePreviewBlockedCountries": "BLOKUJ KRAJ",
|
||||
"aclProfilePreviewAllowedIps": "ZEZWÓL IP",
|
||||
"aclProfilePreviewAllowedIps": "ZEZWÓL IP / DNS",
|
||||
"aclProfilePreviewBlockedIps": "BLOKUJ IP",
|
||||
"aclProfilePreviewIpRules": "REGUŁY IP",
|
||||
"aclProfilePreviewNoExceptions": "Brak skonfigurowanych wyjątków",
|
||||
@@ -1042,7 +1042,7 @@
|
||||
"aclProfileCountriesShortBlock": "Blokowane kraje",
|
||||
"aclProfileIpsShortAllow": "Dozwolone IP",
|
||||
"aclProfileIpsShortBlock": "Blokowane IP",
|
||||
"aclProfileIpRulesShort": "reguł IP",
|
||||
"aclProfileIpRulesShort": "reguł IP/DNS",
|
||||
"aclProfileIpOff": "ACL IP wyłączony",
|
||||
"aclProfileCountriesOff": "Filtrowanie krajów wyłączone",
|
||||
"aclProfileAttached": "Przypięty do",
|
||||
@@ -2624,6 +2624,11 @@
|
||||
"cornerStyleSubtle": "Delikatne",
|
||||
"cornerStyleRounded": "Zaokrąglone",
|
||||
"cornerStyleExtra": "Mocno zaokrąglone",
|
||||
"cornerStyleHelp": "Ustawia zaokrąglenia w całym panelu niezależnie od motywu. Oryginalne zachowują dotychczasowy wygląd."
|
||||
"cornerStyleHelp": "Ustawia zaokrąglenia w całym panelu niezależnie od motywu. Oryginalne zachowują dotychczasowy wygląd.",
|
||||
"aclRuleType": "Typ adresu",
|
||||
"aclRuleIp": "IP / CIDR",
|
||||
"aclRuleDns": "DNS / DDNS",
|
||||
"aclDnsAllowOnly": "Reguły DNS/DDNS obsługują tylko allow. Do deny użyj IP/CIDR.",
|
||||
"aclDnsHelp": "Reguły DNS/DDNS typu allow odświeżają rekordy A/AAAA co 60 sekund. Błąd DNS usuwa zezwolenie (fail closed). Podaj nazwę, np. biuro.example.ddns.net, bez URL i gwiazdek. Wybierz wyjątki zezwalające z domyślną blokadą. Maksymalnie 128 nazw i 32 adresy na nazwę."
|
||||
}
|
||||
}
|
||||
+25
-8
@@ -4,6 +4,8 @@ use ipnet::IpNet;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub(crate) mod dynamic;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Action {
|
||||
@@ -42,22 +44,35 @@ pub struct Acl {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompiledAcl {
|
||||
default: Action,
|
||||
rules: Vec<(IpNet, Action)>,
|
||||
rules: Vec<(RuleMatch, Action)>,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
enum RuleMatch {
|
||||
Ip(IpNet),
|
||||
Dns(String),
|
||||
}
|
||||
impl Acl {
|
||||
pub fn compile(&self) -> Result<CompiledAcl> {
|
||||
ensure!(self.rules.len() <= 128, "ACL supports at most 128 rules");
|
||||
let mut rules = Vec::with_capacity(self.rules.len());
|
||||
for rule in &self.rules {
|
||||
ensure!(rule.network.len() <= 64, "ACL network is too long");
|
||||
let network = parse_network(&rule.network)?;
|
||||
ensure!(rule.network.len() <= 254, "ACL network is too long");
|
||||
let target = if let Ok(network) = parse_network(&rule.network) {
|
||||
if let IpNet::V6(net) = network {
|
||||
ensure!(
|
||||
net.addr().to_ipv4_mapped().is_none(),
|
||||
"use IPv4 CIDR instead of IPv4-mapped IPv6 in ACL rules"
|
||||
);
|
||||
}
|
||||
rules.push((network, rule.action));
|
||||
RuleMatch::Ip(network)
|
||||
} else {
|
||||
ensure!(
|
||||
rule.action == Action::Allow,
|
||||
"dynamic DNS ACL rules only support allow; use a static IP/CIDR for deny"
|
||||
);
|
||||
RuleMatch::Dns(dynamic::hostname(&rule.network)?)
|
||||
};
|
||||
rules.push((target, rule.action));
|
||||
}
|
||||
Ok(CompiledAcl {
|
||||
default: self.default,
|
||||
@@ -73,7 +88,11 @@ impl CompiledAcl {
|
||||
};
|
||||
let mut allowed = false;
|
||||
for (network, action) in &self.rules {
|
||||
if network.contains(&peer) {
|
||||
let matched = match network {
|
||||
RuleMatch::Ip(net) => net.contains(&peer),
|
||||
RuleMatch::Dns(host) => dynamic::matches(host, peer),
|
||||
};
|
||||
if matched {
|
||||
if *action == Action::Deny {
|
||||
return Some(Action::Deny);
|
||||
}
|
||||
@@ -124,9 +143,7 @@ pub fn allows_with_country(
|
||||
&& (ip_action == Some(Action::Allow)
|
||||
|| acl.is_none_or(|value| value.default == Action::Allow))
|
||||
}
|
||||
crate::rate_limit::CountryMode::Allow => {
|
||||
ip_action == Some(Action::Allow) || country_listed
|
||||
}
|
||||
crate::rate_limit::CountryMode::Allow => ip_action == Some(Action::Allow) || country_listed,
|
||||
}
|
||||
}
|
||||
/// Validate the already-decoded routing representation used internally after
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//! Bounded, fail-closed DNS/DDNS ACL address cache. No resolver runs in the request path.
|
||||
use crate::config::Config;
|
||||
use anyhow::{ensure, Result};
|
||||
use arc_swap::ArcSwap;
|
||||
use futures_util::{stream, StreamExt};
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap},
|
||||
net::IpAddr,
|
||||
sync::{Arc, LazyLock, Weak},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::{sync::Notify, time::MissedTickBehavior};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub const REFRESH_SECONDS: u64 = 60;
|
||||
const MAX_AGE_SECONDS: u64 = 180;
|
||||
const MAX_NAMES: usize = 128;
|
||||
const MAX_ADDRESSES: usize = 32;
|
||||
const PARALLEL_LOOKUPS: usize = 8;
|
||||
const LOOKUP_SECONDS: u64 = 4;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Addresses {
|
||||
ips: Vec<IpAddr>,
|
||||
valid_until: Instant,
|
||||
}
|
||||
|
||||
static CACHE: LazyLock<ArcSwap<HashMap<String, Addresses>>> =
|
||||
LazyLock::new(|| ArcSwap::from_pointee(HashMap::new()));
|
||||
static CHANGED: LazyLock<Notify> = LazyLock::new(Notify::new);
|
||||
|
||||
pub(crate) fn matches(name: &str, ip: IpAddr) -> bool {
|
||||
let cache = CACHE.load();
|
||||
cache.get(name).is_some_and(|entry| {
|
||||
entry.valid_until > Instant::now() && entry.ips.binary_search(&ip).is_ok()
|
||||
})
|
||||
}
|
||||
|
||||
/// Strict ASCII DNS hostnames only (IDNs can be supplied in punycode).
|
||||
/// A and AAAA results are eligible. IPs and CIDRs remain separate static rules.
|
||||
pub(crate) fn hostname(value: &str) -> Result<String> {
|
||||
let candidate = value.strip_suffix('.').unwrap_or(value);
|
||||
ensure!(candidate.len() <= 253 && candidate.contains('.'), "ACL DNS name requires a fully qualified hostname");
|
||||
ensure!(candidate.is_ascii() && !candidate.contains('/') && !candidate.contains(':'), "invalid ACL DNS name");
|
||||
let labels: Vec<_> = candidate.split('.').collect();
|
||||
ensure!(labels.last().is_some_and(|part| part.chars().any(|ch| ch.is_ascii_alphabetic())), "invalid ACL DNS name suffix");
|
||||
for label in labels {
|
||||
ensure!(!label.is_empty() && label.len() <= 63, "invalid ACL DNS label length");
|
||||
ensure!(label.as_bytes()[0].is_ascii_alphanumeric()
|
||||
&& label.as_bytes()[label.len() - 1].is_ascii_alphanumeric()
|
||||
&& label.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'-'), "invalid ACL DNS label");
|
||||
}
|
||||
Ok(candidate.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
/// Walk every ACL scope, including standalone host policies, ACL profiles,
|
||||
/// inline vhost/location ACLs and Docker overrides. Limit process-wide DNS work.
|
||||
pub(crate) fn configured_hostnames(config: &Config) -> Result<Vec<String>> {
|
||||
let mut names = BTreeSet::new();
|
||||
let mut inspect = |acl: Option<&crate::acl::Acl>| -> Result<()> {
|
||||
if let Some(acl) = acl {
|
||||
for rule in &acl.rules {
|
||||
if rule.network.parse::<ipnet::IpNet>().is_ok() || rule.network.parse::<IpAddr>().is_ok() {
|
||||
continue;
|
||||
}
|
||||
// A slash is always a CIDR error, never a DNS name.
|
||||
let name = hostname(&rule.network)?;
|
||||
ensure!(rule.action == crate::acl::Action::Allow,
|
||||
"dynamic DNS ACL rules only support allow; use static IP/CIDR for deny");
|
||||
names.insert(name);
|
||||
ensure!(names.len() <= MAX_NAMES, "too many distinct dynamic DNS ACL names (max 128)");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
for policy in &config.host_policies { inspect(Some(&policy.acl))?; }
|
||||
for profile in &config.acl_profiles { inspect(profile.acl.as_ref())?; }
|
||||
for vhost in &config.vhosts {
|
||||
inspect(vhost.acl.as_ref())?;
|
||||
for route in &vhost.routes { inspect(route.acl.as_ref())?; }
|
||||
}
|
||||
for agent in &config.docker_agents.agents {
|
||||
for assignment in &agent.assignments { inspect(assignment.acl.as_ref())?; }
|
||||
}
|
||||
Ok(names.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn resolve(name: String) -> (String, Option<Addresses>) {
|
||||
let answer = tokio::time::timeout(
|
||||
Duration::from_secs(LOOKUP_SECONDS),
|
||||
tokio::net::lookup_host((name.as_str(), 0u16)),
|
||||
).await;
|
||||
let mut ips = match answer {
|
||||
Ok(Ok(sockets)) => sockets.map(|socket| socket.ip()).collect::<Vec<_>>(),
|
||||
_ => {
|
||||
tracing::warn!(hostname = %name, "dynamic ACL DNS lookup failed; no addresses permitted");
|
||||
return (name, None);
|
||||
}
|
||||
};
|
||||
ips.sort_unstable();
|
||||
ips.dedup();
|
||||
if ips.is_empty() || ips.len() > MAX_ADDRESSES || ips.iter().any(|ip| {
|
||||
matches!(ip, IpAddr::V6(v6) if v6.to_ipv4_mapped().is_some())
|
||||
}) {
|
||||
tracing::warn!(hostname = %name, "dynamic ACL DNS returned an invalid or excessive address set");
|
||||
return (name, None);
|
||||
}
|
||||
(name, Some(Addresses { ips, valid_until: Instant::now() + Duration::from_secs(MAX_AGE_SECONDS) }))
|
||||
}
|
||||
|
||||
pub(crate) fn config_changed() { CHANGED.notify_one(); }
|
||||
|
||||
pub(crate) async fn poll_loop(
|
||||
app: Weak<crate::runtime::App>,
|
||||
shutdown: CancellationToken,
|
||||
) {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(REFRESH_SECONDS));
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = shutdown.cancelled() => break,
|
||||
_ = interval.tick() => {},
|
||||
_ = CHANGED.notified() => {},
|
||||
}
|
||||
let Some(state) = app.upgrade() else { break; };
|
||||
let snapshot = state.config.load();
|
||||
let Ok(names) = configured_hostnames(&snapshot.envelope.config) else { continue; };
|
||||
drop(snapshot);
|
||||
// Resolve concurrently with hard bounds; write one immutable snapshot.
|
||||
let results: HashMap<_, _> = tokio::select! {
|
||||
_ = shutdown.cancelled() => break,
|
||||
result = stream::iter(names.iter().cloned())
|
||||
.map(resolve)
|
||||
.buffer_unordered(PARALLEL_LOOKUPS)
|
||||
.filter_map(|(name, answer)| async move { answer.map(|value| (name, value)) })
|
||||
.collect() => result,
|
||||
};
|
||||
// A concurrently changed configuration must not publish obsolete DNS data.
|
||||
let current = state.config.load();
|
||||
if configured_hostnames(¤t.envelope.config).ok().as_ref() != Some(&names) {
|
||||
CHANGED.notify_one();
|
||||
continue;
|
||||
}
|
||||
CACHE.store(Arc::new(results));
|
||||
}
|
||||
// Do not retain permit decisions when the resolver lifecycle stops.
|
||||
CACHE.store(Arc::new(HashMap::new()));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
include!("dynamic/tests.rs");
|
||||
@@ -0,0 +1,41 @@
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn validates_names_without_accepting_cidr_lookalikes_or_urls() {
|
||||
assert_eq!(hostname("My-Office.DDNS.Example.").unwrap(), "my-office.ddns.example");
|
||||
for bad in ["localhost", "", "*.example.com", "http://ddns.example", "1.2.3.999", "foo..example", "-x.example", "a_.example", "a.example/24"] {
|
||||
assert!(hostname(bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn resolver_expiry_fails_closed_and_address_sets_are_exact() {
|
||||
let name = "test-dynamic-unique.example";
|
||||
let ip: IpAddr = "198.51.100.4".parse().unwrap();
|
||||
let entry = Addresses { ips: vec![ip], valid_until: Instant::now() + Duration::from_secs(1) };
|
||||
assert!(entry.valid_until > Instant::now());
|
||||
// The request path accepts only cached exact addresses, never a subnet.
|
||||
let mut cache = HashMap::new(); cache.insert(name.to_owned(), entry);
|
||||
CACHE.store(Arc::new(cache));
|
||||
assert!(matches(name, ip));
|
||||
assert!(!matches(name, "198.51.100.5".parse().unwrap()));
|
||||
// A formerly resolved IP cannot remain permitted after expiration.
|
||||
let mut expired = HashMap::new();
|
||||
expired.insert(name.to_owned(), Addresses {
|
||||
ips: vec![ip], valid_until: Instant::now() - Duration::from_secs(1),
|
||||
});
|
||||
CACHE.store(Arc::new(expired));
|
||||
assert!(!matches(name, ip));
|
||||
// IPv6 addresses have the same exact-match and rotation semantics.
|
||||
let new_ip: IpAddr = "2001:db8::8".parse().unwrap();
|
||||
let mut rotated = HashMap::new();
|
||||
rotated.insert(name.to_owned(), Addresses {
|
||||
ips: vec![new_ip], valid_until: Instant::now() + Duration::from_secs(10),
|
||||
});
|
||||
CACHE.store(Arc::new(rotated));
|
||||
assert!(matches(name, new_ip));
|
||||
assert!(!matches(name, ip));
|
||||
CACHE.store(Arc::new(HashMap::new()));
|
||||
assert!(!matches(name, new_ip));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,51 @@ mod tests {
|
||||
assert!(!acl.allows("2001:db8::8".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_dns_allow_is_fail_closed_until_resolved_and_old_cidrs_still_work() {
|
||||
let acl = Acl {
|
||||
default: Action::Deny,
|
||||
rules: vec![
|
||||
Rule { network: "Office.DDNS.Example".into(), action: Action::Allow },
|
||||
Rule { network: "192.0.2.0/24".into(), action: Action::Allow },
|
||||
Rule { network: "192.0.2.99".into(), action: Action::Deny },
|
||||
],
|
||||
}.compile().unwrap();
|
||||
assert!(!acl.allows("198.51.100.25".parse().unwrap()));
|
||||
assert!(acl.allows("192.0.2.42".parse().unwrap()));
|
||||
assert!(!acl.allows("192.0.2.99".parse().unwrap()));
|
||||
for rejected in ["example.com/24", "*.example.com", "https://example.com", "foo..example.org"] {
|
||||
assert!(Acl {
|
||||
default: Action::Deny,
|
||||
rules: vec![Rule { network: rejected.into(), action: Action::Allow }],
|
||||
}.compile().is_err(), "{rejected}");
|
||||
}
|
||||
assert!(Acl {
|
||||
default: Action::Allow,
|
||||
rules: vec![Rule { network: "example.ddns.net".into(), action: Action::Deny }],
|
||||
}.compile().is_err());
|
||||
}
|
||||
#[test]
|
||||
fn configured_dns_names_are_deduplicated_across_acl_scopes() {
|
||||
let mut config = crate::config::Config::default();
|
||||
config.acl_profiles.push(crate::config::AclProfile {
|
||||
id: "home".into(),
|
||||
name: "Home".into(),
|
||||
acl: Some(Acl { default: Action::Deny, rules: vec![
|
||||
Rule { network: "Home.DDNS.Example".into(), action: Action::Allow },
|
||||
] }),
|
||||
..Default::default()
|
||||
});
|
||||
config.host_policies.push(crate::config::HostPolicy {
|
||||
hosts: vec!["example.org".into()],
|
||||
acl: Acl { default: Action::Deny, rules: vec![
|
||||
Rule { network: "home.ddns.example".into(), action: Action::Allow },
|
||||
Rule { network: "office.ddns.example".into(), action: Action::Allow },
|
||||
] },
|
||||
});
|
||||
let hosts = crate::acl::dynamic::configured_hostnames(&config).unwrap();
|
||||
assert_eq!(hosts, vec!["home.ddns.example", "office.ddns.example"]);
|
||||
}
|
||||
#[test]
|
||||
fn country_allow_and_ip_allow_are_alternatives_inside_one_scope() {
|
||||
let acl = Acl {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.32.4",
|
||||
"version": "0.32.5",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
|
||||
},
|
||||
"servers": [
|
||||
@@ -251,7 +251,8 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"network": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Client IPv4/IPv6 address, CIDR, or allow-only DNS/DDNS hostname (A/AAAA refreshed every 60s; failed lookups deny the hostname)."
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
|
||||
@@ -222,6 +222,7 @@
|
||||
const addButton = $("acl-profile-rule-add"), importButton = $("acl-profile-import-rules");
|
||||
const previewDefault = $("acl-profile-preview-default"), previewFlow = $("acl-profile-preview-flow"), previewNote = $("acl-profile-preview-note");
|
||||
const countryHelp = $("acl-profile-country-help"), countryLabel = $("acl-profile-country-label"), ipHelp = $("acl-profile-ip-help");
|
||||
const dnsHint = i18n("p", "aclDnsHelp", "fine"); ipHelp?.after(dnsHint);
|
||||
const modeInputs = [...form.querySelectorAll('input[name="acl-profile-mode"]')];
|
||||
const i18n = (tag, key, cls) => { const node = el(tag, t(key), cls); node.dataset.i18n = key; return node; };
|
||||
const selectedMode = () => modeInputs.find(node => node.checked)?.value || "allow_exceptions";
|
||||
@@ -240,6 +241,13 @@
|
||||
if (network.includes(":")) return `${network}/128`;
|
||||
return network;
|
||||
}
|
||||
const ruleKind = value => /[a-z]/i.test(value) && value.includes(".") && !value.includes(":") && !value.includes("/") ? "dns" : "ip";
|
||||
function syncRuleKind(row) {
|
||||
const kind = row.querySelector(".acl-profile-rule-type").value;
|
||||
const network = row.querySelector(".acl-profile-rule-network");
|
||||
network.maxLength = kind === "dns" ? 253 : 64;
|
||||
network.placeholder = kind === "dns" ? "office.example.ddns.net" : "10.0.0.0/8 or 203.0.113.5";
|
||||
}
|
||||
function addRule(rule = null, mark = true) {
|
||||
if (ruleRows.children.length >= 128) { toast(t("vhRowLimit"), true); return; }
|
||||
const mode = selectedMode(), fallbackAction = mode === "custom" ? "allow" : guidedAction(mode);
|
||||
@@ -250,11 +258,18 @@
|
||||
for (const actionValue of ["allow", "deny"]) { const option = el("option", t(actionValue === "allow" ? "aclAllow" : "vhDeny")); option.value = actionValue; actionSelect.append(option); }
|
||||
actionSelect.value = value.action || fallbackAction; actionLabel.append(actionSelect);
|
||||
const actionBadge = el("span", "", "acl-profile-rule-action-badge"); actionLabel.append(actionBadge);
|
||||
const typeLabel = el("label"); typeLabel.append(i18n("span", "aclRuleType"));
|
||||
const kind = el("select"); kind.className = "acl-profile-rule-type";
|
||||
for (const [id, key] of [["ip", "aclRuleIp"], ["dns", "aclRuleDns"]]) {
|
||||
const item = i18n("option", key); item.value = id; kind.append(item);
|
||||
}
|
||||
kind.value = ruleKind(value.network || ""); typeLabel.append(kind);
|
||||
const networkLabel = el("label"); networkLabel.append(i18n("span", "aclNetwork"));
|
||||
const network = el("input"); network.className = "acl-profile-rule-network"; network.type = "text"; network.required = true; network.maxLength = 64; network.autocomplete = "off"; network.spellcheck = false; network.placeholder = "10.0.0.0/8 or 203.0.113.5"; network.value = value.network || ""; networkLabel.append(network);
|
||||
const network = el("input"); network.className = "acl-profile-rule-network"; network.type = "text"; network.required = true; network.autocomplete = "off"; network.spellcheck = false; network.value = value.network || ""; networkLabel.append(network);
|
||||
network.addEventListener("blur", () => { const normalized = normalizeNetwork(network.value); if (normalized !== network.value) { network.value = normalized; syncRulesText(); renderPreview(); } });
|
||||
kind.addEventListener("change", () => { network.value = ""; syncRuleKind(row); syncRulesText(); dirty = true; syncPolicy(); network.focus(); });
|
||||
const remove = action(t("remove"), () => { row.remove(); syncRulesText(); dirty = true; syncPolicy(); }); remove.classList.add("acl-profile-rule-remove");
|
||||
row.append(actionLabel, networkLabel, remove); ruleRows.append(row);
|
||||
row.append(actionLabel, typeLabel, networkLabel, remove); ruleRows.append(row); syncRuleKind(row);
|
||||
if (mark) { syncRulesText(); dirty = true; }
|
||||
syncRuleRows();
|
||||
}
|
||||
@@ -370,6 +385,9 @@
|
||||
function refreshTranslations() {
|
||||
$("acl-profile-editing").textContent = t(editing ? "aclProfileEdit" : "aclProfileNew");
|
||||
for (const row of ruleRows.children) {
|
||||
const network = row.querySelector(".acl-profile-rule-network");
|
||||
const kind = row.querySelector(".acl-profile-rule-type");
|
||||
if (kind) { kind.value = ruleKind(network?.value || ""); syncRuleKind(row); }
|
||||
const select = row.querySelector(".acl-profile-rule-action");
|
||||
if (select?.options?.length >= 2) {
|
||||
select.options[0].textContent = t("aclAllow");
|
||||
@@ -433,6 +451,8 @@
|
||||
}
|
||||
const finalMode = selectedMode(), expectedAction = finalMode === "custom" ? null : guidedAction(finalMode);
|
||||
const aclRules = collectRuleRows().map(rule => ({ ...rule, action: expectedAction || rule.action }));
|
||||
if (aclRules.some(rule => rule.action === "deny" && ruleKind(rule.network) === "dns"))
|
||||
throw fieldError(ruleRows, t("aclDnsAllowOnly"));
|
||||
if (aclRules.some(rule => !rule.network)) { const input = [...ruleRows.querySelectorAll(".acl-profile-rule-network")].find(node => !node.value.trim()); throw fieldError(input || rules, t("ruleSyntax")); }
|
||||
for (const [row, rule] of [...ruleRows.children].map((row, index) => [row, aclRules[index]])) row.querySelector(".acl-profile-rule-network").value = rule.network;
|
||||
const finalDefault = finalMode === "custom" ? aclDefault.value : guidedDefault(finalMode);
|
||||
|
||||
+183
-679
File diff suppressed because it is too large.
Load diff
+98
-27
@@ -48,29 +48,58 @@ pub fn compile_with_active(
|
||||
config.compression.validate()?;
|
||||
config.server_header.validate()?;
|
||||
let forwarding = config.forwarding.compile()?;
|
||||
ensure!(config.php_fpm_profiles.len() <= 128, "too many PHP-FPM profiles (max 128)");
|
||||
ensure!(
|
||||
config.php_fpm_profiles.len() <= 128,
|
||||
"too many PHP-FPM profiles (max 128)"
|
||||
);
|
||||
let mut php_profiles = HashMap::new();
|
||||
for profile in &mut config.php_fpm_profiles {
|
||||
profile.validate().with_context(|| format!("PHP-FPM profile {}", profile.id))?;
|
||||
ensure!(php_profiles.insert(profile.id.clone(), profile.php.clone()).is_none(),
|
||||
"duplicate PHP-FPM profile id");
|
||||
profile
|
||||
.validate()
|
||||
.with_context(|| format!("PHP-FPM profile {}", profile.id))?;
|
||||
ensure!(
|
||||
php_profiles
|
||||
.insert(profile.id.clone(), profile.php.clone())
|
||||
.is_none(),
|
||||
"duplicate PHP-FPM profile id"
|
||||
);
|
||||
}
|
||||
ensure!(config.web_services.len() <= 128, "too many Web Services (max 128)");
|
||||
ensure!(
|
||||
config.web_services.len() <= 128,
|
||||
"too many Web Services (max 128)"
|
||||
);
|
||||
let mut web_services = HashMap::new();
|
||||
// PHP-FPM can retain FastCGI PHP_VALUE/PHP_ADMIN_VALUE directives in reused
|
||||
// workers. Different directive sets on one socket can leak to the next vhost.
|
||||
// Report unsafe legacy configurations but keep them loadable for migration.
|
||||
let mut php_endpoint_policies: HashMap<String, (String, std::collections::BTreeMap<String, String>, std::collections::BTreeMap<String, String>)> = HashMap::new();
|
||||
let mut php_endpoint_policies: HashMap<
|
||||
String,
|
||||
(
|
||||
String,
|
||||
std::collections::BTreeMap<String, String>,
|
||||
std::collections::BTreeMap<String, String>,
|
||||
),
|
||||
> = HashMap::new();
|
||||
for service in &mut config.web_services {
|
||||
service.validate().with_context(|| format!("Web Service {}", service.id))?;
|
||||
service
|
||||
.validate()
|
||||
.with_context(|| format!("Web Service {}", service.id))?;
|
||||
// A profile changes effective runtime settings, not the saved standalone
|
||||
// PHP configuration. Detaching a profile restores local settings exactly.
|
||||
let mut effective = service.clone();
|
||||
if let Some(id) = &service.php_profile_id {
|
||||
effective.php = php_profiles.get(id).with_context(|| {
|
||||
format!("Web Service {} references unknown PHP-FPM profile {}", service.id, id)
|
||||
})?.clone();
|
||||
effective.validate().with_context(|| format!("Web Service {} PHP-FPM profile {}", service.id, id))?;
|
||||
effective.php = php_profiles
|
||||
.get(id)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Web Service {} references unknown PHP-FPM profile {}",
|
||||
service.id, id
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
effective
|
||||
.validate()
|
||||
.with_context(|| format!("Web Service {} PHP-FPM profile {}", service.id, id))?;
|
||||
}
|
||||
if effective.php.enabled {
|
||||
let php = &effective.php;
|
||||
@@ -82,13 +111,25 @@ pub fn compile_with_active(
|
||||
);
|
||||
}
|
||||
} else {
|
||||
php_endpoint_policies.insert(php.endpoint.clone(), (
|
||||
effective.id.clone(), php.php_values.clone(), php.php_admin_values.clone(),
|
||||
));
|
||||
php_endpoint_policies.insert(
|
||||
php.endpoint.clone(),
|
||||
(
|
||||
effective.id.clone(),
|
||||
php.php_values.clone(),
|
||||
php.php_admin_values.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
let compiled = Arc::new(effective.compile().with_context(|| format!("Web Service {}", service.id))?);
|
||||
ensure!(web_services.insert(service.id.clone(), compiled).is_none(), "duplicate Web Service id");
|
||||
let compiled = Arc::new(
|
||||
effective
|
||||
.compile()
|
||||
.with_context(|| format!("Web Service {}", service.id))?,
|
||||
);
|
||||
ensure!(
|
||||
web_services.insert(service.id.clone(), compiled).is_none(),
|
||||
"duplicate Web Service id"
|
||||
);
|
||||
}
|
||||
ensure!(config.vhosts.len() <= 64, "too many vhosts (max 64)");
|
||||
let mut vhost_ids = HashSet::new();
|
||||
@@ -264,6 +305,8 @@ pub fn compile_with_active(
|
||||
"duplicate ACL profile id"
|
||||
);
|
||||
}
|
||||
// Prevent unbounded DNS ACL work across the entire published configuration.
|
||||
crate::acl::dynamic::configured_hostnames(&config)?;
|
||||
for agent in &config.docker_agents.agents {
|
||||
if let Some(profile_id) = agent.acl_profile_id.as_deref() {
|
||||
ensure!(
|
||||
@@ -393,7 +436,9 @@ fn validate_route_rewrite(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_location_redirect(redirect: Option<&crate::vhost::LocationRedirect>) -> Result<Option<crate::config::CompiledLocationRedirect>> {
|
||||
fn validate_location_redirect(
|
||||
redirect: Option<&crate::vhost::LocationRedirect>,
|
||||
) -> Result<Option<crate::config::CompiledLocationRedirect>> {
|
||||
let Some(redirect) = redirect else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -409,24 +454,50 @@ fn validate_location_redirect(redirect: Option<&crate::vhost::LocationRedirect>)
|
||||
);
|
||||
// Replace capture references with a harmless segment so URL structure can
|
||||
// be validated before requests reach the data plane.
|
||||
let authority_end = redirect.target.find("://").and_then(|start| {
|
||||
let authority_end = redirect
|
||||
.target
|
||||
.find("://")
|
||||
.and_then(|start| {
|
||||
let rest = &redirect.target[start + 3..];
|
||||
rest.char_indices().find(|(_, ch)| matches!(ch, '/' | '?' | '#')).map(|(offset, _)| start + 3 + offset).or(Some(redirect.target.len()))
|
||||
}).ok_or_else(|| anyhow::anyhow!("location redirect target must be an absolute URL"))?;
|
||||
ensure!(!redirect.target[..authority_end].contains('$'), "location redirect captures are not allowed in the URL authority");
|
||||
rest.char_indices()
|
||||
.find(|(_, ch)| matches!(ch, '/' | '?' | '#'))
|
||||
.map(|(offset, _)| start + 3 + offset)
|
||||
.or(Some(redirect.target.len()))
|
||||
})
|
||||
.ok_or_else(|| anyhow::anyhow!("location redirect target must be an absolute URL"))?;
|
||||
ensure!(
|
||||
!redirect.target[..authority_end].contains('$'),
|
||||
"location redirect captures are not allowed in the URL authority"
|
||||
);
|
||||
let sample = regex::Regex::new(r"\$\{?[A-Za-z0-9_]+\}?")
|
||||
.expect("static capture regex")
|
||||
.replace_all(&redirect.target, "capture");
|
||||
let parsed = url::Url::parse(&sample).context("location redirect target must be an absolute URL")?;
|
||||
ensure!(matches!(parsed.scheme(), "http" | "https") && parsed.host_str().is_some(), "location redirect target must use http/https and include a host");
|
||||
ensure!(parsed.username().is_empty() && parsed.password().is_none(), "location redirect target cannot contain credentials");
|
||||
let parsed =
|
||||
url::Url::parse(&sample).context("location redirect target must be an absolute URL")?;
|
||||
ensure!(
|
||||
matches!(parsed.scheme(), "http" | "https") && parsed.host_str().is_some(),
|
||||
"location redirect target must use http/https and include a host"
|
||||
);
|
||||
ensure!(
|
||||
parsed.username().is_empty() && parsed.password().is_none(),
|
||||
"location redirect target cannot contain credentials"
|
||||
);
|
||||
let rewrite_regex = match redirect.rewrite_pattern.as_deref() {
|
||||
None | Some("") => None,
|
||||
Some(pattern) => {
|
||||
ensure!(pattern.len() <= 1024 && !pattern.chars().any(char::is_control), "invalid location redirect rewrite regex");
|
||||
ensure!(
|
||||
pattern.len() <= 1024 && !pattern.chars().any(char::is_control),
|
||||
"invalid location redirect rewrite regex"
|
||||
);
|
||||
let mut builder = regex::RegexBuilder::new(pattern);
|
||||
builder.size_limit(2 * 1024 * 1024).dfa_size_limit(2 * 1024 * 1024);
|
||||
Some(builder.build().context("invalid location redirect rewrite regex")?)
|
||||
builder
|
||||
.size_limit(2 * 1024 * 1024)
|
||||
.dfa_size_limit(2 * 1024 * 1024);
|
||||
Some(
|
||||
builder
|
||||
.build()
|
||||
.context("invalid location redirect rewrite regex")?,
|
||||
)
|
||||
}
|
||||
};
|
||||
Ok(Some(crate::config::CompiledLocationRedirect {
|
||||
|
||||
@@ -146,7 +146,10 @@ async fn run(
|
||||
}
|
||||
let renewal_observations = store.load_certificate_renewals().await?;
|
||||
let geoip_format = snapshot.envelope.config.geoip_updates.format;
|
||||
let geoip = match rust_proxy::geoip::Database::load_format(&data_dir.join("geoip"), geoip_format) {
|
||||
let geoip = match rust_proxy::geoip::Database::load_format(
|
||||
&data_dir.join("geoip"),
|
||||
geoip_format,
|
||||
) {
|
||||
Ok(database) => std::sync::Arc::new(database),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "GeoIP country database unavailable; country allow lists fail closed");
|
||||
@@ -258,6 +261,7 @@ async fn run(
|
||||
// socket has bound successfully so DNS/network failures can never obscure or
|
||||
// interfere with listener startup failures.
|
||||
state.start_geoip_updates();
|
||||
state.start_dns_acl_updates();
|
||||
state.start_update_check(update_check);
|
||||
let shutdown = state.shutdown.clone();
|
||||
let grace = Duration::from_secs(options.shutdown_timeout_secs);
|
||||
@@ -432,7 +436,10 @@ async fn run(
|
||||
grace
|
||||
};
|
||||
if managed_update {
|
||||
tracing::info!(budget_seconds = cache_flush_budget.as_secs(), "managed-update hot-to-cold cache handoff started");
|
||||
tracing::info!(
|
||||
budget_seconds = cache_flush_budget.as_secs(),
|
||||
"managed-update hot-to-cold cache handoff started"
|
||||
);
|
||||
}
|
||||
let flush_result = if managed_update {
|
||||
tokio::time::timeout(
|
||||
@@ -444,10 +451,9 @@ async fn run(
|
||||
tokio::time::timeout(cache_flush_budget, state.cache.flush_hot_to_cold()).await
|
||||
};
|
||||
match flush_result {
|
||||
Ok(moved) if managed_update => tracing::info!(
|
||||
moved,
|
||||
"managed-update hot-to-cold cache handoff completed"
|
||||
),
|
||||
Ok(moved) if managed_update => {
|
||||
tracing::info!(moved, "managed-update hot-to-cold cache handoff completed")
|
||||
}
|
||||
Ok(moved) if moved > 0 => tracing::info!(
|
||||
moved,
|
||||
"flushed hot cache objects to cold storage before shutdown"
|
||||
|
||||
@@ -77,6 +77,7 @@ impl App {
|
||||
})?
|
||||
.map_err(|error| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{error:#}")))?;
|
||||
self.config.store(Arc::new(prepared));
|
||||
crate::acl::dynamic::config_changed();
|
||||
if let Some(certificate_id) = certificate_id {
|
||||
tracing::info!(
|
||||
revision,
|
||||
|
||||
@@ -37,6 +37,7 @@ impl App {
|
||||
})?
|
||||
.map_err(|error| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{error:#}")))?;
|
||||
self.config.store(Arc::new(prepared));
|
||||
crate::acl::dynamic::config_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
impl App {
|
||||
pub fn start_dns_acl_updates(self: &Arc<Self>) {
|
||||
self.tunnels.spawn(crate::acl::dynamic::poll_loop(
|
||||
Arc::downgrade(self), self.shutdown.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn start_geoip_updates(self: &Arc<Self>) {
|
||||
self.tunnels.spawn(crate::geoip::update_poll_loop(
|
||||
Arc::downgrade(self),
|
||||
@@ -152,6 +158,7 @@ impl App {
|
||||
this.logs.configure(envelope.config.logging.clone());
|
||||
crate::logging::configure_error_log(envelope.config.error_log.clone());
|
||||
this.config.store(Arc::new(prepared));
|
||||
crate::acl::dynamic::config_changed();
|
||||
tracing::info!(revision, "configuration applied");
|
||||
Ok(envelope)
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ import tomllib
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
read = lambda path: (root / path).read_text(encoding='utf-8')
|
||||
version = tomllib.loads(read('Cargo.toml'))['package']['version']
|
||||
assert version == '0.32.4', version
|
||||
assert version == '0.32.5', version
|
||||
spec = json.loads(read('src/admin/openapi.json'))
|
||||
assert spec['info']['version'] == version
|
||||
paths = spec['paths']
|
||||
@@ -64,5 +64,5 @@ for language in ('en_US', 'pl_pl'):
|
||||
for key in ('certificateApiView', 'certificateApiEdit', 'certificateApiSave', 'certificateApiRegenerate',
|
||||
'certificateApiRegenerateConfirm', 'certificateApiTokenHidden', 'certificateApiCreatedAt'):
|
||||
assert strings.get(key), (language, key)
|
||||
assert '0.32.4' in read('docs/TEST_REPORT.md')
|
||||
assert '0.32.5' in read('docs/TEST_REPORT.md')
|
||||
print('PASS: 0.31.9 per-credential API routes, separated metadata editing/token regeneration, OpenAPI security, UI and locales (source-only).')
|
||||
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
|
||||
|
||||
preflight(root)
|
||||
manifest = tomllib.loads((root / "Cargo.toml").read_text())
|
||||
assert manifest["package"]["version"] == "0.32.4"
|
||||
assert manifest["package"]["version"] == "0.32.5"
|
||||
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
||||
h3dep = manifest["dependencies"]["h3"]
|
||||
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.20" and "quinn" in h3dep["features"]
|
||||
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
|
||||
lock_path = root / "Cargo.lock"
|
||||
if lock_path.exists():
|
||||
lock = lock_path.read_text()
|
||||
assert 'name = "rust-proxy"\nversion = "0.32.4"' in lock
|
||||
assert 'name = "rust-proxy"\nversion = "0.32.5"' in lock
|
||||
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
||||
for release_doc, marker in {
|
||||
"README.md": "# RustProxy 0.32.4",
|
||||
"CHANGELOG.md": "## 0.32.4",
|
||||
"ROADMAP.md": "## 0.32.4",
|
||||
"AGENTS.md": "Current source version: 0.32.4.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.32.4",
|
||||
"README.md": "# RustProxy 0.32.5",
|
||||
"CHANGELOG.md": "## 0.32.5",
|
||||
"ROADMAP.md": "## 0.32.5",
|
||||
"AGENTS.md": "Current source version: 0.32.5.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.32.5",
|
||||
}.items():
|
||||
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
|
||||
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
|
||||
@@ -285,7 +285,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
|
||||
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
|
||||
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
|
||||
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
|
||||
for marker in ['"version": "0.32.4"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
for marker in ['"version": "0.32.5"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
assert marker in openapi, marker
|
||||
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
@@ -1396,7 +1396,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
|
||||
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
|
||||
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
|
||||
assert 'location.pathname === "/logging"' in settings_js_0241
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.32.4"' in openapi
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.32.5"' in openapi
|
||||
|
||||
# 0.24.1 compiler and hostname-cardinality regressions.
|
||||
tls_0241 = rust_source(root, "src/tls.rs")
|
||||
|
||||
@@ -14,7 +14,7 @@ parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--require-lock', action='store_true', help='Require the post-upgrade Cargo.lock produced by cargo update')
|
||||
args = parser.parse_args()
|
||||
manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text())
|
||||
assert manifest['package']['version'] == '0.32.4'
|
||||
assert manifest['package']['version'] == '0.32.5'
|
||||
assert manifest['package']['rust-version'] == '1.99'
|
||||
deps = manifest['dependencies']
|
||||
expected_requirements = {
|
||||
|
||||
@@ -51,7 +51,7 @@ assert '"/docs/content.json"' in admin
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
|
||||
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||
assert readme.startswith("# RustProxy 0.32.4")
|
||||
assert readme.startswith("# RustProxy 0.32.5")
|
||||
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
||||
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
||||
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static source contract for DDNS ACL 0.32.5; NOT a Rust test."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import tomllib
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
read = lambda p: (ROOT / p).read_text(encoding="utf-8")
|
||||
acl = read('src/acl.rs')
|
||||
dns = read('src/acl/dynamic.rs')
|
||||
js = read('src/admin/web/management.js')
|
||||
checks = {
|
||||
'keep static IP/CIDR checks': 'RuleMatch::Ip(net) => net.contains(&peer)' in acl,
|
||||
'dynamic DNS only allows explicit allow rules': 'dynamic DNS ACL rules only support allow' in acl,
|
||||
'deny precedence retained': 'return Some(Action::Deny)' in acl,
|
||||
'strict hostname validation': 'pub(crate) fn hostname(' in dns,
|
||||
'scope scan covers vhosts, locations, profiles, host policies and docker': all(f in dns for f in [
|
||||
'config.host_policies', 'config.acl_profiles', 'config.vhosts', 'vhost.routes', 'config.docker_agents.agents', 'agent.assignments'
|
||||
]),
|
||||
'bounded asynchronous DNS work': all(x in dns for x in ['REFRESH_SECONDS: u64 = 60', 'MAX_AGE_SECONDS: u64 = 180', 'MAX_NAMES: usize = 128', 'MAX_ADDRESSES: usize = 32', 'PARALLEL_LOOKUPS: usize = 8', 'LOOKUP_SECONDS: u64 = 4', 'buffer_unordered(PARALLEL_LOOKUPS)']),
|
||||
'lookup on refresh, not on request': 'lookup_host' in dns and 'lookup_host' not in acl and 'dynamic::matches' in acl,
|
||||
'fail closed on empty or failed resolution': 'CACHE.store(Arc::new(results))' in dns and 'return (name, None);' in dns and 'entry.valid_until > Instant::now()' in dns,
|
||||
'update after configuration commit': all('crate::acl::dynamic::config_changed()' in read(p) for p in ['src/runtime/updates.rs', 'src/runtime/docker.rs', 'src/runtime/certificates.rs']),
|
||||
'startup background refresh': 'state.start_dns_acl_updates();' in read('src/main/admin_runtime.rs'),
|
||||
'visual selector and disabled DNS deny': '.acl-profile-rule-type' in js and 'aclDnsAllowOnly' in js,
|
||||
'old network field persisted': 'network: normalizeNetwork(' in js,
|
||||
'PL/EN translations': all(k in json.loads(read(f'langs/{loc}.json'))['translations'] for loc in ('en_US','pl_pl') for k in ('aclRuleType','aclRuleDns','aclDnsHelp','aclDnsAllowOnly')),
|
||||
'package version': tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.5',
|
||||
}
|
||||
for name, ok in checks.items():
|
||||
if not ok: raise AssertionError(name)
|
||||
print(f'PASS: {len(checks)} DNS ACL 0.32.5 static source checks (not compiled or network-tested)')
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
html = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
assert '0.32.4' in (ROOT / "Cargo.toml").read_text()
|
||||
assert '0.32.5' in (ROOT / "Cargo.toml").read_text()
|
||||
assert '<a class="brand" href="/"' in html, 'logo must remain a true full-page navigation'
|
||||
assert 'addEventListener("click"' not in html.split('<a class="brand" href="/"',1)[1].split('</a>',1)[0]
|
||||
body = (ROOT / 'src/proxy/cache_helpers.rs').read_text()
|
||||
|
||||
@@ -51,6 +51,6 @@ for marker in ['margin: 0;', '--overview-gap: max(3px, calc(var(--ui-gap) - 1px)
|
||||
retry = re.search(r'fn retry_request\([\s\S]*?\n}\n', proxy)
|
||||
assert retry and '.uri(uri.clone())' in retry[0]
|
||||
assert 'upstream_request_target(&uri, request.headers_mut()' in retry[0]
|
||||
assert json.loads(read('src/admin/openapi.json'))['info']['version']=='0.32.4'
|
||||
assert 'version = "0.32.4"' in read('Cargo.toml')
|
||||
print('PASS: 0.32.4 origin pinning, authority, retry, CSS gutter source contracts (not Rust runtime)')
|
||||
assert json.loads(read('src/admin/openapi.json'))['info']['version']=='0.32.5'
|
||||
assert 'version = "0.32.5"' in read('Cargo.toml')
|
||||
print('PASS: 0.32.5 origin pinning, authority, retry, CSS gutter source contracts (not Rust runtime)')
|
||||
@@ -8,9 +8,9 @@ from html.parser import HTMLParser
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
def read(rel): return (ROOT / rel).read_text(encoding='utf-8')
|
||||
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.4'
|
||||
assert 'name = "rust-proxy"\nversion = "0.32.4"' in read('Cargo.lock')
|
||||
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.32.4'
|
||||
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.5'
|
||||
assert 'name = "rust-proxy"\nversion = "0.32.5"' in read('Cargo.lock')
|
||||
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.32.5'
|
||||
|
||||
pages = read('src/pages.rs')
|
||||
for code in (400, 401, 403, 404, 408, 410, 413, 429, 500, 502, 503, 504):
|
||||
|
||||
@@ -135,7 +135,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert backup.is_dir()
|
||||
assert (backup / "src/obsolete.rs").is_file()
|
||||
assert not (target / "src/obsolete.rs").exists()
|
||||
assert 'version = "0.32.4"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert 'version = "0.32.5"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
||||
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
||||
assert (target / "langs/site.json").is_file()
|
||||
@@ -146,7 +146,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert (target / "docker-agent/certificate.go").is_file()
|
||||
assert (target / "docker-agent/server.go").is_file()
|
||||
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
||||
assert manifest["to_version"] == "0.32.4"
|
||||
assert manifest["to_version"] == "0.32.5"
|
||||
assert manifest["removed_obsolete_runtime_state"] is True
|
||||
assert not legacy.exists()
|
||||
assert not (backup / "legacy-data").exists()
|
||||
@@ -617,7 +617,7 @@ def source_contract_smoke() -> None:
|
||||
assert marker in admin, marker
|
||||
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
|
||||
assert '"/api/status"' not in anonymous_block
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.32.4"' in openapi_0241
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.32.5"' in openapi_0241
|
||||
assert 'id="panel-logging"' not in html
|
||||
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
|
||||
assert marker in html, marker
|
||||
@@ -1102,7 +1102,7 @@ def source_contract_smoke() -> None:
|
||||
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
|
||||
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
|
||||
assert '"/api/config/history/restore"' in openapi
|
||||
for marker in ['"version": "0.32.4"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
for marker in ['"version": "0.32.5"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
assert '"software_update"' in openapi and '"update_available"' in openapi
|
||||
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
|
||||
|
||||
@@ -21,7 +21,7 @@ const openapi = json('src/admin/openapi.json');
|
||||
const en = json('langs/en_US.json').translations;
|
||||
const pl = json('langs/pl_pl.json').translations;
|
||||
|
||||
assert.match(manifest, /version = "0\.32\.4"/);
|
||||
assert.match(manifest, /version = "0\.32\.5"/);
|
||||
assert.match(lib, /pub mod user_agent;/);
|
||||
assert.match(model, /user_agent_blocking: crate::user_agent::Policy/);
|
||||
assert.match(vhost, /user_agent_blocking: crate::user_agent::Policy/);
|
||||
@@ -52,7 +52,7 @@ assert.match(fileLogs, /localStorage\.setItem\(CONSOLIDATION_STORAGE_KEY/);
|
||||
assert.match(fileLogs, /aggregate_min_count/);
|
||||
assert.match(fileLogs, /file-log-aggregate-copy/);
|
||||
|
||||
assert.equal(openapi.info.version, '0.32.4');
|
||||
assert.equal(openapi.info.version, '0.32.5');
|
||||
assert.ok(openapi.components.schemas.Config.properties.user_agent_blocking);
|
||||
assert.ok(openapi.components.schemas.Vhost.properties.user_agent_blocking);
|
||||
assert.ok(openapi.components.schemas.FileLogSource.properties.group_id);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline source contracts for RustProxy 0.32.4 upstream protocols and card registry.
|
||||
"""Offline source contracts for RustProxy 0.32.5 upstream protocols and card registry.
|
||||
|
||||
These assertions supplement, rather than replace, Rust ALPN, H2C and websocket
|
||||
runtime tests. No code execution against a RustProxy server is implied.
|
||||
@@ -141,7 +141,7 @@ for field in ('dashboard_widgets', 'dashboard_hidden_widgets'):
|
||||
|
||||
|
||||
|
||||
assert '"version": "0.32.4"' in read('src/admin/openapi.json')
|
||||
assert 'version = "0.32.4"' in read('Cargo.toml')
|
||||
assert 'version = "0.32.4"' in read('Cargo.lock')
|
||||
print(f'PASS: 0.32.4 upstream H1/Auto/compatible-HTTP2/strict-H2 source contracts and {len(ids)} registered Overview Cards')
|
||||
assert '"version": "0.32.5"' in read('src/admin/openapi.json')
|
||||
assert 'version = "0.32.5"' in read('Cargo.toml')
|
||||
assert 'version = "0.32.5"' in read('Cargo.lock')
|
||||
print(f'PASS: 0.32.5 upstream H1/Auto/compatible-HTTP2/strict-H2 source contracts and {len(ids)} registered Overview Cards')
|
||||
Reference in new issue
Block a user