fix(nat): 修复 ICMP NAT 映射只增不减导致内存无界增长
每个 ping 包都会以 (对端地址, identifier, sequence) 为键新增一条 永久映射,运行时间越长内存占用越大。 修复: - 收到 EchoReply 命中映射即移除该条目(一次 echo 交换完成)。 - 映射值携带创建时间,select 循环中加入 60s 周期的 GC 分支, 清理超时(60s)未收到应答的残留条目。 测试:test_evict_expired 验证超时条目被清理、未超时条目保留。
This commit is contained in:
@@ -7,10 +7,18 @@ use pnet_packet::icmp::{IcmpPacket, IcmpTypes};
|
||||
use pnet_packet::ipv4::Ipv4Packet;
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::time::{Duration, Instant};
|
||||
use tcp_ip::IpStack;
|
||||
use tcp_ip::icmp::IcmpSocket;
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
/// ICMP echo 映射超时:正常 ping 应答在秒级返回,超时条目视为无应答残留
|
||||
const ICMP_NAT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const ICMP_NAT_GC_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
/// (对端地址, identifier, sequence) -> (内网客户端地址, 创建时间)
|
||||
type IcmpNatMap = HashMap<(Ipv4Addr, Identifier, SequenceNumber), (Ipv4Addr, Instant)>;
|
||||
|
||||
pub async fn start_icmp_nat(
|
||||
task_group: &TaskGroup,
|
||||
ip_stack: &IpStack,
|
||||
@@ -49,24 +57,38 @@ async fn task(
|
||||
) -> anyhow::Result<()> {
|
||||
let mut buf1 = vec![0u8; 65536];
|
||||
let mut buf2 = vec![0u8; 65536];
|
||||
let mut map = HashMap::new();
|
||||
let mut map: IcmpNatMap = HashMap::new();
|
||||
let mut gc_interval = tokio::time::interval(ICMP_NAT_GC_INTERVAL);
|
||||
loop {
|
||||
tokio::select! {
|
||||
rs = tokio_icmp_socket.recv(&mut buf1) => {
|
||||
let len = rs?;
|
||||
tokio_icmp_socket_recv(&buf1[..len],&inner_icmp_socket,&map,no_tun,&network).await?;
|
||||
tokio_icmp_socket_recv(&buf1[..len],&inner_icmp_socket,&mut map,no_tun,&network).await?;
|
||||
}
|
||||
rs = inner_icmp_socket.recv_from_to(&mut buf2) => {
|
||||
let (len,src,dst) = rs?;
|
||||
inner_icmp_socket_recv(&buf2[..len],src,dst,&tokio_icmp_socket,&mut map,no_tun,&network).await?;
|
||||
}
|
||||
_ = gc_interval.tick() => {
|
||||
evict_expired(&mut map, Instant::now(), ICMP_NAT_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 清理超时未收到应答的映射条目,防止 map 无界增长
|
||||
fn evict_expired(map: &mut IcmpNatMap, now: Instant, timeout: Duration) {
|
||||
let before = map.len();
|
||||
map.retain(|_, (_, created)| now.duration_since(*created) < timeout);
|
||||
let evicted = before - map.len();
|
||||
if evicted > 0 {
|
||||
log::debug!("icmp nat evicted {} expired entries", evicted);
|
||||
}
|
||||
}
|
||||
async fn tokio_icmp_socket_recv(
|
||||
buf: &[u8],
|
||||
inner_icmp_socket: &IcmpSocket,
|
||||
map: &HashMap<(Ipv4Addr, Identifier, SequenceNumber), Ipv4Addr>,
|
||||
map: &mut IcmpNatMap,
|
||||
no_tun: bool,
|
||||
network: &SharedNetworkAddr,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -88,7 +110,8 @@ async fn tokio_icmp_socket_recv(
|
||||
let mut src = ipv4.get_source();
|
||||
let identifier = Identifier::new(u16::from_be_bytes([payload[0], payload[1]]));
|
||||
let sequence_number = SequenceNumber::new(u16::from_be_bytes([payload[2], payload[3]]));
|
||||
let Some(dst) = map.get(&(src, identifier, sequence_number)) else {
|
||||
// 收到应答即完成一次 echo 交换,移除映射,避免条目残留
|
||||
let Some((dst, _)) = map.remove(&(src, identifier, sequence_number)) else {
|
||||
return Ok(());
|
||||
};
|
||||
if no_tun && src == Ipv4Addr::LOCALHOST {
|
||||
@@ -96,7 +119,7 @@ async fn tokio_icmp_socket_recv(
|
||||
}
|
||||
|
||||
inner_icmp_socket
|
||||
.send_from_to(ipv4.payload(), src.into(), (*dst).into())
|
||||
.send_from_to(ipv4.payload(), src.into(), dst.into())
|
||||
.await
|
||||
.context("sending ICMPv4 failed")?;
|
||||
Ok(())
|
||||
@@ -106,7 +129,7 @@ async fn inner_icmp_socket_recv(
|
||||
src: IpAddr,
|
||||
dst: IpAddr,
|
||||
tokio_icmp_socket: &UdpSocket,
|
||||
map: &mut HashMap<(Ipv4Addr, Identifier, SequenceNumber), Ipv4Addr>,
|
||||
map: &mut IcmpNatMap,
|
||||
no_tun: bool,
|
||||
network: &SharedNetworkAddr,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -131,9 +154,49 @@ async fn inner_icmp_socket_recv(
|
||||
|
||||
let identifier = Identifier::new(u16::from_be_bytes([payload[0], payload[1]]));
|
||||
let sequence_number = SequenceNumber::new(u16::from_be_bytes([payload[2], payload[3]]));
|
||||
map.insert((dst, identifier, sequence_number), src);
|
||||
map.insert((dst, identifier, sequence_number), (src, Instant::now()));
|
||||
tokio_icmp_socket
|
||||
.send_to(buf, SocketAddr::new(dst.into(), 0))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn entry(
|
||||
ip: &str,
|
||||
id: u16,
|
||||
seq: u16,
|
||||
created: Instant,
|
||||
) -> ((Ipv4Addr, Identifier, SequenceNumber), (Ipv4Addr, Instant)) {
|
||||
(
|
||||
(
|
||||
ip.parse().unwrap(),
|
||||
Identifier::new(id),
|
||||
SequenceNumber::new(seq),
|
||||
),
|
||||
(Ipv4Addr::new(10, 0, 0, 1), created),
|
||||
)
|
||||
}
|
||||
|
||||
/// 超时未应答的条目必须被清理,未超时的保留,map 不会无界增长
|
||||
#[test]
|
||||
fn test_evict_expired() {
|
||||
let now = Instant::now();
|
||||
let mut map: IcmpNatMap = HashMap::new();
|
||||
let (k_fresh, v_fresh) = entry("8.8.8.8", 1, 1, now);
|
||||
let (k_stale, v_stale) =
|
||||
entry("1.1.1.1", 2, 2, now - ICMP_NAT_TIMEOUT - Duration::from_secs(1));
|
||||
map.insert(k_fresh, v_fresh);
|
||||
map.insert(k_stale, v_stale);
|
||||
|
||||
evict_expired(&mut map, now, ICMP_NAT_TIMEOUT);
|
||||
|
||||
assert_eq!(map.len(), 1);
|
||||
assert!(map.contains_key(&k_fresh));
|
||||
assert!(!map.contains_key(&k_stale));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user