fix: include selected networks in dataplane NAT

This commit is contained in:
thakares committed 2026-09-01 18:21:58 +05:30
1 parent 5599e1b5c8
commit 34227efd2b
10 files changed
+545 -54

No files matched your search

+32 -1
View File
@@ -2,9 +2,10 @@
use crate::error::{Result, WireGuardError};
use chrono::{NaiveDateTime, Utc};
use ipnet::IpNet;
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerState};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{BTreeSet, HashMap};
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -36,6 +37,36 @@ pub struct LiveInterfaceStats {
pub is_up: bool,
}
/// Peer tunnel addresses that are not on the Interface connected prefix.
///
/// Interface-CIDR peers (e.g. 10.100.0.x with wg0 10.100.0.1/24) are already
/// reachable via the kernel connected route created by the interface address.
/// Selected-Network peers (e.g. 10.100.2.1/32) are not. Those prefixes must be
/// installed as on-link device routes on the WireGuard interface so the FIB
/// delivers packets into wg0, where cryptokey routing (AllowedIPs) applies.
///
/// This is not a Routes-table LAN-behind-peer destination and has no gateway.
pub fn onlink_peer_address_prefixes(interface: &Interface, peers: &[Peer]) -> Vec<IpNet> {
let mut prefixes = BTreeSet::new();
for peer in peers.iter().filter(|p| p.state == PeerState::Active) {
if let Some(v4) = peer.address_v4
&& v4.prefix_len() > 0
&& !interface.address_v4.contains(&v4.addr())
{
prefixes.insert(v4);
}
if let Some(v6) = peer.address_v6
&& v6.prefix_len() > 0
&& !interface
.address_v6
.is_some_and(|iface_v6| iface_v6.contains(&v6.addr()))
{
prefixes.insert(v6);
}
}
prefixes.into_iter().collect()
}
/// Abstract WireGuard Engine interface for kernel netlink and simulated environments.
#[async_trait::async_trait]
pub trait WireGuardEngine: Send + Sync {
+1 -1
View File
@@ -10,7 +10,7 @@ pub mod qr;
pub use config_builder::ClientConfigBuilder;
pub use engine::{
LiveInterfaceStats, LivePeerStats, NativeLinuxWireGuardEngine, SimulatedWireGuardEngine,
WireGuardEngine,
WireGuardEngine, onlink_peer_address_prefixes,
};
pub use error::{Result, WireGuardError};
pub use qr::{
+190 -2
View File
@@ -8,7 +8,9 @@
//!
//! No external commands (wg, ip, wg-quick, nft, sysctl) are ever executed.
use crate::engine::{LiveInterfaceStats, LivePeerStats, WireGuardEngine};
use crate::engine::{
LiveInterfaceStats, LivePeerStats, WireGuardEngine, onlink_peer_address_prefixes,
};
use crate::error::{Result, WireGuardError};
use base64::Engine as _;
use chrono::NaiveDateTime;
@@ -24,9 +26,13 @@ use netlink_packet_wireguard::{
};
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerState};
use rtnetlink::LinkWireguard;
use rtnetlink::RouteMessageBuilder;
use rtnetlink::packet_route::AddressFamily;
use rtnetlink::packet_route::address::{AddressAttribute, AddressMessage};
use rtnetlink::packet_route::link::{InfoKind, LinkAttribute, LinkFlags, LinkInfo};
use std::net::{IpAddr, SocketAddr};
use rtnetlink::packet_route::route::{RouteAddress, RouteAttribute, RouteMessage};
use std::collections::HashSet;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
/// Linux Native WireGuard Engine using kernel RTNETLINK and Generic Netlink.
#[derive(Debug, Clone, Default)]
@@ -852,6 +858,178 @@ fn parse_endpoint(s: &str) -> Result<SocketAddr> {
)))
}
/// Install on-link device routes for peer tunnel addresses outside the Interface prefix.
///
/// Interface-CIDR peers are already covered by the connected route from the
/// interface address. Selected-Network peer addresses are not; without a FIB
/// path into wg0, cryptokey routing never sees the packet. Routes have no
/// gateway and are not Routes-table LAN destinations.
async fn ensure_onlink_peer_routes(interface: &Interface, peers: &[Peer]) -> Result<()> {
let (handle, _join) = rtnetlink_handle().await?;
let mut links = handle
.link()
.get()
.match_name(interface.name.to_string())
.execute();
let Some(link) = links.try_next().await.map_err(|e| {
WireGuardError::Netlink(format!(
"failed to resolve interface '{}' for on-link routes: {e}",
interface.name
))
})?
else {
return Ok(());
};
let link_index = link.header.index;
let desired: HashSet<IpNet> = onlink_peer_address_prefixes(interface, peers)
.into_iter()
.collect();
let live = list_onlink_routes_for_index(&handle, link_index).await?;
for prefix in &desired {
if live.contains(prefix) {
continue;
}
add_onlink_device_route(&handle, link_index, *prefix).await?;
}
let iface_v4 = interface.address_v4.trunc();
let iface_v6 = interface.address_v6.map(|n| n.trunc());
for prefix in live {
let is_host = matches!(prefix, IpNet::V4(n) if n.prefix_len() == 32)
|| matches!(prefix, IpNet::V6(n) if n.prefix_len() == 128);
if !is_host {
continue;
}
if prefix.trunc() == iface_v4 || iface_v6 == Some(prefix.trunc()) {
continue;
}
if desired.contains(&prefix) {
continue;
}
let _ = delete_onlink_device_route(&handle, link_index, prefix).await;
}
Ok(())
}
async fn list_onlink_routes_for_index(
handle: &rtnetlink::Handle,
link_index: u32,
) -> Result<HashSet<IpNet>> {
let mut results = HashSet::new();
for family in [AddressFamily::Inet, AddressFamily::Inet6] {
let mut req = RouteMessage::default();
req.header.address_family = family;
let mut stream = handle.route().get(req).execute();
while let Some(msg) = stream
.try_next()
.await
.map_err(|e| WireGuardError::Netlink(format!("RTNETLINK route dump failed: {e}")))?
{
let mut dest_ip = match family {
AddressFamily::Inet => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
AddressFamily::Inet6 => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
_ => continue,
};
let mut oif = None;
let mut has_gateway = false;
for attr in &msg.attributes {
match attr {
RouteAttribute::Destination(RouteAddress::Inet(v4)) => {
dest_ip = IpAddr::V4(*v4);
}
RouteAttribute::Destination(RouteAddress::Inet6(v6)) => {
dest_ip = IpAddr::V6(*v6);
}
RouteAttribute::Gateway(_) => has_gateway = true,
RouteAttribute::Oif(idx) => oif = Some(*idx),
_ => {}
}
}
if has_gateway || oif != Some(link_index) {
continue;
}
if let Ok(net) = IpNet::new(dest_ip, msg.header.destination_prefix_length) {
results.insert(net);
}
}
}
Ok(results)
}
async fn add_onlink_device_route(
handle: &rtnetlink::Handle,
link_index: u32,
prefix: IpNet,
) -> Result<()> {
let exec_result = match prefix {
IpNet::V4(v4) => {
let msg = RouteMessageBuilder::<Ipv4Addr>::new()
.destination_prefix(v4.addr(), v4.prefix_len())
.output_interface(link_index)
.build();
handle.route().add(msg).execute().await
}
IpNet::V6(v6) => {
let msg = RouteMessageBuilder::<Ipv6Addr>::new()
.destination_prefix(v6.addr(), v6.prefix_len())
.output_interface(link_index)
.build();
handle.route().add(msg).execute().await
}
};
if let Err(e) = exec_result {
let err_str = e.to_string();
if err_str.contains("File exists") || err_str.contains("17") {
return Ok(());
}
if err_str.contains("permission")
|| err_str.contains("EPERM")
|| err_str.contains("Operation not permitted")
{
return Err(WireGuardError::PermissionDenied(format!(
"insufficient privileges to add on-link route '{prefix}': {e}"
)));
}
return Err(WireGuardError::Netlink(format!(
"failed to add on-link route '{prefix}': {e}"
)));
}
tracing::info!(prefix = %prefix, "On-link peer address route added via RTNETLINK");
Ok(())
}
async fn delete_onlink_device_route(
handle: &rtnetlink::Handle,
link_index: u32,
prefix: IpNet,
) -> Result<()> {
let exec_result = match prefix {
IpNet::V4(v4) => {
let msg = RouteMessageBuilder::<Ipv4Addr>::new()
.destination_prefix(v4.addr(), v4.prefix_len())
.output_interface(link_index)
.build();
handle.route().del(msg).execute().await
}
IpNet::V6(v6) => {
let msg = RouteMessageBuilder::<Ipv6Addr>::new()
.destination_prefix(v6.addr(), v6.prefix_len())
.output_interface(link_index)
.build();
handle.route().del(msg).execute().await
}
};
if let Err(e) = exec_result {
tracing::debug!(prefix = %prefix, error = %e, "On-link peer address route delete skipped");
}
Ok(())
}
// ── WireGuardEngine Trait Implementation ─────────────────────────────────────
#[async_trait::async_trait]
@@ -863,6 +1041,16 @@ impl WireGuardEngine for NativeLinuxWireGuardEngine {
// 2. Configure the WireGuard device (private key, listen port, peers)
configure_device(interface, peers).await?;
// 3. On-link device routes for peer tunnel addresses outside the
// Interface connected prefix (cryptokey routing still uses AllowedIPs).
if let Err(e) = ensure_onlink_peer_routes(interface, peers).await {
tracing::warn!(
interface = %interface.name,
error = %e,
"On-link peer address routes skipped"
);
}
tracing::info!(
interface = %interface.name,
active_peers = peers.iter().filter(|p| p.state == PeerState::Active).count(),