feat(server): add bridge forwarding mode and per-client destination policy overrides

This commit is contained in:
2026-03-31 21:34:49 +00:00
parent 17af7ab289
commit fdeba5eeb5
12 changed files with 583 additions and 25 deletions

View File

@@ -25,7 +25,7 @@ use crate::tunnel::{self, TunConfig};
const DEAD_PEER_TIMEOUT: Duration = Duration::from_secs(180);
/// Destination routing policy for VPN client traffic.
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DestinationPolicyConfig {
/// Default action: "forceTarget", "block", or "allow".
@@ -92,6 +92,17 @@ pub struct ServerConfig {
/// Defaults to ["0.0.0.0/0"] (full tunnel).
#[serde(alias = "clientAllowedIPs")]
pub client_allowed_ips: Option<Vec<String>>,
// Bridge mode configuration (forwarding_mode: "bridge")
/// LAN subnet CIDR for bridge mode (e.g. "192.168.1.0/24").
pub bridge_lan_subnet: Option<String>,
/// Physical network interface to bridge (e.g. "eth0"). Auto-detected if omitted.
pub bridge_physical_interface: Option<String>,
/// Start of VPN client IP range within the LAN subnet (host offset, e.g. 200).
pub bridge_ip_range_start: Option<u32>,
/// End of VPN client IP range within the LAN subnet (host offset, e.g. 250).
pub bridge_ip_range_end: Option<u32>,
}
/// Information about a connected client.
@@ -148,6 +159,8 @@ pub enum ForwardingEngine {
Tun(tokio::io::WriteHalf<tun::AsyncDevice>),
/// Userspace NAT — packets sent to smoltcp-based NAT engine via channel.
Socket(mpsc::Sender<Vec<u8>>),
/// L2 Bridge — packets sent to BridgeEngine via channel, bridged to host LAN.
Bridge(mpsc::Sender<Vec<u8>>),
/// Testing/monitoring — packets are counted but not forwarded.
Testing,
}
@@ -191,7 +204,15 @@ impl VpnServer {
anyhow::bail!("Server is already running");
}
let ip_pool = IpPool::new(&config.subnet)?;
let mode = config.forwarding_mode.as_deref().unwrap_or("testing");
let ip_pool = if mode == "bridge" {
let lan_subnet = config.bridge_lan_subnet.as_deref().unwrap_or(&config.subnet);
let range_start = config.bridge_ip_range_start.unwrap_or(200);
let range_end = config.bridge_ip_range_end.unwrap_or(250);
IpPool::new_with_range(lan_subnet, range_start, range_end)?
} else {
IpPool::new(&config.subnet)?
};
if config.enable_nat.unwrap_or(false) {
if let Err(e) = crate::network::enable_ip_forwarding() {
@@ -205,7 +226,6 @@ impl VpnServer {
}
let link_mtu = config.mtu.unwrap_or(1420);
let mode = config.forwarding_mode.as_deref().unwrap_or("testing");
let gateway_ip = ip_pool.gateway_addr();
// Create forwarding engine based on mode
@@ -220,6 +240,12 @@ impl VpnServer {
packet_rx: mpsc::Receiver<Vec<u8>>,
shutdown_rx: mpsc::Receiver<()>,
},
Bridge {
packet_tx: mpsc::Sender<Vec<u8>>,
packet_rx: mpsc::Receiver<Vec<u8>>,
tap_device: tun::AsyncDevice,
shutdown_rx: mpsc::Receiver<()>,
},
Testing,
}
@@ -243,6 +269,33 @@ impl VpnServer {
let (tx, rx) = mpsc::channel::<()>(1);
(ForwardingSetup::Socket { packet_tx, packet_rx, shutdown_rx: rx }, tx)
}
"bridge" => {
info!("Starting L2 bridge forwarding (requires CAP_NET_ADMIN)");
let phys_iface = match &config.bridge_physical_interface {
Some(i) => i.clone(),
None => crate::bridge::detect_default_interface().await?,
};
let (host_ip, host_prefix) = crate::bridge::get_interface_ip(&phys_iface).await?;
let bridge_name = "svpn_br0";
let tap_name = "svpn_tap0";
// Create TAP + bridge infrastructure
let tap_device = crate::bridge::create_tap(tap_name, link_mtu)?;
crate::bridge::create_bridge(bridge_name).await?;
crate::bridge::set_interface_up(bridge_name).await?;
crate::bridge::bridge_add_interface(bridge_name, tap_name).await?;
crate::bridge::set_interface_up(tap_name).await?;
crate::bridge::bridge_add_interface(bridge_name, &phys_iface).await?;
crate::bridge::migrate_host_ip_to_bridge(&phys_iface, bridge_name, host_ip, host_prefix).await?;
crate::bridge::enable_proxy_arp(bridge_name).await?;
info!("Bridge {} created: TAP={}, physical={}, IP={}/{}", bridge_name, tap_name, phys_iface, 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)
}
_ => {
info!("Forwarding disabled (testing/monitoring mode)");
let (tx, _rx) = mpsc::channel::<()>(1);
@@ -301,6 +354,15 @@ impl VpnServer {
}
});
}
ForwardingSetup::Bridge { packet_tx, packet_rx, tap_device, shutdown_rx } => {
*state.forwarding_engine.lock().await = ForwardingEngine::Bridge(packet_tx);
let bridge_engine = crate::bridge::BridgeEngine::new(state.clone());
tokio::spawn(async move {
if let Err(e) = bridge_engine.run(tap_device, packet_rx, shutdown_rx).await {
error!("Bridge engine error: {}", e);
}
});
}
ForwardingSetup::Testing => {}
}
@@ -1430,6 +1492,9 @@ async fn handle_client_connection(
ForwardingEngine::Socket(sender) => {
let _ = sender.try_send(buf[..len].to_vec());
}
ForwardingEngine::Bridge(sender) => {
let _ = sender.try_send(buf[..len].to_vec());
}
ForwardingEngine::Testing => {}
}
}