mirror of
https://github.com/telemt/telemt.git
synced 2026-09-14 06:24:09 +03:00
WEB: websocket + websocket-lanes as Carrier
This commit is contained in:
@@ -114,7 +114,10 @@ fn web_debug_prefix_requiring_deferred_capacity_is_not_hot_applied() {
|
||||
new.web.debug.body_prefix_bytes = 3 * 1024 * 1024;
|
||||
|
||||
let applied = overlay_hot_fields(&old, &new);
|
||||
assert_eq!(applied.web.limits.max_body_bytes, old.web.limits.max_body_bytes);
|
||||
assert_eq!(
|
||||
applied.web.limits.max_body_bytes,
|
||||
old.web.limits.max_body_bytes
|
||||
);
|
||||
assert_eq!(
|
||||
applied.web.debug.body_prefix_bytes,
|
||||
old.web.debug.body_prefix_bytes
|
||||
|
||||
@@ -50,8 +50,7 @@ pub(super) fn rebuild(config: &mut ProxyConfig) -> Result<()> {
|
||||
client_secret(auth_entry.secret, profile.secret_mode);
|
||||
let capability =
|
||||
derive_web_capability(&client_secret[..client_secret_len], vhost.host.as_bytes())?;
|
||||
let key_fingerprint =
|
||||
debug_key_fingerprint(&client_secret[..client_secret_len]);
|
||||
let key_fingerprint = debug_key_fingerprint(&client_secret[..client_secret_len]);
|
||||
if !capabilities.insert(capability) {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"WEB vhost `{}` contains profiles with the same client capability",
|
||||
|
||||
@@ -259,7 +259,9 @@ const LISTENER_CONFIG_KEYS: &[&str] = &[
|
||||
"web_trusted_proxy_cidrs",
|
||||
];
|
||||
|
||||
const WEB_CONFIG_KEYS: &[&str] = &["enabled", "carrier", "debug", "limits", "timeouts", "vhosts"];
|
||||
const WEB_CONFIG_KEYS: &[&str] = &[
|
||||
"enabled", "carrier", "debug", "limits", "timeouts", "vhosts",
|
||||
];
|
||||
|
||||
const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[
|
||||
"max_header_bytes",
|
||||
@@ -269,6 +271,10 @@ const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[
|
||||
"max_frames_per_body",
|
||||
"max_http_connections",
|
||||
"max_http_handlers",
|
||||
"websocket_bytes_global",
|
||||
"websocket_admission_watermark_pct",
|
||||
"websocket_eviction_watermark_pct",
|
||||
"websocket_http_connection_reserve",
|
||||
"max_body_readers",
|
||||
"max_body_bytes_global",
|
||||
"max_sessions_global",
|
||||
@@ -319,6 +325,9 @@ const WEB_TIMEOUTS_CONFIG_KEYS: &[&str] = &[
|
||||
"body_secs",
|
||||
"stream_handshake_secs",
|
||||
"long_poll_secs",
|
||||
"websocket_write_secs",
|
||||
"websocket_backpressure_secs",
|
||||
"websocket_eviction_secs",
|
||||
"bootstrap_lifetime_secs",
|
||||
"reconnect_grace_secs",
|
||||
"http_idle_secs",
|
||||
|
||||
@@ -6,6 +6,8 @@ use super::*;
|
||||
mod debug;
|
||||
// Memory-envelope arithmetic remains isolated from protocol validation.
|
||||
mod memory;
|
||||
// WebSocket transport policy is validated independently from HTTP body policy.
|
||||
mod websocket;
|
||||
|
||||
const WEB_FRAME_HEADER_BYTES: usize = 8;
|
||||
const WEB_QUEUE_ITEM_COST: usize = 256;
|
||||
@@ -69,6 +71,7 @@ pub(super) fn validate(config: &mut ProxyConfig) -> Result<()> {
|
||||
return config_error("web.carrier=https-lanes requires web.limits.max_http_handlers >= 2");
|
||||
}
|
||||
validate_timeouts(&config.web.timeouts)?;
|
||||
websocket::validate(config.web.carrier, &config.web.limits, &config.web.timeouts)?;
|
||||
validate_vhosts(config)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -327,6 +330,12 @@ fn validate_timeouts(timeouts: &WebTimeoutsConfig) -> Result<()> {
|
||||
("body_secs", timeouts.body_secs),
|
||||
("stream_handshake_secs", timeouts.stream_handshake_secs),
|
||||
("long_poll_secs", timeouts.long_poll_secs),
|
||||
("websocket_write_secs", timeouts.websocket_write_secs),
|
||||
(
|
||||
"websocket_backpressure_secs",
|
||||
timeouts.websocket_backpressure_secs,
|
||||
),
|
||||
("websocket_eviction_secs", timeouts.websocket_eviction_secs),
|
||||
("bootstrap_lifetime_secs", timeouts.bootstrap_lifetime_secs),
|
||||
("reconnect_grace_secs", timeouts.reconnect_grace_secs),
|
||||
("http_idle_secs", timeouts.http_idle_secs),
|
||||
|
||||
@@ -18,7 +18,9 @@ pub(super) fn validate(policy: &WebDebugConfig, limits: &WebLimitsConfig) -> Res
|
||||
);
|
||||
}
|
||||
if policy.body_prefix_bytes > limits.max_body_bytes {
|
||||
return config_error("web.debug.body_prefix_bytes must not exceed web.limits.max_body_bytes");
|
||||
return config_error(
|
||||
"web.debug.body_prefix_bytes must not exceed web.limits.max_body_bytes",
|
||||
);
|
||||
}
|
||||
if policy.body_prefix_bytes > limits.debug_bytes_global
|
||||
|| policy.decoy_body_prefix_bytes > limits.debug_bytes_global
|
||||
|
||||
@@ -46,9 +46,7 @@ pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
|
||||
.checked_mul(WEB_DEBUG_GROUP_SCRATCH_BYTES)
|
||||
.and_then(|scratch| value.checked_add(scratch))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("web.debug reservations overflowed usize".to_string())
|
||||
})?;
|
||||
.ok_or_else(|| ProxyError::Config("web.debug reservations overflowed usize".to_string()))?;
|
||||
let reserved = limits
|
||||
.pending_bytes_global
|
||||
.checked_add(limits.max_body_bytes_global)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
use super::*;
|
||||
|
||||
const MAX_WEBSOCKET_BATCH_BYTES: usize = 2 * 1024 * 1024;
|
||||
const WEBSOCKET_IO_BUFFER_BYTES: usize = 64 * 1024;
|
||||
const WEBSOCKET_DRIVER_OVERHEAD_BYTES: usize = 4 * 1024;
|
||||
const WEBSOCKET_FRAME_OVERHEAD_BYTES: usize = 14;
|
||||
|
||||
/// Validates WebSocket admission, memory, and deadline invariants.
|
||||
pub(super) fn validate(
|
||||
carrier: WebCarrier,
|
||||
limits: &WebLimitsConfig,
|
||||
timeouts: &WebTimeoutsConfig,
|
||||
) -> Result<()> {
|
||||
if !(1..100).contains(&limits.websocket_admission_watermark_pct)
|
||||
|| !(1..100).contains(&limits.websocket_eviction_watermark_pct)
|
||||
|| limits.websocket_admission_watermark_pct >= limits.websocket_eviction_watermark_pct
|
||||
{
|
||||
return config_error(
|
||||
"web.limits WebSocket watermarks must satisfy 1 <= admission < eviction < 100",
|
||||
);
|
||||
}
|
||||
if limits.websocket_bytes_global == 0 {
|
||||
return config_error("web.limits.websocket_bytes_global must be > 0");
|
||||
}
|
||||
if timeouts.websocket_eviction_secs > timeouts.websocket_write_secs {
|
||||
return config_error(
|
||||
"web.timeouts.websocket_eviction_secs must not exceed websocket_write_secs",
|
||||
);
|
||||
}
|
||||
if !carrier.uses_websocket() {
|
||||
return Ok(());
|
||||
}
|
||||
if limits.carrier_batch_bytes > MAX_WEBSOCKET_BATCH_BYTES {
|
||||
return config_error(
|
||||
"WebSocket carriers require web.limits.carrier_batch_bytes <= 2097152",
|
||||
);
|
||||
}
|
||||
if limits.websocket_http_connection_reserve == 0
|
||||
|| limits.websocket_http_connection_reserve >= limits.max_http_connections
|
||||
{
|
||||
return config_error(
|
||||
"WebSocket carriers require websocket_http_connection_reserve within [1, max_http_connections)",
|
||||
);
|
||||
}
|
||||
let socket_base = WEBSOCKET_IO_BUFFER_BYTES
|
||||
.checked_mul(2)
|
||||
.and_then(|value| value.checked_add(WEBSOCKET_DRIVER_OVERHEAD_BYTES))
|
||||
.ok_or_else(|| ProxyError::Config("WebSocket base reservation overflowed usize".into()))?;
|
||||
let minimum_websocket_progress = limits
|
||||
.carrier_batch_bytes
|
||||
.checked_add(WEBSOCKET_FRAME_OVERHEAD_BYTES)
|
||||
.and_then(|value| value.checked_mul(2))
|
||||
.and_then(|value| value.checked_add(socket_base))
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("WebSocket progress reservation overflowed usize".into())
|
||||
})?;
|
||||
if limits.websocket_bytes_global < minimum_websocket_progress {
|
||||
return config_error(
|
||||
"web.limits.websocket_bytes_global must preserve one socket read and write",
|
||||
);
|
||||
}
|
||||
let data_bytes = limits
|
||||
.pending_bytes_global
|
||||
.saturating_sub(limits.control_bytes_global);
|
||||
let queue_progress = limits
|
||||
.max_body_bytes
|
||||
.checked_add(
|
||||
limits
|
||||
.max_frames_per_body
|
||||
.checked_mul(WEB_QUEUE_ITEM_COST)
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("WEB queue progress reservation overflowed usize".into())
|
||||
})?,
|
||||
)
|
||||
.and_then(|value| value.checked_add(limits.carrier_batch_bytes))
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("WEB queue progress reservation overflowed usize".into())
|
||||
})?;
|
||||
if limits.websocket_bytes_global > data_bytes.saturating_sub(queue_progress) {
|
||||
return config_error("web.limits.websocket_bytes_global must leave bounded queue progress");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -64,10 +64,13 @@ fn web_debug_table_uses_debug_name_and_bounded_defaults() {
|
||||
assert_eq!(config.web.debug.default_window_secs, 180);
|
||||
assert_eq!(config.web.debug.max_window_secs, 900);
|
||||
|
||||
let old_name = format!("[general]\nconfig_strict = true\n{}", WEB_CONFIG.replace(
|
||||
"[[web.vhosts]]",
|
||||
"[web.trace]\nenabled = true\n\n[[web.vhosts]]",
|
||||
));
|
||||
let old_name = format!(
|
||||
"[general]\nconfig_strict = true\n{}",
|
||||
WEB_CONFIG.replace(
|
||||
"[[web.vhosts]]",
|
||||
"[web.trace]\nenabled = true\n\n[[web.vhosts]]",
|
||||
)
|
||||
);
|
||||
let error = load_config_error_from_temp_toml(&old_name);
|
||||
assert!(error.contains("web.trace"));
|
||||
}
|
||||
@@ -150,3 +153,52 @@ fn web_ipv6_decoy_uses_a_valid_http_authority() {
|
||||
};
|
||||
assert_eq!(authority, "[::1]:18081");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_carriers_build_runtime_profiles_with_bounded_defaults() {
|
||||
for (name, carrier) in [
|
||||
("websocket", WebCarrier::Websocket),
|
||||
("websocket-lanes", WebCarrier::WebsocketLanes),
|
||||
] {
|
||||
let configured = WEB_CONFIG.replace("https-lanes", name);
|
||||
let config = load_config_from_temp_toml(&configured);
|
||||
let profile = &config.web.runtime.unwrap().profiles[0];
|
||||
assert_eq!(profile.carrier, carrier);
|
||||
assert_eq!(config.web.limits.websocket_bytes_global, 256 * 1024 * 1024);
|
||||
assert_eq!(config.web.limits.websocket_admission_watermark_pct, 75);
|
||||
assert_eq!(config.web.limits.websocket_eviction_watermark_pct, 90);
|
||||
assert_eq!(config.web.limits.websocket_http_connection_reserve, 64);
|
||||
assert_eq!(config.web.timeouts.websocket_write_secs, 30);
|
||||
assert_eq!(config.web.timeouts.websocket_backpressure_secs, 30);
|
||||
assert_eq!(config.web.timeouts.websocket_eviction_secs, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_limits_reject_ambiguous_or_nonprogressing_policy() {
|
||||
let reversed_watermarks = WEB_CONFIG.replace(
|
||||
"carrier = \"https-lanes\"",
|
||||
"carrier = \"websocket\"\n\n[web.limits]\nwebsocket_admission_watermark_pct = 90\nwebsocket_eviction_watermark_pct = 75",
|
||||
);
|
||||
assert!(
|
||||
load_config_error_from_temp_toml(&reversed_watermarks).contains("WebSocket watermarks")
|
||||
);
|
||||
|
||||
let no_http_reserve = WEB_CONFIG.replace(
|
||||
"carrier = \"https-lanes\"",
|
||||
"carrier = \"websocket\"\n\n[web.limits]\nwebsocket_http_connection_reserve = 0",
|
||||
);
|
||||
assert!(
|
||||
load_config_error_from_temp_toml(&no_http_reserve)
|
||||
.contains("websocket_http_connection_reserve")
|
||||
);
|
||||
|
||||
let oversized_batch = WEB_CONFIG.replace(
|
||||
"carrier = \"https-lanes\"",
|
||||
"carrier = \"websocket\"\n\n[web.limits]\nmax_body_bytes = 4194304\ncarrier_batch_bytes = 4194304\nmax_body_readers = 16",
|
||||
);
|
||||
assert!(
|
||||
load_config_error_from_temp_toml(&oversized_batch)
|
||||
.contains("carrier_batch_bytes <= 2097152")
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -54,12 +54,12 @@ pub use web::{
|
||||
WebCarrier, WebConfig, WebDecoyConfig, WebLimitsConfig, WebProfileConfig, WebSecretMode,
|
||||
WebTimeoutsConfig, WebVhostConfig,
|
||||
};
|
||||
pub use web_debug::{WebDebugBodyCapture, WebDebugConfig};
|
||||
pub(crate) use web_debug::web_debug_fits_limits;
|
||||
pub(crate) use web::{
|
||||
WebRuntimeConfig, WebRuntimeDecoy, WebRuntimeProfile, WebRuntimeVhost, WebStaticAsset,
|
||||
WebStaticSite,
|
||||
};
|
||||
pub(crate) use web_debug::web_debug_fits_limits;
|
||||
pub use web_debug::{WebDebugBodyCapture, WebDebugConfig};
|
||||
|
||||
fn default_quota_state_path() -> PathBuf {
|
||||
PathBuf::from("telemt.limit.json")
|
||||
|
||||
+65
-1
@@ -18,7 +18,7 @@ pub enum WebSecretMode {
|
||||
Dd,
|
||||
}
|
||||
|
||||
/// HTTP carrier selected for newly issued WEB bridge sessions.
|
||||
/// Carrier selected for newly issued WEB bridge sessions.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum WebCarrier {
|
||||
@@ -27,6 +27,10 @@ pub enum WebCarrier {
|
||||
Https,
|
||||
/// Give every logical stream independent HTTPS sequencing and polling state.
|
||||
HttpsLanes,
|
||||
/// Multiplex all logical streams over one ordered WebSocket.
|
||||
Websocket,
|
||||
/// Give every logical stream an independently owned WebSocket lane.
|
||||
WebsocketLanes,
|
||||
}
|
||||
|
||||
impl WebCarrier {
|
||||
@@ -35,8 +39,25 @@ impl WebCarrier {
|
||||
match self {
|
||||
Self::Https => "https",
|
||||
Self::HttpsLanes => "https-lanes",
|
||||
Self::Websocket => "websocket",
|
||||
Self::WebsocketLanes => "websocket-lanes",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether one carrier owns independent state per logical stream.
|
||||
pub(crate) const fn uses_lanes(self) -> bool {
|
||||
matches!(self, Self::HttpsLanes | Self::WebsocketLanes)
|
||||
}
|
||||
|
||||
/// Returns whether carrier messages use RFC 6455 instead of HTTP bodies.
|
||||
pub(crate) const fn uses_websocket(self) -> bool {
|
||||
matches!(self, Self::Websocket | Self::WebsocketLanes)
|
||||
}
|
||||
|
||||
/// Returns whether all logical streams share one carrier state machine.
|
||||
pub(crate) const fn is_multiplexed(self) -> bool {
|
||||
matches!(self, Self::Https | Self::Websocket)
|
||||
}
|
||||
}
|
||||
|
||||
/// One access user explicitly exposed through a WEB virtual host.
|
||||
@@ -114,6 +135,18 @@ pub struct WebLimitsConfig {
|
||||
/// Process-wide concurrently executing HTTP handler ceiling.
|
||||
#[serde(default = "default_web_max_http_handlers")]
|
||||
pub max_http_handlers: usize,
|
||||
/// Process-wide transient WebSocket byte sub-budget inside pending bytes.
|
||||
#[serde(default = "default_web_websocket_bytes_global")]
|
||||
pub websocket_bytes_global: usize,
|
||||
/// WebSocket usage percentage above which ordinary admission uses replacement.
|
||||
#[serde(default = "default_web_websocket_admission_watermark_pct")]
|
||||
pub websocket_admission_watermark_pct: u8,
|
||||
/// WebSocket usage percentage that triggers pressure eviction.
|
||||
#[serde(default = "default_web_websocket_eviction_watermark_pct")]
|
||||
pub websocket_eviction_watermark_pct: u8,
|
||||
/// Accepted HTTP connections that WebSocket upgrades must leave available.
|
||||
#[serde(default = "default_web_websocket_http_connection_reserve")]
|
||||
pub websocket_http_connection_reserve: usize,
|
||||
/// Process-wide concurrently collected request body ceiling.
|
||||
#[serde(default = "default_web_max_body_readers")]
|
||||
pub max_body_readers: usize,
|
||||
@@ -216,6 +249,10 @@ impl Default for WebLimitsConfig {
|
||||
max_frames_per_body: default_web_max_frames_per_body(),
|
||||
max_http_connections: default_web_max_http_connections(),
|
||||
max_http_handlers: default_web_max_http_handlers(),
|
||||
websocket_bytes_global: default_web_websocket_bytes_global(),
|
||||
websocket_admission_watermark_pct: default_web_websocket_admission_watermark_pct(),
|
||||
websocket_eviction_watermark_pct: default_web_websocket_eviction_watermark_pct(),
|
||||
websocket_http_connection_reserve: default_web_websocket_http_connection_reserve(),
|
||||
max_body_readers: default_web_max_body_readers(),
|
||||
max_body_bytes_global: default_web_max_body_bytes_global(),
|
||||
max_sessions_global: default_web_max_sessions_global(),
|
||||
@@ -265,6 +302,15 @@ pub struct WebTimeoutsConfig {
|
||||
/// Maximum wait for one empty downlink long poll.
|
||||
#[serde(default = "default_web_long_poll_timeout_secs")]
|
||||
pub long_poll_secs: u64,
|
||||
/// Maximum wait for one WebSocket write to complete.
|
||||
#[serde(default = "default_web_websocket_write_secs")]
|
||||
pub websocket_write_secs: u64,
|
||||
/// Maximum wait for WebSocket queue or byte-budget progress.
|
||||
#[serde(default = "default_web_websocket_backpressure_secs")]
|
||||
pub websocket_backpressure_secs: u64,
|
||||
/// Maximum graceful close wait for an evicted WebSocket.
|
||||
#[serde(default = "default_web_websocket_eviction_secs")]
|
||||
pub websocket_eviction_secs: u64,
|
||||
/// Lifetime of an unused bootstrap credential and closed-token replay marker.
|
||||
#[serde(default = "default_web_bootstrap_lifetime_secs")]
|
||||
pub bootstrap_lifetime_secs: u64,
|
||||
@@ -289,6 +335,9 @@ impl Default for WebTimeoutsConfig {
|
||||
body_secs: default_web_body_timeout_secs(),
|
||||
stream_handshake_secs: default_web_stream_handshake_timeout_secs(),
|
||||
long_poll_secs: default_web_long_poll_timeout_secs(),
|
||||
websocket_write_secs: default_web_websocket_write_secs(),
|
||||
websocket_backpressure_secs: default_web_websocket_backpressure_secs(),
|
||||
websocket_eviction_secs: default_web_websocket_eviction_secs(),
|
||||
bootstrap_lifetime_secs: default_web_bootstrap_lifetime_secs(),
|
||||
reconnect_grace_secs: default_web_reconnect_grace_secs(),
|
||||
http_idle_secs: default_web_http_idle_secs(),
|
||||
@@ -418,6 +467,14 @@ macro_rules! u32_default {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! u8_default {
|
||||
($name:ident, $value:expr) => {
|
||||
fn $name() -> u8 {
|
||||
$value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! u64_default {
|
||||
($name:ident, $value:expr) => {
|
||||
fn $name() -> u64 {
|
||||
@@ -433,6 +490,10 @@ usize_default!(default_web_carrier_batch_bytes, 2 * 1024 * 1024);
|
||||
usize_default!(default_web_max_frames_per_body, 4096);
|
||||
usize_default!(default_web_max_http_connections, 1024);
|
||||
usize_default!(default_web_max_http_handlers, 512);
|
||||
usize_default!(default_web_websocket_bytes_global, 256 * 1024 * 1024);
|
||||
u8_default!(default_web_websocket_admission_watermark_pct, 75);
|
||||
u8_default!(default_web_websocket_eviction_watermark_pct, 90);
|
||||
usize_default!(default_web_websocket_http_connection_reserve, 64);
|
||||
usize_default!(default_web_max_body_readers, 32);
|
||||
usize_default!(default_web_max_body_bytes_global, 64 * 1024 * 1024);
|
||||
usize_default!(default_web_max_sessions_global, 128);
|
||||
@@ -467,6 +528,9 @@ u64_default!(default_web_header_timeout_secs, 10);
|
||||
u64_default!(default_web_body_timeout_secs, 30);
|
||||
u64_default!(default_web_stream_handshake_timeout_secs, 10);
|
||||
u64_default!(default_web_long_poll_timeout_secs, 25);
|
||||
u64_default!(default_web_websocket_write_secs, 30);
|
||||
u64_default!(default_web_websocket_backpressure_secs, 30);
|
||||
u64_default!(default_web_websocket_eviction_secs, 1);
|
||||
u64_default!(default_web_bootstrap_lifetime_secs, 120);
|
||||
u64_default!(default_web_reconnect_grace_secs, 120);
|
||||
u64_default!(default_web_http_idle_secs, 75);
|
||||
|
||||
@@ -90,10 +90,7 @@ fn default_max_window_secs() -> u64 {
|
||||
}
|
||||
|
||||
/// Checks whether a hot debug policy fits restart-frozen process capacities.
|
||||
pub(crate) fn web_debug_fits_limits(
|
||||
policy: &WebDebugConfig,
|
||||
limits: &WebLimitsConfig,
|
||||
) -> bool {
|
||||
pub(crate) fn web_debug_fits_limits(policy: &WebDebugConfig, limits: &WebLimitsConfig) -> bool {
|
||||
policy.body_prefix_bytes <= limits.max_body_bytes
|
||||
&& policy.body_prefix_bytes <= limits.debug_bytes_global
|
||||
&& policy.decoy_body_prefix_bytes <= limits.debug_bytes_global
|
||||
|
||||
Reference in New Issue
Block a user