fix(server): clean up bridge and hybrid shutdown handling

This commit is contained in:
2026-04-06 10:15:37 +00:00
parent a293986d6d
commit d4bad38908
5 changed files with 111 additions and 13 deletions

View File

@@ -173,6 +173,14 @@ pub enum ForwardingEngine {
Testing,
}
/// Info needed to tear down bridge infrastructure on stop().
pub struct BridgeCleanupInfo {
pub physical_iface: String,
pub bridge_name: String,
pub host_ip: Ipv4Addr,
pub host_prefix: u8,
}
/// Shared server state.
pub struct ServerState {
pub config: ServerConfig,
@@ -189,6 +197,10 @@ pub struct ServerState {
pub tun_routes: RwLock<HashMap<Ipv4Addr, mpsc::Sender<Vec<u8>>>>,
/// Shutdown signal for the forwarding background task (TUN reader or NAT engine).
pub tun_shutdown: mpsc::Sender<()>,
/// Shutdown signal for the bridge engine (bridge/hybrid modes only).
pub bridge_shutdown: Option<mpsc::Sender<()>>,
/// Bridge teardown info (bridge/hybrid modes only).
pub bridge_cleanup: Option<BridgeCleanupInfo>,
}
/// The VPN server.
@@ -267,6 +279,9 @@ impl VpnServer {
Testing,
}
let mut bridge_cleanup_info: Option<BridgeCleanupInfo> = None;
let mut bridge_shut_tx: Option<mpsc::Sender<()>> = None;
let (setup, fwd_shutdown_tx) = match mode {
"tun" => {
let tun_config = TunConfig {
@@ -310,6 +325,13 @@ impl VpnServer {
info!("Bridge {} created: TAP={}, physical={}, IP={}/{}", bridge_name, tap_name, phys_iface, host_ip, host_prefix);
bridge_cleanup_info = Some(BridgeCleanupInfo {
physical_iface: phys_iface,
bridge_name: bridge_name.to_string(),
host_ip,
host_prefix,
});
let (packet_tx, packet_rx) = mpsc::channel::<Vec<u8>>(4096);
let (tx, rx) = mpsc::channel::<()>(1);
(ForwardingSetup::Bridge { packet_tx, packet_rx, tap_device, shutdown_rx: rx }, tx)
@@ -319,7 +341,7 @@ impl VpnServer {
// Socket engine setup
let (s_tx, s_rx) = mpsc::channel::<Vec<u8>>(4096);
let (_s_shut_tx, s_shut_rx) = mpsc::channel::<()>(1);
let (s_shut_tx, s_shut_rx) = mpsc::channel::<()>(1);
// Bridge engine setup
let phys_iface = match &config.bridge_physical_interface {
@@ -347,14 +369,20 @@ impl VpnServer {
info!("Hybrid mode: socket + bridge (TAP={}, physical={}, IP={}/{})", tap_name, phys_iface, host_ip, host_prefix);
// We use s_shut_tx as the main shutdown (it will trigger both)
let _ = b_shut_tx; // bridge shutdown handled separately
let (tx, _) = mpsc::channel::<()>(1);
bridge_cleanup_info = Some(BridgeCleanupInfo {
physical_iface: phys_iface,
bridge_name: bridge_name.to_string(),
host_ip,
host_prefix,
});
bridge_shut_tx = Some(b_shut_tx);
// Socket engine uses fwd_shutdown_tx (stored in state.tun_shutdown)
(ForwardingSetup::Hybrid {
socket_tx: s_tx, socket_rx: s_rx, socket_shutdown_rx: s_shut_rx,
bridge_tx: b_tx, bridge_rx: b_rx, bridge_shutdown_rx: b_shut_rx,
tap_device, routing_table,
}, tx)
}, s_shut_tx)
}
_ => {
info!("Forwarding disabled (testing/monitoring mode)");
@@ -365,7 +393,7 @@ impl VpnServer {
// Compute effective MTU from overhead
let overhead = TunnelOverhead::default_overhead();
let mtu_config = MtuConfig::new(overhead.effective_tun_mtu(1500).max(link_mtu));
let mtu_config = MtuConfig::new(overhead.effective_tun_mtu(1500).min(link_mtu));
// Build client registry from config
let registry = ClientRegistry::from_entries(
@@ -385,6 +413,8 @@ impl VpnServer {
forwarding_engine: Mutex::new(ForwardingEngine::Testing),
tun_routes: RwLock::new(HashMap::new()),
tun_shutdown: fwd_shutdown_tx,
bridge_shutdown: bridge_shut_tx,
bridge_cleanup: bridge_cleanup_info,
});
// Spawn the forwarding background task and set the engine
@@ -588,6 +618,43 @@ impl VpnServer {
let _ = state.tun_shutdown.send(()).await;
*state.forwarding_engine.lock().await = ForwardingEngine::Testing;
}
"bridge" => {
let _ = state.tun_shutdown.send(()).await;
*state.forwarding_engine.lock().await = ForwardingEngine::Testing;
// Restore host networking: move IP back and remove bridge
if let Some(ref cleanup) = state.bridge_cleanup {
if let Err(e) = crate::bridge::restore_host_ip(
&cleanup.physical_iface, &cleanup.bridge_name,
cleanup.host_ip, cleanup.host_prefix,
).await {
warn!("Failed to restore host IP: {}", e);
}
if let Err(e) = crate::bridge::remove_bridge(&cleanup.bridge_name).await {
warn!("Failed to remove bridge: {}", e);
}
}
}
"hybrid" => {
// Shut down socket (NAT) engine
let _ = state.tun_shutdown.send(()).await;
// Shut down bridge engine
if let Some(ref bridge_shut) = state.bridge_shutdown {
let _ = bridge_shut.send(()).await;
}
*state.forwarding_engine.lock().await = ForwardingEngine::Testing;
// Restore host networking: move IP back and remove bridge
if let Some(ref cleanup) = state.bridge_cleanup {
if let Err(e) = crate::bridge::restore_host_ip(
&cleanup.physical_iface, &cleanup.bridge_name,
cleanup.host_ip, cleanup.host_prefix,
).await {
warn!("Failed to restore host IP: {}", e);
}
if let Err(e) = crate::bridge::remove_bridge(&cleanup.bridge_name).await {
warn!("Failed to remove bridge: {}", e);
}
}
}
_ => {}
}
@@ -807,8 +874,11 @@ impl VpnServer {
vlan_id: partial.get("vlanId").and_then(|v| v.as_u64()).map(|v| v as u16),
};
// Add to registry
state.client_registry.write().await.add(entry.clone())?;
// Add to registry — release IP on failure to avoid pool leak
if let Err(e) = state.client_registry.write().await.add(entry.clone()) {
state.ip_pool.lock().await.release(&assigned_ip);
return Err(e);
}
// Register WG peer with the running WG listener (if active)
if self.wg_command_tx.is_some() {

View File

@@ -319,10 +319,12 @@ fn extract_peer_vpn_ip(allowed_ips: &[AllowedIp]) -> Option<Ipv4Addr> {
}
}
}
// Fallback: use the first IPv4 address from any prefix length
// Fallback: use the first non-unspecified IPv4 address from any prefix length
for aip in allowed_ips {
if let IpAddr::V4(v4) = aip.addr {
return Some(v4);
if !v4.is_unspecified() {
return Some(v4);
}
}
}
None