perf: avoid smoltcp packet copy

Reserve NIC packet headroom in BufferDevice's TxToken so smoltcp writes
the IP packet directly into a buf already carrying the ZCPacket NIC
offset. socks5/tcp_proxy then wrap that buf zero-copy via
ZCPacket::new_from_buf instead of copying through new_with_payload.

Benchmark: tunnel::packet_def::tests::smoltcp_zcpacket_construct_bench
  copy (new_with_payload)    1280B: 13.5M pps
  zerocopy (new_from_buf)    1280B: 28.3M pps  (2.09x)
  copy (new_with_payload)    4096B: 10.7M pps
  zerocopy (new_from_buf)    4096B: 19.1M pps  (1.79x)

Environment: AMD Ryzen 9 9955HX, rustc 1.95.0, --release, median of 3 runs
This commit is contained in:
fanyang
2026-06-25 23:16:54 +08:00
parent 130d89a057
commit 626a5fb4f1
4 changed files with 64 additions and 13 deletions
+12 -5
View File
@@ -28,7 +28,7 @@ use crate::{
ip_reassembler::IpReassembler,
tokio_smoltcp::{BufferSize, Net, NetConfig, channel_device},
},
tunnel::packet_def::{PacketType, ZCPacket},
tunnel::packet_def::{PacketType, ZCPacket, ZCPacketType},
};
use anyhow::Context;
use dashmap::DashMap;
@@ -377,13 +377,19 @@ impl Socks5ServerNet {
"receive from smoltcp stack and send to peer mgr packet, len = {}",
data.len()
);
let Some(ipv4) = Ipv4Packet::new(&data) else {
tracing::error!(?data, "smoltcp stack stream get non ipv4 packet");
let packet = ZCPacket::new_from_buf(
bytes::BytesMut::from(bytes::Bytes::from(data)),
ZCPacketType::NIC,
);
let Some(ipv4) = Ipv4Packet::new(packet.payload()) else {
tracing::error!(
payload_len = packet.payload_len(),
"smoltcp stack stream get non ipv4 packet"
);
continue;
};
let dst = ipv4.get_destination();
let packet = ZCPacket::new_with_payload(&data);
let Some(peer_manager) = peer_manager.upgrade() else {
tracing::warn!("peer manager is gone, smoltcp sender exited");
return;
@@ -412,7 +418,8 @@ impl Socks5ServerNet {
tcp_tx_size: 1024 * 128,
..Default::default()
}),
),
)
.with_packet_tx_headroom(ZCPacketType::NIC.get_packet_offsets().payload_offset),
);
let forward_tasks = Arc::new(std::sync::Mutex::new(forward_tasks));
+13 -4
View File
@@ -39,6 +39,8 @@ use super::CidrSet;
#[cfg(feature = "smoltcp")]
use super::tokio_smoltcp::{self, Net, NetConfig, channel_device};
#[cfg(feature = "smoltcp")]
use crate::tunnel::packet_def::ZCPacketType;
#[async_trait::async_trait]
pub(crate) trait NatDstConnector: Send + Sync + Clone + 'static {
@@ -575,13 +577,19 @@ impl<C: NatDstConnector> TcpProxy<C> {
?data,
"receive from smoltcp stack and send to peer mgr packet"
);
let Some(ipv4) = Ipv4Packet::new(&data) else {
tracing::error!(?data, "smoltcp stack stream get non ipv4 packet");
let packet = ZCPacket::new_from_buf(
bytes::BytesMut::from(bytes::Bytes::from(data)),
ZCPacketType::NIC,
);
let Some(ipv4) = Ipv4Packet::new(packet.payload()) else {
tracing::error!(
payload_len = packet.payload_len(),
"smoltcp stack stream get non ipv4 packet"
);
continue;
};
let dst = ipv4.get_destination();
let packet = ZCPacket::new_with_payload(&data);
let Some(peer_mgr) = peer_mgr.upgrade() else {
tracing::warn!("peer manager is gone, smoltcp sender exited");
return;
@@ -610,7 +618,8 @@ impl<C: NatDstConnector> TcpProxy<C> {
tcp_tx_size: 1024 * 16,
..Default::default()
}),
),
)
.with_packet_tx_headroom(ZCPacketType::NIC.get_packet_offsets().payload_offset),
);
net.set_any_ip(true);
self.smoltcp_net.lock().await.replace(net);
+30 -3
View File
@@ -33,6 +33,7 @@ where
pub struct BufferDevice {
caps: DeviceCapabilities,
max_burst_size: usize,
tx_headroom: usize,
recv_queue: VecDeque<Packet>,
send_queue: VecDeque<Packet>,
}
@@ -59,8 +60,9 @@ impl<'d> TxToken for BufferTxToken<'d> {
where
F: FnOnce(&mut [u8]) -> R,
{
let mut buffer = vec![0u8; len];
let result = f(&mut buffer);
let tx_headroom = self.0.tx_headroom;
let mut buffer = vec![0u8; tx_headroom + len];
let result = f(&mut buffer[tx_headroom..]);
self.0.send_queue.push_back(buffer);
@@ -98,11 +100,12 @@ impl Device for BufferDevice {
}
impl BufferDevice {
pub(crate) fn new(caps: DeviceCapabilities) -> BufferDevice {
pub(crate) fn new(caps: DeviceCapabilities, tx_headroom: usize) -> BufferDevice {
let max_burst_size = caps.max_burst_size.unwrap_or(DEFAULT_MAX_BURST_SIZE);
BufferDevice {
caps,
max_burst_size,
tx_headroom,
recv_queue: VecDeque::with_capacity(max_burst_size),
send_queue: VecDeque::with_capacity(max_burst_size),
}
@@ -123,3 +126,27 @@ impl BufferDevice {
self.recv_queue.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn buffer_device_reserves_tx_headroom() {
let mut caps = DeviceCapabilities::default();
caps.max_burst_size = Some(1);
let mut device = BufferDevice::new(caps, 16);
let token = device.transmit(Instant::now()).unwrap();
token.consume(4, |buf| {
assert_eq!(buf.len(), 4);
buf.copy_from_slice(&[1, 2, 3, 4]);
});
let mut queue = device.take_send_queue();
let packet = queue.pop_front().unwrap();
assert_eq!(packet.len(), 20);
assert_eq!(&packet[..16], &[0; 16]);
assert_eq!(&packet[16..], &[1, 2, 3, 4]);
}
}
+9 -1
View File
@@ -51,6 +51,7 @@ pub struct NetConfig {
pub ip_addr: IpCidr,
pub gateway: Vec<IpAddress>,
pub buffer_size: BufferSize,
pub packet_tx_headroom: usize,
}
impl NetConfig {
@@ -65,8 +66,14 @@ impl NetConfig {
ip_addr,
gateway,
buffer_size: buffer_size.unwrap_or_default(),
packet_tx_headroom: 0,
}
}
pub fn with_packet_tx_headroom(mut self, packet_tx_headroom: usize) -> Self {
self.packet_tx_headroom = packet_tx_headroom;
self
}
}
/// `Net` is the main interface to the network stack.
@@ -97,7 +104,8 @@ impl Net {
}
fn new2<D: device::AsyncDevice + 'static>(device: D, config: NetConfig) -> Net {
let mut buffer_device = BufferDevice::new(device.capabilities().clone());
let mut buffer_device =
BufferDevice::new(device.capabilities().clone(), config.packet_tx_headroom);
let mut iface = Interface::new(config.interface_config, &mut buffer_device, Instant::now());
let ip_addr = config.ip_addr;
iface.update_ip_addrs(|ip_addrs| {