353 lines
13 KiB
Rust
353 lines
13 KiB
Rust
|
|
//! L2 Bridge forwarding engine.
|
||
|
|
//!
|
||
|
|
//! Provides server-side bridging: receives L3 IP packets from VPN clients,
|
||
|
|
//! wraps them in Ethernet frames, and injects them into a Linux bridge
|
||
|
|
//! connected to the host's physical network interface.
|
||
|
|
//!
|
||
|
|
//! Return traffic from the bridge is stripped of its Ethernet header and
|
||
|
|
//! routed back to VPN clients via `tun_routes`.
|
||
|
|
|
||
|
|
use anyhow::Result;
|
||
|
|
use std::collections::HashMap;
|
||
|
|
use std::net::Ipv4Addr;
|
||
|
|
use std::sync::Arc;
|
||
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||
|
|
use tokio::sync::mpsc;
|
||
|
|
use tracing::{debug, error, info, warn};
|
||
|
|
|
||
|
|
use crate::server::ServerState;
|
||
|
|
|
||
|
|
/// Configuration for the bridge forwarding engine.
|
||
|
|
pub struct BridgeConfig {
|
||
|
|
/// TAP device name (e.g., "svpn_tap0")
|
||
|
|
pub tap_name: String,
|
||
|
|
/// Linux bridge name (e.g., "svpn_br0")
|
||
|
|
pub bridge_name: String,
|
||
|
|
/// Physical interface to bridge (e.g., "eth0")
|
||
|
|
pub physical_interface: String,
|
||
|
|
/// Gateway IP on the bridge (host's LAN IP)
|
||
|
|
pub gateway_ip: Ipv4Addr,
|
||
|
|
/// Subnet prefix length (e.g., 24)
|
||
|
|
pub prefix_len: u8,
|
||
|
|
/// MTU for the TAP device
|
||
|
|
pub mtu: u16,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ethernet frame constants
|
||
|
|
const ETH_HEADER_LEN: usize = 14;
|
||
|
|
const ETH_TYPE_IPV4: [u8; 2] = [0x08, 0x00];
|
||
|
|
const ETH_TYPE_ARP: [u8; 2] = [0x08, 0x06];
|
||
|
|
const BROADCAST_MAC: [u8; 6] = [0xff; 6];
|
||
|
|
|
||
|
|
/// Generate a deterministic locally-administered MAC from an IPv4 address.
|
||
|
|
/// Uses prefix 02:53:56 (locally administered, "SVP" in hex-ish).
|
||
|
|
fn mac_from_ip(ip: Ipv4Addr) -> [u8; 6] {
|
||
|
|
let octets = ip.octets();
|
||
|
|
[0x02, 0x53, 0x56, octets[1], octets[2], octets[3]]
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Wrap an IP packet in an Ethernet frame.
|
||
|
|
fn wrap_in_ethernet(ip_packet: &[u8], src_mac: [u8; 6], dst_mac: [u8; 6]) -> Vec<u8> {
|
||
|
|
let mut frame = Vec::with_capacity(ETH_HEADER_LEN + ip_packet.len());
|
||
|
|
frame.extend_from_slice(&dst_mac);
|
||
|
|
frame.extend_from_slice(&src_mac);
|
||
|
|
frame.extend_from_slice(Ð_TYPE_IPV4);
|
||
|
|
frame.extend_from_slice(ip_packet);
|
||
|
|
frame
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Extract the EtherType and payload from an Ethernet frame.
|
||
|
|
fn unwrap_ethernet(frame: &[u8]) -> Option<([u8; 2], &[u8])> {
|
||
|
|
if frame.len() < ETH_HEADER_LEN {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
let ether_type = [frame[12], frame[13]];
|
||
|
|
Some((ether_type, &frame[ETH_HEADER_LEN..]))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Extract destination IPv4 from a raw IP packet header.
|
||
|
|
fn dst_ip_from_packet(packet: &[u8]) -> Option<Ipv4Addr> {
|
||
|
|
if packet.len() < 20 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
// Version must be 4
|
||
|
|
if (packet[0] >> 4) != 4 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
Some(Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Extract source IPv4 from a raw IP packet header.
|
||
|
|
fn src_ip_from_packet(packet: &[u8]) -> Option<Ipv4Addr> {
|
||
|
|
if packet.len() < 20 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
if (packet[0] >> 4) != 4 {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
Some(Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Build a gratuitous ARP announcement frame.
|
||
|
|
fn build_garp(ip: Ipv4Addr, mac: [u8; 6]) -> Vec<u8> {
|
||
|
|
let ip_bytes = ip.octets();
|
||
|
|
let mut frame = Vec::with_capacity(42); // 14 eth + 28 ARP
|
||
|
|
// Ethernet header
|
||
|
|
frame.extend_from_slice(&BROADCAST_MAC); // dst: broadcast
|
||
|
|
frame.extend_from_slice(&mac); // src: our MAC
|
||
|
|
frame.extend_from_slice(Ð_TYPE_ARP); // EtherType: ARP
|
||
|
|
// ARP payload
|
||
|
|
frame.extend_from_slice(&[0x00, 0x01]); // Hardware type: Ethernet
|
||
|
|
frame.extend_from_slice(&[0x08, 0x00]); // Protocol type: IPv4
|
||
|
|
frame.push(6); // Hardware addr len
|
||
|
|
frame.push(4); // Protocol addr len
|
||
|
|
frame.extend_from_slice(&[0x00, 0x01]); // Operation: ARP Request (GARP uses request)
|
||
|
|
frame.extend_from_slice(&mac); // Sender hardware addr
|
||
|
|
frame.extend_from_slice(&ip_bytes); // Sender protocol addr
|
||
|
|
frame.extend_from_slice(&[0x00; 6]); // Target hardware addr (ignored in GARP)
|
||
|
|
frame.extend_from_slice(&ip_bytes); // Target protocol addr (same as sender for GARP)
|
||
|
|
frame
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Linux bridge management (ip commands)
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
async fn run_ip_cmd(args: &[&str]) -> Result<String> {
|
||
|
|
let output = tokio::process::Command::new("ip")
|
||
|
|
.args(args)
|
||
|
|
.output()
|
||
|
|
.await?;
|
||
|
|
if !output.status.success() {
|
||
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||
|
|
anyhow::bail!("ip {} failed: {}", args.join(" "), stderr.trim());
|
||
|
|
}
|
||
|
|
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a Linux bridge interface.
|
||
|
|
pub async fn create_bridge(name: &str) -> Result<()> {
|
||
|
|
run_ip_cmd(&["link", "add", name, "type", "bridge"]).await?;
|
||
|
|
info!("Created bridge {}", name);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Add an interface to a bridge.
|
||
|
|
pub async fn bridge_add_interface(bridge: &str, iface: &str) -> Result<()> {
|
||
|
|
run_ip_cmd(&["link", "set", iface, "master", bridge]).await?;
|
||
|
|
info!("Added {} to bridge {}", iface, bridge);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Bring an interface up.
|
||
|
|
pub async fn set_interface_up(iface: &str) -> Result<()> {
|
||
|
|
run_ip_cmd(&["link", "set", iface, "up"]).await?;
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Remove a bridge interface.
|
||
|
|
pub async fn remove_bridge(name: &str) -> Result<()> {
|
||
|
|
// First bring it down, ignore errors
|
||
|
|
let _ = run_ip_cmd(&["link", "set", name, "down"]).await;
|
||
|
|
run_ip_cmd(&["link", "del", name]).await?;
|
||
|
|
info!("Removed bridge {}", name);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Detect the default network interface from the routing table.
|
||
|
|
pub async fn detect_default_interface() -> Result<String> {
|
||
|
|
let output = run_ip_cmd(&["route", "show", "default"]).await?;
|
||
|
|
// Format: "default via X.X.X.X dev IFACE ..."
|
||
|
|
let parts: Vec<&str> = output.split_whitespace().collect();
|
||
|
|
if let Some(idx) = parts.iter().position(|&s| s == "dev") {
|
||
|
|
if let Some(iface) = parts.get(idx + 1) {
|
||
|
|
return Ok(iface.to_string());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
anyhow::bail!("Could not detect default network interface from route table");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Get the IP address and prefix length of a network interface.
|
||
|
|
pub async fn get_interface_ip(iface: &str) -> Result<(Ipv4Addr, u8)> {
|
||
|
|
let output = run_ip_cmd(&["-4", "addr", "show", "dev", iface]).await?;
|
||
|
|
// Parse "inet X.X.X.X/NN" from output
|
||
|
|
for line in output.lines() {
|
||
|
|
let trimmed = line.trim();
|
||
|
|
if let Some(rest) = trimmed.strip_prefix("inet ") {
|
||
|
|
let addr_cidr = rest.split_whitespace().next().unwrap_or("");
|
||
|
|
let parts: Vec<&str> = addr_cidr.split('/').collect();
|
||
|
|
if parts.len() == 2 {
|
||
|
|
let ip: Ipv4Addr = parts[0].parse()?;
|
||
|
|
let prefix: u8 = parts[1].parse()?;
|
||
|
|
return Ok((ip, prefix));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
anyhow::bail!("Could not find IPv4 address on interface {}", iface);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Migrate the host's IP from a physical interface to a bridge.
|
||
|
|
/// This is the most delicate operation — briefly interrupts connectivity.
|
||
|
|
pub async fn migrate_host_ip_to_bridge(
|
||
|
|
physical_iface: &str,
|
||
|
|
bridge: &str,
|
||
|
|
ip: Ipv4Addr,
|
||
|
|
prefix: u8,
|
||
|
|
) -> Result<()> {
|
||
|
|
let cidr = format!("{}/{}", ip, prefix);
|
||
|
|
// Remove IP from physical interface
|
||
|
|
let _ = run_ip_cmd(&["addr", "del", &cidr, "dev", physical_iface]).await;
|
||
|
|
// Add IP to bridge
|
||
|
|
run_ip_cmd(&["addr", "add", &cidr, "dev", bridge]).await?;
|
||
|
|
info!("Migrated IP {} from {} to {}", cidr, physical_iface, bridge);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Restore the host's IP from bridge back to the physical interface.
|
||
|
|
pub async fn restore_host_ip(
|
||
|
|
physical_iface: &str,
|
||
|
|
bridge: &str,
|
||
|
|
ip: Ipv4Addr,
|
||
|
|
prefix: u8,
|
||
|
|
) -> Result<()> {
|
||
|
|
let cidr = format!("{}/{}", ip, prefix);
|
||
|
|
let _ = run_ip_cmd(&["addr", "del", &cidr, "dev", bridge]).await;
|
||
|
|
run_ip_cmd(&["addr", "add", &cidr, "dev", physical_iface]).await?;
|
||
|
|
info!("Restored IP {} to {}", cidr, physical_iface);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Enable proxy ARP on an interface via sysctl.
|
||
|
|
pub async fn enable_proxy_arp(iface: &str) -> Result<()> {
|
||
|
|
let path = format!("/proc/sys/net/ipv4/conf/{}/proxy_arp", iface);
|
||
|
|
tokio::fs::write(&path, "1").await?;
|
||
|
|
info!("Enabled proxy_arp on {}", iface);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a TAP device (L2) using the tun crate.
|
||
|
|
pub fn create_tap(name: &str, mtu: u16) -> Result<tun::AsyncDevice> {
|
||
|
|
let mut config = tun::Configuration::default();
|
||
|
|
config
|
||
|
|
.tun_name(name)
|
||
|
|
.layer(tun::Layer::L2)
|
||
|
|
.mtu(mtu)
|
||
|
|
.up();
|
||
|
|
|
||
|
|
#[cfg(target_os = "linux")]
|
||
|
|
config.platform_config(|p| {
|
||
|
|
p.ensure_root_privileges(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
let device = tun::create_as_async(&config)?;
|
||
|
|
info!("TAP device {} created (L2, mtu={})", name, mtu);
|
||
|
|
Ok(device)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// BridgeEngine — main event loop
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
/// The BridgeEngine wraps/unwraps Ethernet frames and bridges VPN traffic
|
||
|
|
/// to the host's physical LAN via a Linux bridge + TAP device.
|
||
|
|
pub struct BridgeEngine {
|
||
|
|
state: Arc<ServerState>,
|
||
|
|
/// Learned MAC addresses for LAN peers (dst IP → MAC).
|
||
|
|
/// Populated from ARP replies and Ethernet frame src MACs.
|
||
|
|
arp_cache: HashMap<Ipv4Addr, [u8; 6]>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl BridgeEngine {
|
||
|
|
pub fn new(state: Arc<ServerState>) -> Self {
|
||
|
|
Self {
|
||
|
|
state,
|
||
|
|
arp_cache: HashMap::new(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Run the bridge engine event loop.
|
||
|
|
/// Receives L3 IP packets from VPN clients, wraps in Ethernet, writes to TAP.
|
||
|
|
/// Reads Ethernet frames from TAP, strips header, routes back to VPN clients.
|
||
|
|
pub async fn run(
|
||
|
|
mut self,
|
||
|
|
mut tap_device: tun::AsyncDevice,
|
||
|
|
mut packet_rx: mpsc::Receiver<Vec<u8>>,
|
||
|
|
mut shutdown_rx: mpsc::Receiver<()>,
|
||
|
|
) -> Result<()> {
|
||
|
|
let mut buf = vec![0u8; 2048];
|
||
|
|
|
||
|
|
info!("BridgeEngine started");
|
||
|
|
|
||
|
|
loop {
|
||
|
|
tokio::select! {
|
||
|
|
// Packet from VPN client → wrap in Ethernet → write to TAP
|
||
|
|
Some(ip_packet) = packet_rx.recv() => {
|
||
|
|
if let Some(dst_ip) = dst_ip_from_packet(&ip_packet) {
|
||
|
|
let src_ip = src_ip_from_packet(&ip_packet).unwrap_or(Ipv4Addr::UNSPECIFIED);
|
||
|
|
let src_mac = mac_from_ip(src_ip);
|
||
|
|
let dst_mac = self.arp_cache.get(&dst_ip)
|
||
|
|
.copied()
|
||
|
|
.unwrap_or(BROADCAST_MAC);
|
||
|
|
let frame = wrap_in_ethernet(&ip_packet, src_mac, dst_mac);
|
||
|
|
if let Err(e) = tap_device.write_all(&frame).await {
|
||
|
|
warn!("TAP write error: {}", e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Frame from TAP (LAN) → strip Ethernet → route to VPN client
|
||
|
|
result = tap_device.read(&mut buf) => {
|
||
|
|
match result {
|
||
|
|
Ok(len) if len >= ETH_HEADER_LEN => {
|
||
|
|
let frame = &buf[..len];
|
||
|
|
|
||
|
|
// Learn src MAC from incoming frames
|
||
|
|
if let Some((ether_type, payload)) = unwrap_ethernet(frame) {
|
||
|
|
// Learn ARP cache from src MAC + src IP
|
||
|
|
let src_mac: [u8; 6] = frame[6..12].try_into().unwrap_or([0; 6]);
|
||
|
|
if ether_type == ETH_TYPE_IPV4 {
|
||
|
|
if let Some(src_ip) = src_ip_from_packet(payload) {
|
||
|
|
self.arp_cache.insert(src_ip, src_mac);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Only forward IPv4 packets to VPN clients
|
||
|
|
if ether_type == ETH_TYPE_IPV4 {
|
||
|
|
if let Some(dst_ip) = dst_ip_from_packet(payload) {
|
||
|
|
// Look up VPN client by dst IP in tun_routes
|
||
|
|
let routes = self.state.tun_routes.read().await;
|
||
|
|
if let Some(sender) = routes.get(&dst_ip) {
|
||
|
|
let _ = sender.try_send(payload.to_vec());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Ok(_) => {} // Frame too short, ignore
|
||
|
|
Err(e) => {
|
||
|
|
warn!("TAP read error: {}", e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_ = shutdown_rx.recv() => {
|
||
|
|
info!("BridgeEngine shutting down");
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Send a gratuitous ARP for a VPN client IP.
|
||
|
|
pub async fn announce_client(tap: &mut tun::AsyncDevice, ip: Ipv4Addr) -> Result<()> {
|
||
|
|
let mac = mac_from_ip(ip);
|
||
|
|
let garp = build_garp(ip, mac);
|
||
|
|
tap.write_all(&garp).await?;
|
||
|
|
debug!("Sent GARP for {} (MAC {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x})",
|
||
|
|
ip, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|