mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-08-07 21:19:51 +00:00
d55e63b88e
Overhaul the WASI guest data plane for throughput and add the host capabilities it relies on. The externally driven Tokio runtime now runs its timer pre-turn only when a tracked deadline has expired, and all WASI-reachable timers (STUN, port mapping, WebClient, UDP flow cleanup) go through the portable time facade so conditional timer driving cannot starve them. Data plane: - Move read/write deadlines onto TCP and UDP resources with one ABI setter per direction, reuse a single expiration timer per resource, and drop timeout arguments from the four hot data-plane submissions (ABI v3). Checked absolute instants treat unrepresentable finite timeouts as unbounded instead of panicking. - Batch host traffic: vectored TCP frame writes combine queued slices into one host operation, and reads request a bounded 64 KiB while retaining excess bytes in the stream buffer. - Complete TCP writes inside the guest with cancellation-safe writes, reporting the completed prefix before honoring cancellation or timeout so hosts never replay bytes. - Repoll smoltcp egress immediately on zero poll delay, enlarge virtual UDP receive queues to 128 KiB payload with 128 metadata slots, and bound UDP session receive buffers to 8 KiB plus one byte while keeping oversized-datagram detection. Host integration: - Add optional algorithm-neutral AEAD seal/open imports with the ring backend as fallback, and pin the ring AES-128-GCM wire vector so the Go host stays interoperable. - Forward instance events to hosts through one best-effort, synchronous, non-blocking import. - Add a repository-owned build entry point for the Go host artifact: Binaryen 131 at -O4 with cached, SHA-256-verified official archives.
418 lines
14 KiB
Rust
418 lines
14 KiB
Rust
use std::{
|
|
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4},
|
|
sync::Arc,
|
|
};
|
|
|
|
use async_trait::async_trait;
|
|
#[cfg(any(feature = "wireguard", test))]
|
|
use easytier_core::socket::{
|
|
NetNamespace,
|
|
udp::{UdpSessionAcceptKind, UdpSessionListenRequest, UdpSessionSocketListener},
|
|
};
|
|
use easytier_core::socket::{
|
|
SocketContext,
|
|
udp::{
|
|
MAX_UDP_DATAGRAM_SIZE, MAX_UDP_SESSION_DATAGRAM_SIZE, UdpBindOptions, UdpSocketDatagram,
|
|
UdpSocketPurpose, UdpSocketRecvMeta, UdpSocketSendMeta, VirtualUdpSocket,
|
|
VirtualUdpSocketFactory,
|
|
},
|
|
};
|
|
use tokio::net::UdpSocket;
|
|
|
|
#[cfg(any(feature = "wireguard", test))]
|
|
use crate::host_runtime::{NativeHostRuntime, native_host_runtime};
|
|
use crate::{
|
|
common::netns::NetNS,
|
|
tunnel::common::{BindDev, bind},
|
|
};
|
|
|
|
use super::udp_src;
|
|
|
|
#[cfg(any(feature = "wireguard", test))]
|
|
pub(crate) type RuntimeUdpSessionSocketListener = UdpSessionSocketListener<NativeHostRuntime>;
|
|
|
|
#[cfg(any(feature = "wireguard", test))]
|
|
pub(crate) fn new_runtime_udp_session_listener(
|
|
url: url::Url,
|
|
mut request: UdpSessionListenRequest,
|
|
accept_kind: UdpSessionAcceptKind,
|
|
net_ns: NetNS,
|
|
) -> RuntimeUdpSessionSocketListener {
|
|
request.bind.context.netns = net_ns.name().map(NetNamespace::new);
|
|
let runtime = native_host_runtime();
|
|
UdpSessionSocketListener::new_with_request(url, request, accept_kind, runtime)
|
|
}
|
|
|
|
pub struct RuntimeUdpSocket {
|
|
socket: Arc<UdpSocket>,
|
|
context: SocketContext,
|
|
}
|
|
|
|
impl RuntimeUdpSocket {
|
|
#[cfg(test)]
|
|
fn new(socket: Arc<UdpSocket>) -> Self {
|
|
Self::new_with_context(socket, SocketContext::default())
|
|
}
|
|
|
|
pub(crate) fn new_with_context(socket: Arc<UdpSocket>, context: SocketContext) -> Self {
|
|
if let Err(err) = udp_src::enable_recv_pktinfo(&socket) {
|
|
tracing::debug!(?err, "enable udp pktinfo failed");
|
|
}
|
|
Self { socket, context }
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
pub(crate) fn socket(&self) -> Arc<UdpSocket> {
|
|
self.socket.clone()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VirtualUdpSocket for RuntimeUdpSocket {
|
|
fn local_addr(&self) -> std::io::Result<SocketAddr> {
|
|
self.socket.local_addr()
|
|
}
|
|
|
|
fn socket_context(&self) -> SocketContext {
|
|
self.context.clone()
|
|
}
|
|
|
|
async fn send_to(&self, data: &[u8], addr: SocketAddr) -> std::io::Result<usize> {
|
|
self.socket.send_to(data, addr).await
|
|
}
|
|
|
|
async fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, SocketAddr)> {
|
|
self.socket.recv_from(buf).await
|
|
}
|
|
|
|
async fn send_to_with_meta(
|
|
&self,
|
|
data: &[u8],
|
|
addr: SocketAddr,
|
|
meta: UdpSocketSendMeta,
|
|
) -> std::io::Result<usize> {
|
|
if let (Some(IpAddr::V6(src)), Some(ifindex), SocketAddr::V6(dst)) =
|
|
(meta.src_ip, meta.src_ifindex, addr)
|
|
{
|
|
return udp_src::send_to_with_src_ipv6(&self.socket, src, ifindex, dst, data);
|
|
}
|
|
if let Some(src_ip) = meta.src_ip {
|
|
return udp_src::send_to_with_src_ip(&self.socket, src_ip, addr, data).await;
|
|
}
|
|
self.socket.try_send_to(data, addr)
|
|
}
|
|
|
|
async fn recv_from_with_meta(
|
|
&self,
|
|
buf: &mut [u8],
|
|
) -> std::io::Result<(usize, SocketAddr, UdpSocketRecvMeta)> {
|
|
let (len, addr, dst_ip) = udp_src::recv_from_with_dst_ip(&self.socket, buf).await?;
|
|
Ok((len, addr, UdpSocketRecvMeta { dst_ip }))
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
async fn recv_datagram(&self) -> std::io::Result<UdpSocketDatagram> {
|
|
let (payload, remote_addr, dst_ip) =
|
|
udp_src::recv_datagram_with_dst_ip(&self.socket, MAX_UDP_DATAGRAM_SIZE).await?;
|
|
Ok(UdpSocketDatagram {
|
|
payload,
|
|
remote_addr,
|
|
meta: UdpSocketRecvMeta { dst_ip },
|
|
})
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
async fn recv_session_datagram(&self) -> std::io::Result<UdpSocketDatagram> {
|
|
let (payload, remote_addr, dst_ip) =
|
|
udp_src::recv_datagram_with_dst_ip(&self.socket, MAX_UDP_SESSION_DATAGRAM_SIZE).await?;
|
|
Ok(UdpSocketDatagram {
|
|
payload,
|
|
remote_addr,
|
|
meta: UdpSocketRecvMeta { dst_ip },
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
pub(crate) struct RuntimeUdpSocketFactory;
|
|
|
|
impl RuntimeUdpSocketFactory {
|
|
pub(crate) fn new() -> Self {
|
|
Self
|
|
}
|
|
|
|
fn bind_device_for(&self, options: &UdpBindOptions) -> BindDev {
|
|
if let Some(bind_device) = &options.bind_device {
|
|
return BindDev::from(bind_device.as_str());
|
|
}
|
|
|
|
if matches!(
|
|
options.purpose,
|
|
UdpSocketPurpose::DirectConnect
|
|
| UdpSocketPurpose::PortBoundListener
|
|
| UdpSocketPurpose::PortForward
|
|
) {
|
|
return BindDev::Auto;
|
|
}
|
|
|
|
BindDev::Disabled
|
|
}
|
|
|
|
fn reuse_addr_for(&self, options: &UdpBindOptions) -> bool {
|
|
options.reuse_addr
|
|
|| (matches!(
|
|
options.purpose,
|
|
UdpSocketPurpose::PortBoundListener
|
|
| UdpSocketPurpose::ProxyNat
|
|
| UdpSocketPurpose::PortForward
|
|
) && !cfg!(target_os = "windows"))
|
|
}
|
|
|
|
fn bind_udp_socket(&self, options: UdpBindOptions) -> anyhow::Result<Arc<RuntimeUdpSocket>> {
|
|
let context = options.context.clone();
|
|
let bind_addr = options
|
|
.local_addr
|
|
.unwrap_or_else(|| SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)));
|
|
let bind_device = self.bind_device_for(&options);
|
|
let reuse_addr = self.reuse_addr_for(&options);
|
|
let socket = bind::<UdpSocket>()
|
|
.addr(bind_addr)
|
|
.dev(bind_device)
|
|
.maybe_net_ns(Some(NetNS::from_socket_context(&context)))
|
|
.only_v6(options.only_v6)
|
|
.reuse_addr(reuse_addr)
|
|
.reuse_port(options.reuse_port)
|
|
.maybe_socket_mark(context.socket_mark)
|
|
.call()?;
|
|
Ok(Arc::new(RuntimeUdpSocket::new_with_context(
|
|
Arc::new(socket),
|
|
context,
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl VirtualUdpSocketFactory for RuntimeUdpSocketFactory {
|
|
type Socket = RuntimeUdpSocket;
|
|
|
|
async fn bind_udp(&self, options: UdpBindOptions) -> anyhow::Result<Arc<Self::Socket>> {
|
|
self.bind_udp_socket(options)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use easytier_core::{
|
|
socket::SocketListener,
|
|
socket::udp::{
|
|
UdpSessionListenRequest, send_v4_hole_punch_control_packet,
|
|
send_v6_hole_punch_control_packet,
|
|
},
|
|
};
|
|
|
|
use crate::host_runtime::native_host_runtime;
|
|
|
|
use super::*;
|
|
|
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
|
#[tokio::test]
|
|
async fn runtime_udp_socket_reports_ipv4_destination_ip() {
|
|
let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap());
|
|
let runtime_socket = RuntimeUdpSocket::new(socket.clone());
|
|
let client = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
|
client
|
|
.send_to(
|
|
b"pktinfo",
|
|
SocketAddr::from(([127, 0, 0, 1], socket.local_addr().unwrap().port())),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let mut buf = [0; 32];
|
|
let (len, _peer, meta) = runtime_socket.recv_from_with_meta(&mut buf).await.unwrap();
|
|
|
|
assert_eq!(&buf[..len], b"pktinfo");
|
|
assert_eq!(meta.dst_ip, Some(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST)));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn runtime_udp_socket_receives_owned_datagram_with_destination_ip() {
|
|
let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await.unwrap());
|
|
let runtime_socket = RuntimeUdpSocket::new(socket.clone());
|
|
let client = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
|
client
|
|
.send_to(
|
|
b"owned-pktinfo",
|
|
SocketAddr::from(([127, 0, 0, 1], socket.local_addr().unwrap().port())),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let datagram = runtime_socket.recv_datagram().await.unwrap();
|
|
|
|
assert_eq!(datagram.payload, b"owned-pktinfo".as_slice());
|
|
assert_eq!(
|
|
datagram.meta.dst_ip,
|
|
Some(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST))
|
|
);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn runtime_udp_socket_drops_truncated_datagrams() {
|
|
let socket = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
|
|
let runtime_socket = RuntimeUdpSocket::new(socket.clone());
|
|
let client = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
|
let server_addr = socket.local_addr().unwrap();
|
|
|
|
client
|
|
.send_to(&vec![0xAA; MAX_UDP_SESSION_DATAGRAM_SIZE + 1], server_addr)
|
|
.await
|
|
.unwrap();
|
|
client
|
|
.send_to(b"after-oversized", server_addr)
|
|
.await
|
|
.unwrap();
|
|
|
|
let datagram = tokio::time::timeout(
|
|
std::time::Duration::from_secs(1),
|
|
runtime_socket.recv_session_datagram(),
|
|
)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
assert_eq!(datagram.payload, b"after-oversized".as_slice());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn runtime_v4_hole_punch_control_packet_is_forwarded() {
|
|
let local_addr = SocketAddr::from(([0, 0, 0, 0], 0));
|
|
let mut listener = new_runtime_udp_session_listener(
|
|
"udp://0.0.0.0:0".parse().unwrap(),
|
|
UdpSessionListenRequest::new(UdpBindOptions::port_bound_listener(local_addr)),
|
|
UdpSessionAcceptKind::EasyTierMux,
|
|
NetNS::new(None),
|
|
);
|
|
listener.listen().await.unwrap();
|
|
|
|
let receiver = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
|
let runtime = native_host_runtime();
|
|
send_v4_hole_punch_control_packet(
|
|
runtime.as_ref(),
|
|
SocketContext::default(),
|
|
listener.local_url().port().unwrap(),
|
|
match receiver.local_addr().unwrap() {
|
|
SocketAddr::V4(addr) => addr,
|
|
SocketAddr::V6(_) => unreachable!(),
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let mut buf = [0; 128];
|
|
tokio::time::timeout(
|
|
std::time::Duration::from_secs(2),
|
|
receiver.recv_from(&mut buf),
|
|
)
|
|
.await
|
|
.expect("timeout waiting for v4 hole-punch packet")
|
|
.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn runtime_v6_hole_punch_control_packet_is_forwarded() {
|
|
let local_addr = "[::]:0".parse().unwrap();
|
|
let mut listener = new_runtime_udp_session_listener(
|
|
"udp://[::]:0".parse().unwrap(),
|
|
UdpSessionListenRequest::new(UdpBindOptions::port_bound_listener(local_addr)),
|
|
UdpSessionAcceptKind::EasyTierMux,
|
|
NetNS::new(None),
|
|
);
|
|
listener.listen().await.unwrap();
|
|
|
|
let receiver = UdpSocket::bind("[::]:0").await.unwrap();
|
|
let runtime = native_host_runtime();
|
|
send_v6_hole_punch_control_packet(
|
|
runtime.as_ref(),
|
|
SocketContext::default(),
|
|
listener.local_url().port().unwrap(),
|
|
match receiver.local_addr().unwrap() {
|
|
SocketAddr::V6(addr) => addr,
|
|
SocketAddr::V4(_) => unreachable!(),
|
|
},
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let mut buf = [0; 128];
|
|
tokio::time::timeout(
|
|
std::time::Duration::from_secs(2),
|
|
receiver.recv_from(&mut buf),
|
|
)
|
|
.await
|
|
.expect("timeout waiting for v6 hole-punch packet")
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn factory_interprets_bind_defaults_by_purpose() {
|
|
let listener_addr = SocketAddr::from(([0, 0, 0, 0], 11010));
|
|
let factory = RuntimeUdpSocketFactory::new();
|
|
|
|
assert!(matches!(
|
|
factory.bind_device_for(&UdpBindOptions::port_bound_listener(listener_addr)),
|
|
BindDev::Auto
|
|
));
|
|
assert!(matches!(
|
|
factory.bind_device_for(&UdpBindOptions::direct_connect()),
|
|
BindDev::Auto
|
|
));
|
|
assert!(matches!(
|
|
factory.bind_device_for(&UdpBindOptions::port_forward(listener_addr)),
|
|
BindDev::Auto
|
|
));
|
|
assert!(matches!(
|
|
factory.bind_device_for(&UdpBindOptions::port_lease(listener_addr)),
|
|
BindDev::Disabled
|
|
));
|
|
assert!(matches!(
|
|
factory.bind_device_for(&UdpBindOptions::hole_punch_control()),
|
|
BindDev::Disabled
|
|
));
|
|
assert_eq!(
|
|
factory.reuse_addr_for(&UdpBindOptions::port_bound_listener(listener_addr)),
|
|
!cfg!(target_os = "windows")
|
|
);
|
|
assert!(!factory.reuse_addr_for(&UdpBindOptions::hole_punch_control()));
|
|
assert_eq!(
|
|
factory.reuse_addr_for(&UdpBindOptions::proxy_nat()),
|
|
!cfg!(target_os = "windows")
|
|
);
|
|
assert_eq!(
|
|
factory.reuse_addr_for(&UdpBindOptions::port_forward(listener_addr)),
|
|
!cfg!(target_os = "windows")
|
|
);
|
|
assert!(!factory.reuse_addr_for(&UdpBindOptions::port_lease(listener_addr)));
|
|
}
|
|
|
|
#[test]
|
|
fn factory_applies_listener_bind_device_option() {
|
|
let listener_addr = SocketAddr::from(([0, 0, 0, 0], 11010));
|
|
let factory = RuntimeUdpSocketFactory::new();
|
|
let options = UdpBindOptions::port_bound_listener(listener_addr)
|
|
.with_bind_device(Some("eth0".to_owned()));
|
|
|
|
match factory.bind_device_for(&options) {
|
|
BindDev::Custom(dev) => assert_eq!(dev, "eth0"),
|
|
bind_device => panic!("unexpected bind device: {bind_device:?}"),
|
|
}
|
|
assert!(matches!(
|
|
factory.bind_device_for(&UdpBindOptions::hole_punch_control()),
|
|
BindDev::Disabled
|
|
));
|
|
}
|
|
}
|