feat(nat): add destination routing policy support for socket-mode VPN traffic

This commit is contained in:
2026-03-30 12:52:17 +00:00
parent c3afb83470
commit e06667b298
6 changed files with 126 additions and 19 deletions

View File

@@ -13,7 +13,8 @@ use tokio::net::{TcpStream, UdpSocket};
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
use crate::server::ServerState;
use crate::acl;
use crate::server::{DestinationPolicyConfig, ServerState};
use crate::tunnel;
// ============================================================================
@@ -194,10 +195,22 @@ pub struct NatEngine {
/// When true, outbound TCP connections prepend PROXY protocol v2 headers
/// with the VPN client's tunnel IP as source address.
proxy_protocol: bool,
/// Destination routing policy: forceTarget, block, or allow.
destination_policy: Option<DestinationPolicyConfig>,
}
/// Result of destination policy evaluation.
enum DestinationAction {
/// Connect to the original destination.
PassThrough(SocketAddr),
/// Redirect to a target IP, preserving original port.
ForceTarget(SocketAddr),
/// Drop the packet silently.
Drop,
}
impl NatEngine {
pub fn new(gateway_ip: Ipv4Addr, mtu: usize, state: Arc<ServerState>, proxy_protocol: bool) -> Self {
pub fn new(gateway_ip: Ipv4Addr, mtu: usize, state: Arc<ServerState>, proxy_protocol: bool, destination_policy: Option<DestinationPolicyConfig>) -> Self {
let mut device = VirtualIpDevice::new(mtu);
let config = Config::new(HardwareAddress::Ip);
let now = smoltcp::time::Instant::from_millis(0);
@@ -230,6 +243,7 @@ impl NatEngine {
bridge_tx,
start_time: std::time::Instant::now(),
proxy_protocol,
destination_policy,
}
}
@@ -237,6 +251,40 @@ impl NatEngine {
smoltcp::time::Instant::from_millis(self.start_time.elapsed().as_millis() as i64)
}
/// Evaluate destination policy for a packet's destination IP.
fn evaluate_destination(&self, dst_ip: Ipv4Addr, dst_port: u16) -> DestinationAction {
let policy = match &self.destination_policy {
Some(p) => p,
None => return DestinationAction::PassThrough(SocketAddr::new(dst_ip.into(), dst_port)),
};
// 1. Block list wins (deny overrides allow)
if let Some(ref block_list) = policy.block_list {
if !block_list.is_empty() && acl::ip_matches_any(dst_ip, block_list) {
return DestinationAction::Drop;
}
}
// 2. Allow list — pass through directly
if let Some(ref allow_list) = policy.allow_list {
if !allow_list.is_empty() && acl::ip_matches_any(dst_ip, allow_list) {
return DestinationAction::PassThrough(SocketAddr::new(dst_ip.into(), dst_port));
}
}
// 3. Default action
match policy.default.as_str() {
"forceTarget" => {
let target_ip = policy.target.as_deref()
.and_then(|t| t.parse::<Ipv4Addr>().ok())
.unwrap_or(Ipv4Addr::LOCALHOST);
DestinationAction::ForceTarget(SocketAddr::new(target_ip.into(), dst_port))
}
"block" => DestinationAction::Drop,
_ => DestinationAction::PassThrough(SocketAddr::new(dst_ip.into(), dst_port)),
}
}
/// Inject a raw IP packet from a VPN client and handle new session creation.
fn inject_packet(&mut self, packet: Vec<u8>) {
let Some((ihl, src_ip, dst_ip, protocol)) = parse_ipv4_header(&packet) else {
@@ -261,7 +309,14 @@ impl NatEngine {
// SYN without ACK = new connection
let is_syn = (flags & 0x02) != 0 && (flags & 0x10) == 0;
if is_syn && !self.tcp_sessions.contains_key(&key) {
self.create_tcp_session(&key);
match self.evaluate_destination(dst_ip, dst_port) {
DestinationAction::Drop => {
debug!("NAT: destination policy blocked TCP {}:{} -> {}:{}", src_ip, src_port, dst_ip, dst_port);
return;
}
DestinationAction::PassThrough(addr) => self.create_tcp_session(&key, addr),
DestinationAction::ForceTarget(addr) => self.create_tcp_session(&key, addr),
}
}
}
17 => {
@@ -278,7 +333,14 @@ impl NatEngine {
};
if !self.udp_sessions.contains_key(&key) {
self.create_udp_session(&key);
match self.evaluate_destination(dst_ip, dst_port) {
DestinationAction::Drop => {
debug!("NAT: destination policy blocked UDP {}:{} -> {}:{}", src_ip, src_port, dst_ip, dst_port);
return;
}
DestinationAction::PassThrough(addr) => self.create_udp_session(&key, addr),
DestinationAction::ForceTarget(addr) => self.create_udp_session(&key, addr),
}
}
// Update last_activity for existing sessions
@@ -295,7 +357,7 @@ impl NatEngine {
self.device.inject_packet(packet);
}
fn create_tcp_session(&mut self, key: &SessionKey) {
fn create_tcp_session(&mut self, key: &SessionKey, connect_addr: SocketAddr) {
// Create smoltcp TCP socket
let tcp_rx_buf = tcp::SocketBuffer::new(vec![0u8; 65535]);
let tcp_tx_buf = tcp::SocketBuffer::new(vec![0u8; 65535]);
@@ -323,12 +385,12 @@ impl NatEngine {
};
self.tcp_sessions.insert(key.clone(), session);
// Spawn bridge task that connects to the real destination
// Spawn bridge task that connects to the resolved destination
let bridge_tx = self.bridge_tx.clone();
let key_clone = key.clone();
let proxy_protocol = self.proxy_protocol;
tokio::spawn(async move {
tcp_bridge_task(key_clone, data_rx, bridge_tx, proxy_protocol).await;
tcp_bridge_task(key_clone, data_rx, bridge_tx, proxy_protocol, connect_addr).await;
});
debug!(
@@ -337,7 +399,7 @@ impl NatEngine {
);
}
fn create_udp_session(&mut self, key: &SessionKey) {
fn create_udp_session(&mut self, key: &SessionKey, connect_addr: SocketAddr) {
// Create smoltcp UDP socket
let udp_rx_buf = udp::PacketBuffer::new(
vec![udp::PacketMetadata::EMPTY; 32],
@@ -373,7 +435,7 @@ impl NatEngine {
let bridge_tx = self.bridge_tx.clone();
let key_clone = key.clone();
tokio::spawn(async move {
udp_bridge_task(key_clone, data_rx, bridge_tx).await;
udp_bridge_task(key_clone, data_rx, bridge_tx, connect_addr).await;
});
debug!(
@@ -537,20 +599,19 @@ async fn tcp_bridge_task(
mut data_rx: mpsc::Receiver<Vec<u8>>,
bridge_tx: mpsc::Sender<BridgeMessage>,
proxy_protocol: bool,
connect_addr: SocketAddr,
) {
let addr = SocketAddr::new(key.dst_ip.into(), key.dst_port);
// Connect to real destination with timeout
let stream = match tokio::time::timeout(Duration::from_secs(30), TcpStream::connect(addr)).await
// Connect to resolved destination (may differ from key.dst_ip if policy rewrote it)
let stream = match tokio::time::timeout(Duration::from_secs(30), TcpStream::connect(connect_addr)).await
{
Ok(Ok(s)) => s,
Ok(Err(e)) => {
debug!("NAT TCP connect to {} failed: {}", addr, e);
debug!("NAT TCP connect to {} failed: {}", connect_addr, e);
let _ = bridge_tx.send(BridgeMessage::TcpClosed { key }).await;
return;
}
Err(_) => {
debug!("NAT TCP connect to {} timed out", addr);
debug!("NAT TCP connect to {} timed out", connect_addr);
let _ = bridge_tx.send(BridgeMessage::TcpClosed { key }).await;
return;
}
@@ -564,7 +625,7 @@ async fn tcp_bridge_task(
let dst = SocketAddr::new(key.dst_ip.into(), key.dst_port);
let pp_header = crate::proxy_protocol::build_pp_v2_header(src, dst);
if let Err(e) = writer.write_all(&pp_header).await {
debug!("NAT: failed to send PP v2 header to {}: {}", addr, e);
debug!("NAT: failed to send PP v2 header to {}: {}", connect_addr, e);
let _ = bridge_tx.send(BridgeMessage::TcpClosed { key }).await;
return;
}
@@ -612,6 +673,7 @@ async fn udp_bridge_task(
key: SessionKey,
mut data_rx: mpsc::Receiver<Vec<u8>>,
bridge_tx: mpsc::Sender<BridgeMessage>,
connect_addr: SocketAddr,
) {
let socket = match UdpSocket::bind("0.0.0.0:0").await {
Ok(s) => s,
@@ -620,7 +682,7 @@ async fn udp_bridge_task(
return;
}
};
let dest = SocketAddr::new(key.dst_ip.into(), key.dst_port);
let dest = connect_addr;
let socket = Arc::new(socket);
let socket2 = socket.clone();