feat(rustproxy-passthrough): add selective connection recycling for route, security, and certificate updates

This commit is contained in:
2026-03-27 22:34:13 +00:00
parent 5be93c8d38
commit 3c24bf659b
9 changed files with 514 additions and 8 deletions

View File

@@ -356,12 +356,17 @@ impl RustProxy {
// Bind UDP ports (if any)
if !udp_ports.is_empty() {
let conn_tracker = self.listener_manager.as_ref().unwrap().conn_tracker().clone();
let tcp_mgr = self.listener_manager.as_ref().unwrap();
let conn_tracker = tcp_mgr.conn_tracker().clone();
let route_cancels = tcp_mgr.route_cancels().clone();
let connection_registry = tcp_mgr.connection_registry().clone();
let mut udp_mgr = UdpListenerManager::new(
Arc::clone(&*self.route_table.load()),
Arc::clone(&self.metrics),
conn_tracker,
self.cancel_token.clone(),
route_cancels,
connection_registry,
);
udp_mgr.set_proxy_ips(udp_proxy_ips.clone());
@@ -707,6 +712,9 @@ impl RustProxy {
.collect();
self.metrics.retain_backends(&active_backends);
// Capture old route manager for diff-based connection recycling
let old_manager = self.route_table.load_full();
// Atomically swap the route table
let new_manager = Arc::new(new_manager);
self.route_table.store(Arc::clone(&new_manager));
@@ -742,9 +750,47 @@ impl RustProxy {
listener.update_route_manager(Arc::clone(&new_manager));
// Cancel connections on routes that were removed or disabled
listener.invalidate_removed_routes(&active_route_ids);
// Clean up registry entries for removed routes
listener.connection_registry().cleanup_removed_routes(&active_route_ids);
// Prune HTTP proxy caches (rate limiters, regex cache, round-robin counters)
listener.prune_http_proxy_caches(&active_route_ids);
// Diff-based connection recycling for changed routes
{
let registry = listener.connection_registry();
for new_route in &routes {
let new_id = match &new_route.id {
Some(id) => id.as_str(),
None => continue,
};
// Find corresponding old route
let old_route = old_manager.routes().iter().find(|r| {
r.id.as_deref() == Some(new_id)
});
let old_route = match old_route {
Some(r) => r,
None => continue, // new route, no existing connections to recycle
};
// Security diff: re-evaluate existing connections' IPs
let old_sec = serde_json::to_string(&old_route.security).ok();
let new_sec = serde_json::to_string(&new_route.security).ok();
if old_sec != new_sec {
if let Some(ref security) = new_route.security {
registry.recycle_for_security_change(new_id, security);
}
// If security removed entirely (became more permissive), no recycling needed
}
// Action diff (targets, TLS mode, etc.): recycle all connections on route
let old_action = serde_json::to_string(&old_route.action).ok();
let new_action = serde_json::to_string(&new_route.action).ok();
if old_action != new_action {
registry.recycle_for_route_change(new_id);
}
}
}
// Add new ports
for port in &new_ports {
if !old_ports.contains(port) {
@@ -787,12 +833,16 @@ impl RustProxy {
if self.udp_listener_manager.is_none() {
if let Some(ref listener) = self.listener_manager {
let conn_tracker = listener.conn_tracker().clone();
let route_cancels = listener.route_cancels().clone();
let connection_registry = listener.connection_registry().clone();
let conn_config = Self::build_connection_config(&self.options);
let mut udp_mgr = UdpListenerManager::new(
Arc::clone(&new_manager),
Arc::clone(&self.metrics),
conn_tracker,
self.cancel_token.clone(),
route_cancels,
connection_registry,
);
udp_mgr.set_proxy_ips(conn_config.proxy_ips);
self.udp_listener_manager = Some(udp_mgr);
@@ -1096,6 +1146,10 @@ impl RustProxy {
}
/// Load a certificate for a domain and hot-swap the TLS configuration.
///
/// If the cert PEM differs from the currently loaded cert for this domain,
/// existing connections for the domain are gracefully recycled (GOAWAY for
/// HTTP/2, Connection: close for HTTP/1.1, graceful FIN for TCP).
pub async fn load_certificate(
&mut self,
domain: &str,
@@ -1105,6 +1159,12 @@ impl RustProxy {
) -> Result<()> {
info!("Loading certificate for domain: {}", domain);
// Check if the cert actually changed (for selective connection recycling)
let cert_changed = self.loaded_certs
.get(domain)
.map(|existing| existing.cert_pem != cert_pem)
.unwrap_or(false); // new domain = no existing connections to recycle
// Store in cert manager if available
if let Some(ref cm_arc) = self.cert_manager {
let now = std::time::SystemTime::now()
@@ -1153,6 +1213,13 @@ impl RustProxy {
}
}
// Recycle existing connections if cert actually changed
if cert_changed {
if let Some(ref listener) = self.listener_manager {
listener.connection_registry().recycle_for_cert_change(domain);
}
}
info!("Certificate loaded and TLS config updated for {}", domain);
Ok(())
}