fix: harden tunnel security and timeout handling

This commit is contained in:
fawney19
2026-05-23 14:17:04 +08:00
parent 74f7348529
commit 6447fda852
19 changed files with 571 additions and 92 deletions

View File

@@ -9,11 +9,12 @@ use uuid::Uuid;
use super::types::{
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
log_reported_tunnel_error_event, normalize_proxy_metadata,
reconcile_remote_config_after_heartbeat, ProxyNodeEventQuery, ProxyNodeHeartbeatMutation,
ProxyNodeManualCreateMutation, ProxyNodeManualUpdateMutation, ProxyNodeMetricsCleanupSummary,
ProxyNodeMetricsStep, ProxyNodeReadRepository, ProxyNodeRegistrationMutation,
ProxyNodeRemoteConfigMutation, ProxyNodeTrafficMutation, ProxyNodeTunnelStatusMutation,
ProxyNodeWriteRepository, StoredProxyFleetMetricsBucket, StoredProxyNode, StoredProxyNodeEvent,
preserve_proxy_metadata_tunnel_security, reconcile_remote_config_after_heartbeat,
ProxyNodeEventQuery, ProxyNodeHeartbeatMutation, ProxyNodeManualCreateMutation,
ProxyNodeManualUpdateMutation, ProxyNodeMetricsCleanupSummary, ProxyNodeMetricsStep,
ProxyNodeReadRepository, ProxyNodeRegistrationMutation, ProxyNodeRemoteConfigMutation,
ProxyNodeTrafficMutation, ProxyNodeTunnelStatusMutation, ProxyNodeWriteRepository,
StoredProxyFleetMetricsBucket, StoredProxyNode, StoredProxyNodeEvent,
StoredProxyNodeMetricsBucket, TunnelMetricsSample, PROXY_NODE_EVENT_TYPE_TUNNEL_ERROR,
};
use crate::DataLayerError;
@@ -590,6 +591,10 @@ impl ProxyNodeWriteRepository for InMemoryProxyNodeRepository {
mutation.proxy_metadata.as_ref(),
mutation.proxy_version.as_deref(),
);
let normalized_proxy_metadata = preserve_proxy_metadata_tunnel_security(
previous_proxy_metadata.as_ref(),
normalized_proxy_metadata,
);
if let Some(value) = normalized_proxy_metadata {
node.proxy_metadata = Some(value);
}

View File

@@ -4,11 +4,12 @@ use sqlx::{mysql::MySqlRow, Row};
use super::types::{
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
log_reported_tunnel_error_event, normalize_proxy_metadata,
reconcile_remote_config_after_heartbeat, ProxyNodeEventQuery, ProxyNodeHeartbeatMutation,
ProxyNodeManualCreateMutation, ProxyNodeManualUpdateMutation, ProxyNodeMetricsCleanupSummary,
ProxyNodeMetricsStep, ProxyNodeReadRepository, ProxyNodeRegistrationMutation,
ProxyNodeRemoteConfigMutation, ProxyNodeTrafficMutation, ProxyNodeTunnelStatusMutation,
ProxyNodeWriteRepository, StoredProxyFleetMetricsBucket, StoredProxyNode, StoredProxyNodeEvent,
preserve_proxy_metadata_tunnel_security, reconcile_remote_config_after_heartbeat,
ProxyNodeEventQuery, ProxyNodeHeartbeatMutation, ProxyNodeManualCreateMutation,
ProxyNodeManualUpdateMutation, ProxyNodeMetricsCleanupSummary, ProxyNodeMetricsStep,
ProxyNodeReadRepository, ProxyNodeRegistrationMutation, ProxyNodeRemoteConfigMutation,
ProxyNodeTrafficMutation, ProxyNodeTunnelStatusMutation, ProxyNodeWriteRepository,
StoredProxyFleetMetricsBucket, StoredProxyNode, StoredProxyNodeEvent,
StoredProxyNodeMetricsBucket, PROXY_NODE_EVENT_TYPE_TUNNEL_ERROR,
};
use crate::driver::mysql::MysqlPool;
@@ -779,10 +780,15 @@ WHERE is_manual = 0
if let Some(value) = mutation.avg_latency_ms {
node.avg_latency_ms = Some(value);
}
if let Some(value) = normalize_proxy_metadata(
let normalized_proxy_metadata = normalize_proxy_metadata(
mutation.proxy_metadata.as_ref(),
mutation.proxy_version.as_deref(),
) {
);
let normalized_proxy_metadata = preserve_proxy_metadata_tunnel_security(
previous_proxy_metadata.as_ref(),
normalized_proxy_metadata,
);
if let Some(value) = normalized_proxy_metadata {
node.proxy_metadata = Some(value);
}
if let Some(value) = mutation.total_requests_delta.filter(|value| *value > 0) {

View File

@@ -6,11 +6,12 @@ use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
use super::types::{
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
log_reported_tunnel_error_event, normalize_proxy_metadata,
reconcile_remote_config_after_heartbeat, ProxyNodeEventQuery, ProxyNodeHeartbeatMutation,
ProxyNodeManualCreateMutation, ProxyNodeManualUpdateMutation, ProxyNodeMetricsCleanupSummary,
ProxyNodeMetricsStep, ProxyNodeReadRepository, ProxyNodeRegistrationMutation,
ProxyNodeRemoteConfigMutation, ProxyNodeTrafficMutation, ProxyNodeTunnelStatusMutation,
ProxyNodeWriteRepository, StoredProxyFleetMetricsBucket, StoredProxyNode, StoredProxyNodeEvent,
preserve_proxy_metadata_tunnel_security, reconcile_remote_config_after_heartbeat,
ProxyNodeEventQuery, ProxyNodeHeartbeatMutation, ProxyNodeManualCreateMutation,
ProxyNodeManualUpdateMutation, ProxyNodeMetricsCleanupSummary, ProxyNodeMetricsStep,
ProxyNodeReadRepository, ProxyNodeRegistrationMutation, ProxyNodeRemoteConfigMutation,
ProxyNodeTrafficMutation, ProxyNodeTunnelStatusMutation, ProxyNodeWriteRepository,
StoredProxyFleetMetricsBucket, StoredProxyNode, StoredProxyNodeEvent,
StoredProxyNodeMetricsBucket, TunnelMetricsSample, PROXY_NODE_EVENT_TYPE_TUNNEL_ERROR,
};
use crate::{
@@ -1174,6 +1175,10 @@ impl ProxyNodeWriteRepository for SqlxProxyNodeRepository {
mutation.proxy_metadata.as_ref(),
mutation.proxy_version.as_deref(),
);
let normalized_proxy_metadata = preserve_proxy_metadata_tunnel_security(
existing.proxy_metadata.as_ref(),
normalized_proxy_metadata,
);
sqlx::query(APPLY_HEARTBEAT_SQL)
.bind(&mutation.node_id)

View File

@@ -4,11 +4,12 @@ use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite};
use super::types::{
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
log_reported_tunnel_error_event, normalize_proxy_metadata,
reconcile_remote_config_after_heartbeat, ProxyNodeEventQuery, ProxyNodeHeartbeatMutation,
ProxyNodeManualCreateMutation, ProxyNodeManualUpdateMutation, ProxyNodeMetricsCleanupSummary,
ProxyNodeMetricsStep, ProxyNodeReadRepository, ProxyNodeRegistrationMutation,
ProxyNodeRemoteConfigMutation, ProxyNodeTrafficMutation, ProxyNodeTunnelStatusMutation,
ProxyNodeWriteRepository, StoredProxyFleetMetricsBucket, StoredProxyNode, StoredProxyNodeEvent,
preserve_proxy_metadata_tunnel_security, reconcile_remote_config_after_heartbeat,
ProxyNodeEventQuery, ProxyNodeHeartbeatMutation, ProxyNodeManualCreateMutation,
ProxyNodeManualUpdateMutation, ProxyNodeMetricsCleanupSummary, ProxyNodeMetricsStep,
ProxyNodeReadRepository, ProxyNodeRegistrationMutation, ProxyNodeRemoteConfigMutation,
ProxyNodeTrafficMutation, ProxyNodeTunnelStatusMutation, ProxyNodeWriteRepository,
StoredProxyFleetMetricsBucket, StoredProxyNode, StoredProxyNodeEvent,
StoredProxyNodeMetricsBucket, PROXY_NODE_EVENT_TYPE_TUNNEL_ERROR,
};
use crate::driver::sqlite::SqlitePool;
@@ -770,10 +771,15 @@ WHERE is_manual = 0
if let Some(value) = mutation.avg_latency_ms {
node.avg_latency_ms = Some(value);
}
if let Some(value) = normalize_proxy_metadata(
let normalized_proxy_metadata = normalize_proxy_metadata(
mutation.proxy_metadata.as_ref(),
mutation.proxy_version.as_deref(),
) {
);
let normalized_proxy_metadata = preserve_proxy_metadata_tunnel_security(
previous_proxy_metadata.as_ref(),
normalized_proxy_metadata,
);
if let Some(value) = normalized_proxy_metadata {
node.proxy_metadata = Some(value);
}
if let Some(value) = mutation.total_requests_delta.filter(|value| *value > 0) {

View File

@@ -485,6 +485,34 @@ pub fn normalize_proxy_metadata(
}
}
pub fn preserve_proxy_metadata_tunnel_security(
previous_proxy_metadata: Option<&Value>,
next_proxy_metadata: Option<Value>,
) -> Option<Value> {
let Some(tunnel_security) = previous_proxy_metadata
.and_then(|value| value.get("tunnel_security"))
.filter(|value| value.is_object())
.cloned()
else {
return next_proxy_metadata;
};
match next_proxy_metadata {
Some(Value::Object(mut metadata)) => {
metadata
.entry("tunnel_security".to_string())
.or_insert(tunnel_security);
Some(Value::Object(metadata))
}
Some(value) => Some(value),
None => {
let mut metadata = serde_json::Map::new();
metadata.insert("tunnel_security".to_string(), tunnel_security);
Some(Value::Object(metadata))
}
}
}
fn extract_tunnel_metrics_counters(
proxy_metadata: Option<&Value>,
) -> Option<TunnelMetricsCounters> {
@@ -772,10 +800,10 @@ mod tests {
use super::{
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
normalize_proxy_node_scheduling_state, proxy_node_accepts_new_tunnels,
proxy_reported_version, reconcile_remote_config_after_heartbeat,
remote_config_scheduling_state, remote_config_upgrade_target, ProxyNodeMetricsStep,
StoredProxyNode,
normalize_proxy_node_scheduling_state, preserve_proxy_metadata_tunnel_security,
proxy_node_accepts_new_tunnels, proxy_reported_version,
reconcile_remote_config_after_heartbeat, remote_config_scheduling_state,
remote_config_upgrade_target, ProxyNodeMetricsStep, StoredProxyNode,
};
#[test]
@@ -934,6 +962,40 @@ mod tests {
);
}
#[test]
fn preserves_secure_tunnel_metadata_across_heartbeat_metadata_refresh() {
let previous = json!({
"version": "1.0.0",
"tunnel_security": {
"mode": "non_tls_required",
"encryption_key": "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
}
});
let next = json!({
"version": "1.0.1",
"tunnel_metrics": {"connect_successes": 1}
});
let merged = preserve_proxy_metadata_tunnel_security(Some(&previous), Some(next))
.expect("metadata should remain present");
assert_eq!(
merged
.pointer("/tunnel_security/mode")
.and_then(|v| v.as_str()),
Some("non_tls_required")
);
assert_eq!(
merged
.pointer("/tunnel_security/encryption_key")
.and_then(|v| v.as_str()),
Some("BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=")
);
assert_eq!(
merged.pointer("/tunnel_metrics/connect_successes"),
Some(&json!(1))
);
}
#[test]
fn maps_timestamps_to_metric_buckets() {
assert_eq!(