0.34.9
This commit is contained in:
1 parent
70388578cb
commit
2b7c67d755
22 files changed
+369
-59
No files matched your search
@@ -24,6 +24,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
reader_doc!("docs/CACHE.md", "guides"),
|
||||
reader_doc!("docs/RESPONSE_DELIVERY.md", "guides"),
|
||||
reader_doc!("docs/TRANSPORT.md", "guides"),
|
||||
reader_doc!("docs/TLS_POST_QUANTUM.md", "guides"),
|
||||
reader_doc!("docs/STREAMS.md", "guides"),
|
||||
reader_doc!("docs/ACME_PROVIDERS.md", "guides"),
|
||||
reader_doc!("docs/DOCKER_DISCOVERY.md", "guides"),
|
||||
|
||||
@@ -6,13 +6,22 @@ mod tests {
|
||||
let value: Value = serde_json::from_slice(reader_bundle()).unwrap();
|
||||
let documents = value["documents"].as_array().unwrap();
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "README.md"));
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "docs/WEB_SERVER.md"));
|
||||
assert!(documents
|
||||
.iter()
|
||||
.any(|doc| doc["path"] == "docs/WEB_SERVER.md"));
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "docs/PHP_FPM.md"));
|
||||
assert!(documents
|
||||
.iter()
|
||||
.any(|doc| doc["path"] == "docs/TLS_POST_QUANTUM.md"));
|
||||
assert!(documents.iter().all(|doc| {
|
||||
doc["path"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["title"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["category"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["content"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["category"]
|
||||
.as_str()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
&& doc["content"]
|
||||
.as_str()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
+35
-2
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.34.8",
|
||||
"version": "0.34.9",
|
||||
"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. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
@@ -622,6 +622,11 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"post_quantum": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Experimental hybrid X25519MLKEM768 TLS 1.3 key exchange. Can only be enabled for a Vhost when enabled in the global TLS policy. HTTP/3 Vhosts must match the global policy."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -772,7 +777,35 @@
|
||||
},
|
||||
"tls": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"properties": {
|
||||
"policy": {
|
||||
"type": "object",
|
||||
"description": "Global TLS policy inherited by frontends, including experimental post-quantum key exchange.",
|
||||
"properties": {
|
||||
"tls12": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"tls13": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"cipher_suites": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "TLS cipher suites, independent of key exchange groups."
|
||||
},
|
||||
"post_quantum": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Experimental hybrid X25519MLKEM768 TLS 1.3 key exchange. Requires tls13=true, uses aws-lc-rs for frontend TLS; classic groups remain as fallback."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"cache": {
|
||||
|
||||
+3
-2
@@ -223,8 +223,9 @@ fn status_payload(state: &App) -> Value {
|
||||
});
|
||||
payload["tls"] = json!({
|
||||
"policy": &snapshot.envelope.config.tls.policy,
|
||||
"cipher_suites": crate::tls::cipher_suite_names(),
|
||||
"effective_cipher_suites": snapshot.envelope.config.tls.policy.effective_cipher_suites()
|
||||
"cipher_suites": crate::tls::cipher_suite_names_for(snapshot.envelope.config.tls.policy.post_quantum),
|
||||
"effective_cipher_suites": snapshot.envelope.config.tls.policy.effective_cipher_suites(),
|
||||
"key_exchange_groups": snapshot.envelope.config.tls.policy.key_exchange_groups()
|
||||
});
|
||||
payload
|
||||
}
|
||||
|
||||
@@ -3071,6 +3071,19 @@
|
||||
<label><span data-i18n="tlsCiphers">Cipher suites (one per line; empty = rustls
|
||||
defaults)</span><textarea id="tls-policy-ciphers" rows="8"
|
||||
spellcheck="false"></textarea></label>
|
||||
<div class="tls-pqc-control">
|
||||
<label class="check"><input id="tls-policy-pqc" type="checkbox" />
|
||||
<span data-i18n="tlsPqcLabel">Experimental post-quantum key exchange
|
||||
(PQC)</span>
|
||||
</label>
|
||||
<p class="fine" data-i18n="tlsPqcHelp">TLS 1.3 only: prioritize hybrid
|
||||
X25519MLKEM768 with classic key exchanges as fallback. This is a key exchange
|
||||
group, not a cipher suite. Disabled by default.</p>
|
||||
<a href="/docs#doc=docs%2FTLS_POST_QUANTUM.md" target="_blank"
|
||||
rel="noopener noreferrer" data-i18n="tlsPqcDocs">Learn about PQC, compatibility
|
||||
and testing (documentation)</a>
|
||||
</div>
|
||||
<p class="fine" id="tls-policy-key-exchange"></p>
|
||||
<p class="fine tls-policy-effective" id="tls-policy-effective"></p>
|
||||
<p class="fine" id="tls-policy-available"></p>
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply"
|
||||
|
||||
@@ -244,9 +244,11 @@
|
||||
]) { const node = $(id); if (node) node.textContent = value || t("unavailable"); }
|
||||
|
||||
const policy = envelope.config.tls?.policy || { tls12: true, tls13: true, cipher_suites: [] };
|
||||
if (!settingsDirty.has("tls-policy-form")) { $("tls-policy-12").checked = policy.tls12 !== false; $("tls-policy-13").checked = policy.tls13 !== false; $("tls-policy-ciphers").value = (policy.cipher_suites || []).join("\n"); }
|
||||
if (!settingsDirty.has("tls-policy-form")) { $("tls-policy-12").checked = policy.tls12 !== false; $("tls-policy-13").checked = policy.tls13 !== false; $("tls-policy-ciphers").value = (policy.cipher_suites || []).join("\n"); $("tls-policy-pqc").checked = policy.post_quantum === true; }
|
||||
const available = latestStatus?.tls?.cipher_suites || [];
|
||||
const effective = latestStatus?.tls?.effective_cipher_suites || [];
|
||||
const kxGroups = latestStatus?.tls?.key_exchange_groups || [];
|
||||
$("tls-policy-key-exchange").textContent = kxGroups.length ? `${t("tlsPqcKeyExchangeGroups")}: ${kxGroups.join(", ")}` : "";
|
||||
$("tls-policy-effective").textContent = effective.length ? `${t("tlsEffectiveCiphers")}: ${effective.join(", ")}` : "";
|
||||
$("tls-policy-available").textContent = available.length ? `${t("tlsAvailableCiphers")}: ${available.join(", ")}` : "";
|
||||
window.rustProxyCertificateApi?.render(); window.rustProxyCertificateApi?.load();
|
||||
@@ -287,7 +289,8 @@
|
||||
}, "administration");
|
||||
bindSettingsForm("tls-policy-form", next => {
|
||||
if (!$("tls-policy-12").checked && !$("tls-policy-13").checked) throw fieldError($("tls-policy-12"), t("tlsPolicyVersionRequired"));
|
||||
next.tls ||= {}; next.tls.policy = { tls12: $("tls-policy-12").checked, tls13: $("tls-policy-13").checked, cipher_suites: $("tls-policy-ciphers").value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean) };
|
||||
if ($("tls-policy-pqc").checked && !$("tls-policy-13").checked) throw fieldError($("tls-policy-13"), t("tlsPqcTls13Required"));
|
||||
next.tls ||= {}; next.tls.policy = { tls12: $("tls-policy-12").checked, tls13: $("tls-policy-13").checked, cipher_suites: $("tls-policy-ciphers").value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean), post_quantum: $("tls-policy-pqc").checked };
|
||||
}, "tls");
|
||||
$("admin-tls").addEventListener("change", () => { $("admin-certificate").disabled = !$("admin-tls").checked; });
|
||||
$("prometheus-form").addEventListener("input", example);
|
||||
|
||||
@@ -328,8 +328,9 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
const tls12 = field(g, "tls-policy-12", "tls12", tlsPolicy.tls12 !== false, "checkbox");
|
||||
const tls13 = field(g, "tls-policy-13", "tls13", tlsPolicy.tls13 !== false, "checkbox");
|
||||
const tlsCiphers = field(g, "tls-policy-ciphers", "tlsCiphers", (tlsPolicy.cipher_suites || []).join("\n"), "textarea", { rows: 7, maxLength: 8192 });
|
||||
const syncTlsPolicy = () => { for (const input of [tls12, tls13, tlsCiphers]) input.disabled = !customTlsPolicy.checked; };
|
||||
customTlsPolicy.addEventListener("change", syncTlsPolicy); syncTlsPolicy(); note(g, "vhTlsOverrideHelp");
|
||||
const tlsPqc = field(g, "tls-policy-pqc", "tlsPqcLabel", tlsPolicy.post_quantum === true, "checkbox");
|
||||
const syncTlsPolicy = () => { for (const input of [tls12, tls13, tlsCiphers, tlsPqc]) input.disabled = !customTlsPolicy.checked; };
|
||||
customTlsPolicy.addEventListener("change", syncTlsPolicy); syncTlsPolicy(); note(g, "vhTlsOverrideHelp"); note(g, "tlsPqcVhostHelp");
|
||||
g = group("vhTinyauth"); const auth = value.tinyauth || {};
|
||||
const authMode = field(g, "auth-mode", "authMode", value.basic_auth?.enabled ? "basic" : auth.enabled ? "tinyauth" : "none", "select", { values: [["none", "authNone"], ["tinyauth", "authTinyauth"], ["basic", "authBasic"]] });
|
||||
note(g, "authModeHelp");
|
||||
@@ -485,19 +486,21 @@ function collect() {
|
||||
if (!tlsPorts.has(443) && tlsPorts.size !== 1) throw new Error(t("vhForceSslPortConflict"));
|
||||
}
|
||||
if (yes("tls-policy-custom") && !yes("tls-policy-12") && !yes("tls-policy-13")) throw new Error(t("tlsPolicyVersionRequired"));
|
||||
if (yes("tls-policy-custom") && yes("tls-policy-pqc") && !yes("tls-policy-13")) throw new Error(t("tlsPqcTls13Required"));
|
||||
if (yes("tls-policy-custom") && yes("tls-policy-pqc") && envelope?.config.tls?.policy?.post_quantum !== true) throw new Error(t("tlsPqcGlobalRequired"));
|
||||
const selectedUsesHttp3 = bindingSelection.some(id => {
|
||||
const endpoint = envelope?.config.listeners?.endpoints?.find(item => item.id === id);
|
||||
return !!endpoint?.binding?.http3;
|
||||
});
|
||||
if (yes("http3-enabled") && selectedUsesHttp3 && yes("tls-policy-custom")) {
|
||||
const selected = { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")).sort() };
|
||||
const selected = { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")).sort(), post_quantum: yes("tls-policy-pqc") };
|
||||
const inherited = envelope?.config.tls?.policy || { tls12: true, tls13: true, cipher_suites: [] };
|
||||
const globalPolicy = { tls12: inherited.tls12 !== false, tls13: inherited.tls13 !== false, cipher_suites: [...(inherited.cipher_suites || [])].sort() };
|
||||
const globalPolicy = { tls12: inherited.tls12 !== false, tls13: inherited.tls13 !== false, cipher_suites: [...(inherited.cipher_suites || [])].sort(), post_quantum: inherited.post_quantum === true };
|
||||
if (JSON.stringify(selected) !== JSON.stringify(globalPolicy)) throw new Error(t("vhTlsHttp3Conflict"));
|
||||
}
|
||||
if (yes("content-type-block-enabled") && !lines(str("content-type-block-list")).length) throw new Error(t("vhContentTypeRequired"));
|
||||
return {
|
||||
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, error_fallback: mainFallbackTarget ? { target: mainFallbackTarget, statuses: mainFallbackStatuses.length ? mainFallbackStatuses : [404] } : null, force_ssl: yes("force-ssl"), www_redirect: yes("www-redirect"), www_redirect_target: str("www-redirect-target") || null, forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), allow_certificate_mismatch: yes("allow-certificate-mismatch"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), compression: collectCompression(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: null,
|
||||
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, error_fallback: mainFallbackTarget ? { target: mainFallbackTarget, statuses: mainFallbackStatuses.length ? mainFallbackStatuses : [404] } : null, force_ssl: yes("force-ssl"), www_redirect: yes("www-redirect"), www_redirect_target: str("www-redirect-target") || null, forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), allow_certificate_mismatch: yes("allow-certificate-mismatch"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")), post_quantum: yes("tls-policy-pqc") } : null, cache: collectCache(), compression: collectCompression(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: null,
|
||||
acl_profile_ids: window.rustProxyAclPicker.read(form.querySelector(".vh-acl-profile-choices")),
|
||||
proxy_timeouts: { connect_timeout_seconds: num("proxy-connect-timeout"), response_header_timeout_seconds: num("proxy-header-timeout"), read_timeout_seconds: num("proxy-read-timeout"), send_timeout_seconds: num("proxy-send-timeout") },
|
||||
rate_limit: { enabled: yes("rate-enabled"), requests: num("rate-requests"), window_seconds: num("rate-window"), burst: num("rate-burst"), country_access: { mode: str("country-mode"), countries: lines(str("country-list")).map(value => value.toUpperCase()) }, country_limits: countryRatesReader(), user_agent_limits: uaRatesReader() },
|
||||
|
||||
Reference in new issue
Block a user