feat(nat): add destination routing policy support for socket-mode VPN traffic
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user