Skip to content

Commit 6b5bb64

Browse files
committed
feat(vpn): add cidr mapping to fix ip conflict
1 parent 727efc0 commit 6b5bb64

File tree

8 files changed

+170
-5
lines changed

8 files changed

+170
-5
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ An AI-driven intelligent VPN tunnel built with Rust, featuring automatic path se
3434
- 🔐 **Secure Encryption** - ChaCha20-Poly1305 (default), AES-256-GCM, XOR/Plain options
3535
- 🚀 **Dual-Path P2P** - IPv6 direct connection + STUN hole punching with auto-fallback to relay
3636
- 🌐 **Smart Routing** - Automatic path selection: IPv6 (lowest latency) → STUN (NAT traversal) → Relay
37+
- 🔄 **CIDR Mapping** - Resolve network conflicts by mapping overlapping CIDR ranges (Linux only)
3738
- 🌍 **Cross-Platform** - Linux, macOS, Windows with pre-built binaries
3839

3940
## 📋 Table of Contents
@@ -273,7 +274,8 @@ Create or edit `/etc/rustun/routes.json`:
273274
| `private_ip` | Virtual IP assigned to the client | `"10.0.1.1"` |
274275
| `mask` | Subnet mask for the VPN network | `"255.255.255.0"` |
275276
| `gateway` | Gateway IP for routing | `"10.0.1.254"` |
276-
| `ciders` | CIDR ranges routable through this client | `["192.168.1.0/24"]` |
277+
| `ciders` | CIDR ranges routable through this client (mapped CIDRs that other clients see) | `["192.168.1.0/24"]` |
278+
| `cider_mapping` | CIDR mapping to resolve network conflicts (Linux only). Maps `ciders` to real network CIDRs | `{"192.168.11.0/24": "192.168.10.0/24"}` |
277279

278280
## 📖 Usage
279281

src/client/main.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ async fn init_device(device_config: &HandshakeReplyFrame, enable_masq: bool) ->
124124

125125
dev.reload_route(device_config.peer_details.clone()).await;
126126

127+
// Setup CIDR mapping DNAT rules
128+
if !device_config.cider_mapping.is_empty() {
129+
if let Err(e) = dev.setup_cidr_mapping(&device_config.cider_mapping) {
130+
tracing::error!("Failed to setup CIDR mapping DNAT rules: {}", e);
131+
// Don't fail initialization, just log the error
132+
// This allows the client to continue even if DNAT setup fails
133+
}
134+
}
135+
127136
Ok(dev)
128137
}
129138

src/codec/frame.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
//! - Type: Frame type identifier (1 byte)
1616
//! - Payload Length: Length of the payload in bytes (2 bytes, big-endian)
1717
18+
use std::collections::HashMap;
1819
pub(crate) use crate::codec::errors::FrameError;
1920
use serde::{Deserialize, Serialize};
2021
use std::fmt::Display;
@@ -157,6 +158,9 @@ pub struct HandshakeReplyFrame {
157158
/// ciders to me
158159
pub(crate) ciders: Vec<String>,
159160

161+
/// ciders mapping
162+
pub cider_mapping: HashMap<String, String>,
163+
160164
/// List of other peers in the same cluster
161165
///
162166
/// Each PeerDetail contains routing information for a peer node,
@@ -184,7 +188,7 @@ pub struct PeerDetail {
184188
/// Traffic destined for these ranges will be routed through this peer
185189
pub ciders: Vec<String>,
186190

187-
/// IPv6 is public ip address of ther peer
191+
/// IPv6 is public ip address of their peer
188192
pub ipv6: String,
189193

190194
pub port: u16,
@@ -213,10 +217,10 @@ pub struct KeepAliveFrame {
213217
pub name: String,
214218
/// Peer identity (unique identifier)
215219
pub identity: String,
216-
220+
217221
/// Public IPv6 address
218222
pub ipv6: String,
219-
223+
220224
/// UDP port for P2P connections
221225
pub port: u16,
222226

src/server/client_manager.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ pub struct ClientConfig {
1212
pub mask: String,
1313
pub gateway: String,
1414
pub ciders: Vec<String>,
15+
// virtual cidr to actual cidr
16+
#[serde(default)]
17+
pub cider_mapping: HashMap<String, String>,
1518
}
1619

1720
pub struct ClientManager {

src/server/conf_agent.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashMap;
12
use crate::server::client_manager::{ClientConfig, ClientManager};
23
use crate::server::config::ConfAgentConfig;
34
use crate::network::connection_manager::ConnectionManager;
@@ -24,6 +25,8 @@ struct ClientConfigResponse {
2425
mask: String,
2526
gateway: String,
2627
ciders: Vec<String>,
28+
#[serde(default)]
29+
cider_mapping: HashMap<String, String>,
2730
}
2831

2932
pub struct ConfAgent {
@@ -181,6 +184,7 @@ impl ConfAgent {
181184
mask: r.mask,
182185
gateway: r.gateway,
183186
ciders: r.ciders,
187+
cider_mapping: r.cider_mapping,
184188
})
185189
.collect();
186190

src/server/server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ impl Handler {
147147
mask: client_config.mask.clone(),
148148
gateway: client_config.gateway.clone(),
149149
ciders: client_config.ciders.clone(),
150+
cider_mapping: client_config.cider_mapping.clone(),
150151
peer_details: route_items,
151152
}))
152153
.await?;

src/utils/device.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use tokio::sync::{mpsc, oneshot};
44
use tun::AbstractDevice;
55
use crate::codec::frame::{HandshakeReplyFrame, PeerDetail};
66
use crate::utils::sys_route::SysRoute;
7-
use std::collections::HashSet;
7+
use std::collections::{HashSet, HashMap};
88
#[allow(unused_imports)]
99
use crate::utils::sys_route::{ip_to_network, mask_to_prefix_length};
1010

@@ -315,6 +315,34 @@ impl DeviceHandler {
315315
}
316316
Ok(())
317317
}
318+
319+
/// Setup CIDR mapping DNAT rules based on HandshakeReplyFrame
320+
/// This should be called after receiving HandshakeReplyFrame
321+
/// Maps destination IPs from mapped CIDR to real CIDR using iptables NETMAP
322+
#[cfg(target_os = "linux")]
323+
pub fn setup_cidr_mapping(&mut self, cidr_mapping: &HashMap<String, String>) -> crate::Result<()> {
324+
let sys_route = SysRoute::new();
325+
326+
for (mapped_cidr, real_cidr) in cidr_mapping {
327+
// Add DNAT rule (iptables will check if it already exists)
328+
if let Err(e) = sys_route.enable_cidr_dnat(mapped_cidr, real_cidr) {
329+
tracing::error!(
330+
"Failed to add DNAT rule for {} -> {}: {}",
331+
mapped_cidr, real_cidr, e
332+
);
333+
return Err(e);
334+
}
335+
336+
tracing::info!("Added DNAT rule for CIDR mapping: {} -> {}", mapped_cidr, real_cidr);
337+
}
338+
339+
Ok(())
340+
}
341+
342+
#[cfg(not(target_os = "linux"))]
343+
pub fn setup_cidr_mapping(&mut self, _cidr_mapping: &HashMap<String, String>) -> crate::Result<()> {
344+
Ok(())
345+
}
318346
}
319347

320348
impl Default for DeviceHandler {

src/utils/sys_route.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,120 @@ impl SysRoute {
398398
pub fn disable_snat_for_local_network(&self, _local_cidr: &str, _tun_interface: &str, _virtual_ip: &str) -> crate::Result<()> {
399399
Ok(())
400400
}
401+
402+
/// Enable DNAT/NETMAP for CIDR mapping (Linux only)
403+
/// Maps destination IPs from mapped CIDR to real CIDR
404+
/// Uses NETMAP target: iptables -t nat -A PREROUTING -d <mapped_cidr> -j NETMAP --to <real_cidr>
405+
///
406+
/// # Arguments
407+
/// * `mapped_cidr` - The CIDR that other clients see (e.g., "192.168.11.0/24")
408+
/// * `real_cidr` - The real CIDR network (e.g., "192.168.10.0/24")
409+
///
410+
/// # Example
411+
/// When a packet arrives with destination IP in `mapped_cidr`, it will be translated
412+
/// to the corresponding IP in `real_cidr` before being forwarded to the local network.
413+
#[cfg(target_os = "linux")]
414+
pub fn enable_cidr_dnat(&self, mapped_cidr: &str, real_cidr: &str) -> crate::Result<()> {
415+
// Check if NETMAP rule already exists: iptables -t nat -C PREROUTING -d <mapped_cidr> -j NETMAP --to <real_cidr>
416+
let check_output = Command::new("iptables")
417+
.args([
418+
"-t", "nat",
419+
"-C", "PREROUTING",
420+
"-d", mapped_cidr,
421+
"-j", "NETMAP",
422+
"--to", real_cidr,
423+
])
424+
.output();
425+
426+
match check_output {
427+
Ok(output) if output.status.success() => {
428+
tracing::debug!("DNAT rule already exists: {} -> {}", mapped_cidr, real_cidr);
429+
return Ok(());
430+
}
431+
_ => {}
432+
}
433+
434+
// Add NETMAP rule: iptables -t nat -A PREROUTING -d <mapped_cidr> -j NETMAP --to <real_cidr>
435+
let output = Command::new("iptables")
436+
.args([
437+
"-t", "nat",
438+
"-A", "PREROUTING",
439+
"-d", mapped_cidr,
440+
"-j", "NETMAP",
441+
"--to", real_cidr,
442+
])
443+
.output()
444+
.map_err(|e| {
445+
if e.kind() == std::io::ErrorKind::NotFound {
446+
format!(
447+
"iptables command not found. CIDR mapping requires iptables with NETMAP support.\n\
448+
Please install iptables and ensure your kernel supports NETMAP target.\n\
449+
NETMAP requires Linux kernel 2.6.32+ with netfilter NETMAP module."
450+
)
451+
} else {
452+
format!("Failed to execute iptables command: {}", e)
453+
}
454+
})?;
455+
456+
if !output.status.success() {
457+
let stderr = String::from_utf8_lossy(&output.stderr);
458+
// Check if NETMAP is not supported
459+
if stderr.contains("No chain/target/match") || stderr.contains("NETMAP") {
460+
return Err(format!(
461+
"NETMAP target not supported. CIDR mapping requires kernel support for NETMAP.\n\
462+
Please ensure your kernel has NETMAP support (Linux 2.6.32+) or use a different approach.\n\
463+
Error: {}", stderr
464+
).into());
465+
}
466+
return Err(format!("Failed to add DNAT rule: {}", stderr).into());
467+
}
468+
469+
tracing::info!("Added DNAT rule: {} -> {}", mapped_cidr, real_cidr);
470+
Ok(())
471+
}
472+
473+
/// Disable DNAT/NETMAP for CIDR mapping (Linux only)
474+
/// Removes the NETMAP rule that was previously added
475+
///
476+
/// # Arguments
477+
/// * `mapped_cidr` - The mapped CIDR (e.g., "192.168.11.0/24")
478+
/// * `real_cidr` - The real CIDR (e.g., "192.168.10.0/24")
479+
#[cfg(target_os = "linux")]
480+
pub fn disable_cidr_dnat(&self, mapped_cidr: &str, real_cidr: &str) -> crate::Result<()> {
481+
let output = Command::new("iptables")
482+
.args([
483+
"-t", "nat",
484+
"-D", "PREROUTING",
485+
"-d", mapped_cidr,
486+
"-j", "NETMAP",
487+
"--to", real_cidr,
488+
])
489+
.output()
490+
.map_err(|e| format!("Failed to execute iptables command: {}", e))?;
491+
492+
if !output.status.success() {
493+
let stderr = String::from_utf8_lossy(&output.stderr);
494+
// Ignore "not found" error (rule already deleted)
495+
if stderr.contains("not found") || stderr.contains("找不到") || stderr.contains("No rule") {
496+
tracing::debug!("DNAT rule not found (already deleted): {} -> {}", mapped_cidr, real_cidr);
497+
return Ok(());
498+
}
499+
return Err(format!("Failed to delete DNAT rule: {}", stderr).into());
500+
}
501+
502+
tracing::info!("Deleted DNAT rule: {} -> {}", mapped_cidr, real_cidr);
503+
Ok(())
504+
}
505+
506+
#[cfg(not(target_os = "linux"))]
507+
pub fn enable_cidr_dnat(&self, _mapped_cidr: &str, _real_cidr: &str) -> crate::Result<()> {
508+
Err("CIDR mapping DNAT is only supported on Linux".into())
509+
}
510+
511+
#[cfg(not(target_os = "linux"))]
512+
pub fn disable_cidr_dnat(&self, _mapped_cidr: &str, _real_cidr: &str) -> crate::Result<()> {
513+
Err("CIDR mapping DNAT is only supported on Linux".into())
514+
}
401515
}
402516

403517
impl Default for SysRoute {

0 commit comments

Comments
 (0)