perf(wasi): optimize data plane and extend host ABI to v3 (#2455)

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.
This commit is contained in:
KKRainbow
2026-07-28 00:29:08 +08:00
committed by GitHub
parent 7b506e25a7
commit d55e63b88e
40 changed files with 1342 additions and 220 deletions
+17 -1
View File
@@ -15,11 +15,22 @@
/// WebAssembly import module a WASI runtime must implement.
pub const HOST_IMPORT_MODULE: &str = "easytier_host";
/// AEAD algorithm identifiers accepted by the optional crypto imports.
pub const AEAD_AES_128_GCM: u32 = 1;
pub const AEAD_AES_256_GCM: u32 = 2;
pub const AEAD_CHACHA20_POLY1305: u32 = 3;
/// The Host could not authenticate an AEAD record.
///
/// Unlike other non-zero crypto statuses, this must not fall back to the
/// built-in implementation because an in-place open may have changed bytes.
pub const HOST_CRYPTO_AUTH_FAILED: i32 = -10;
/// Version of the JSON document accepted by `easytier_instance_create`.
pub const CORE_INSTANCE_CONFIG_VERSION: u32 = 14;
/// Version of the public data-plane guest export contract.
pub const DATA_PLANE_ABI_VERSION: u32 = 2;
pub const DATA_PLANE_ABI_VERSION: u32 = 3;
/// The guest exposes an instance-scoped data-plane operation broker.
pub const DATA_PLANE_CAPABILITY: u64 = 1 << 0;
@@ -27,6 +38,10 @@ pub const DATA_PLANE_CAPABILITY: u64 = 1 << 0;
pub const DATA_PLANE_TCP_CAPABILITY: u64 = 1 << 1;
/// The guest data plane supports UDP sockets.
pub const DATA_PLANE_UDP_CAPABILITY: u64 = 1 << 2;
/// Update the read deadline in `easytier_data_plane_resource_deadline_set`.
pub const DATA_PLANE_DEADLINE_READ: u32 = 1 << 0;
/// Update the write deadline in `easytier_data_plane_resource_deadline_set`.
pub const DATA_PLANE_DEADLINE_WRITE: u32 = 1 << 1;
/// Guest exports a WASI runtime calls to manage a core instance.
///
@@ -68,6 +83,7 @@ pub const DATA_PLANE_GUEST_EXPORTS: &[&str] = &[
"easytier_data_plane_udp_bind_submit",
"easytier_data_plane_udp_receive_submit",
"easytier_data_plane_udp_send_submit",
"easytier_data_plane_resource_deadline_set",
// Completion, result, and resource lifecycle.
"easytier_data_plane_completion_drain",
"easytier_data_plane_result_size",
+60
View File
@@ -0,0 +1,60 @@
use crate::{
events::{CoreEvent, CoreEventSink},
wasi::imports::emit_event,
};
#[derive(Debug)]
pub struct WasiHostEventSink {
handle: u64,
}
impl WasiHostEventSink {
pub fn new(handle: u64) -> Self {
Self { handle }
}
}
impl CoreEventSink for WasiHostEventSink {
fn emit(&self, event: CoreEvent) {
let kind = event_kind(&event);
let message = format!("{event:?}");
let _ = unsafe {
emit_event(
self.handle,
kind.as_ptr() as u32,
kind.len() as u32,
message.as_ptr() as u32,
message.len() as u32,
)
};
}
}
fn event_kind(event: &CoreEvent) -> &'static str {
match event {
CoreEvent::PeerAdded(_) => "peer_added",
CoreEvent::PeerRemoved(_) => "peer_removed",
CoreEvent::PeerConnAdded(_) => "peer_connection_added",
CoreEvent::PeerConnRemoved(_) => "peer_connection_removed",
CoreEvent::CredentialChanged => "credential_changed",
CoreEvent::ManualConnecting { .. } => "connecting",
CoreEvent::ManualConnectError { .. } => "connect_error",
CoreEvent::ListenerPlanFailed { .. } => "listener_plan_failed",
CoreEvent::ListenerAdded { .. } => "listener_added",
CoreEvent::ListenerRemoved { .. } => "listener_removed",
CoreEvent::ListenerAddFailed { .. } => "listener_add_failed",
CoreEvent::ListenerAcceptFailed { .. } => "listener_accept_failed",
CoreEvent::ListenerSocketAccepted { .. } => "listener_socket_accepted",
CoreEvent::ListenerAcceptedSocketHandleFailed { .. } => "listener_socket_handle_failed",
CoreEvent::TunnelAccepted { .. } => "tunnel_accepted",
CoreEvent::TunnelAdmissionFailed { .. } => "tunnel_admission_failed",
CoreEvent::UdpPortMappingEstablished { .. } => "udp_port_mapping_established",
CoreEvent::ProxyCidrsUpdated { .. } => "proxy_cidrs_updated",
CoreEvent::PublicIpv6LeaseChanged { .. } => "public_ipv6_lease_changed",
CoreEvent::PublicIpv6RoutesChanged { .. } => "public_ipv6_routes_changed",
CoreEvent::VpnPortalStarted(_) => "vpn_portal_started",
CoreEvent::VpnPortalClientConnected { .. } => "vpn_portal_client_connected",
CoreEvent::VpnPortalClientDisconnected { .. } => "vpn_portal_client_disconnected",
CoreEvent::GatewayPortForwardAdded(_) => "gateway_port_forward_added",
}
}
+1
View File
@@ -2,5 +2,6 @@
pub mod dns;
pub mod environment;
pub mod event;
pub mod packet;
pub mod socket;
+51
View File
@@ -9,6 +9,57 @@ pub(crate) const HOST_WOULD_BLOCK: i32 = -5;
#[link(wasm_import_module = "easytier_host")]
unsafe extern "C" {
/// Emits one best-effort instance event after the host copies both strings.
///
/// The host must not block the guest. A non-zero status drops this event
/// without affecting core execution.
pub(crate) fn emit_event(
handle: u64,
kind: u32,
kind_len: u32,
message: u32,
message_len: u32,
) -> i32;
/// Encrypts `text_len` bytes in place and writes the AEAD tag immediately
/// after them.
///
/// The guest reserves the algorithm's tag size in linear memory before
/// calling. Every non-zero result except
/// [`crate::wasi::abi::HOST_CRYPTO_AUTH_FAILED`] must leave the buffer
/// unchanged so the guest can use its built-in implementation.
#[cfg(feature = "wasi-crypto-offload")]
pub(crate) fn crypto_aead_seal(
algorithm: u32,
key: u32,
key_len: u32,
nonce: u32,
nonce_len: u32,
aad: u32,
aad_len: u32,
buffer: u32,
text_len: u32,
) -> i32;
/// Authenticates and decrypts `text_len` bytes in place using the AEAD tag
/// immediately after them.
///
/// Authentication failure may change the buffer and must return
/// [`crate::wasi::abi::HOST_CRYPTO_AUTH_FAILED`]. Every other non-zero
/// result must leave the buffer unchanged so the guest can fall back.
#[cfg(feature = "wasi-crypto-offload")]
pub(crate) fn crypto_aead_open(
algorithm: u32,
key: u32,
key_len: u32,
nonce: u32,
nonce_len: u32,
aad: u32,
aad_len: u32,
buffer: u32,
text_len: u32,
) -> i32;
/// Starts one TCP read into a host-owned pending operation.
///
/// The host records at most `capacity` bytes for `operation` and must not
+18 -11
View File
@@ -31,20 +31,18 @@ pub(super) fn new_wasi_core_runtime(
process_runtime: std::sync::Arc<crate::process_runtime::CoreProcessRuntime>,
environment_snapshot: HostConnectorEnvironmentSnapshot,
packet_sink: crate::host::packet::HostPacketSinkHandle,
event_sink: u64,
) -> anyhow::Result<WasiCoreRuntime> {
use std::sync::Arc;
use crate::host::{
dns::HostDnsResolver,
packet::{HostPacket, HostPacketSink},
socket::HostSocketRuntime,
};
use crate::host::{dns::HostDnsResolver, packet::HostPacketSink, socket::HostSocketRuntime};
use crate::{
connectivity::connector_host::new_connector_host,
instance::{CoreHostAdapters, CoreInstance},
wasi::adapter::{
dns::WasiHostDnsIo, environment::WasiHostConnectorEnvironmentIo,
packet::WasiHostPacketIo, socket::backend::WasiHostSocketBackend,
event::WasiHostEventSink, packet::WasiHostPacketIo,
socket::backend::WasiHostSocketBackend,
},
};
@@ -64,7 +62,8 @@ pub(super) fn new_wasi_core_runtime(
Arc::new(WasiHostPacketIo),
packet_sink,
));
let adapters = CoreHostAdapters::new(host, dns, packet_sink, process_runtime);
let mut adapters = CoreHostAdapters::new(host, dns, packet_sink, process_runtime);
adapters.events = Arc::new(WasiHostEventSink::new(event_sink));
let core = CoreInstance::from_toml(config, adapters)?;
Ok(WasiCoreRuntime {
@@ -85,7 +84,7 @@ mod abi {
use crate::{
config::toml::{ConfigLoader as _, TomlConfig},
foundation::time::{clear_domain, enter_domain, next_deadline_millis},
host::packet::HostPacketSinkHandle,
host::packet::{HostPacket, HostPacketSinkHandle},
instance::{
CoreInstanceState,
manager::{InstanceFactory, ManagedInstance},
@@ -164,6 +163,7 @@ mod abi {
domain: u64,
environment: crate::connectivity::connector_host::HostConnectorEnvironmentSnapshot,
packet_sink: HostPacketSinkHandle,
event_sink: u64,
}
struct WasiInstance {
@@ -220,6 +220,7 @@ mod abi {
self.process_runtime.clone(),
context.environment,
context.packet_sink,
context.event_sink,
)?
};
@@ -348,8 +349,11 @@ mod abi {
fn drive(&self) -> anyhow::Result<()> {
let _domain = enter_domain(self.domain);
let advance_timers = next_deadline_millis(self.domain) == Some(0);
let mut execution = self.execution.lock().unwrap();
execution.drive_again = execution.runtime_driver.drive(&execution.runtime)
execution.drive_again = execution
.runtime_driver
.drive(&execution.runtime, advance_timers)
== RuntimeDriveOutcome::BudgetExhausted;
if execution
@@ -561,13 +565,15 @@ mod abi {
#[unsafe(no_mangle)]
/// Creates one core instance from a versioned envelope containing TOML.
///
/// `config_pointer` must name a live ABI buffer and `packet_sink_handle`
/// identifies the host sink used for locally delivered raw IP packets.
/// `config_pointer` must name a live ABI buffer. `packet_sink_handle` and
/// `event_sink_handle` identify the host sinks used for locally delivered
/// raw IP packets and best-effort instance events.
/// Returns zero on failure; retrieve the reason through the error exports.
pub extern "C" fn easytier_instance_create(
config_pointer: u32,
config_length: u32,
packet_sink_handle: u64,
event_sink_handle: u64,
) -> u64 {
let encoded = match read_guest_buffer(config_pointer, config_length, MAX_CREATE_CONFIG_LEN)
{
@@ -601,6 +607,7 @@ mod abi {
domain: handle,
environment: create_config.environment,
packet_sink: HostPacketSinkHandle(packet_sink_handle),
event_sink: event_sink_handle,
},
);
let instance = match instance {
@@ -9,8 +9,8 @@ use crate::{
},
wasi::{
abi::{
DATA_PLANE_ABI_VERSION, DATA_PLANE_CAPABILITY, DATA_PLANE_TCP_CAPABILITY,
DATA_PLANE_UDP_CAPABILITY,
DATA_PLANE_ABI_VERSION, DATA_PLANE_CAPABILITY, DATA_PLANE_DEADLINE_READ,
DATA_PLANE_DEADLINE_WRITE, DATA_PLANE_TCP_CAPABILITY, DATA_PLANE_UDP_CAPABILITY,
},
wire::{
data_plane::{
@@ -42,12 +42,10 @@ impl WasiInstance {
self.core.core().data_plane_session()
}
fn submit_data_plane(
fn submit_data_plane<T>(
&self,
submit: impl FnOnce(
&std::sync::Arc<WasiDataPlaneSession>,
) -> Result<DataPlaneOperationId, DataPlaneError>,
) -> Result<DataPlaneOperationId, DataPlaneError> {
submit: impl FnOnce(&std::sync::Arc<WasiDataPlaneSession>) -> Result<T, DataPlaneError>,
) -> Result<T, DataPlaneError> {
let execution = self.execution.lock().unwrap();
let _domain = crate::foundation::time::enter_domain(self.domain);
let _runtime = execution.runtime.enter();
@@ -316,7 +314,6 @@ pub extern "C" fn easytier_data_plane_tcp_read_submit(
handle: u64,
stream: u64,
max_len: u32,
timeout_ms: u64,
output_operation: u32,
) -> i32 {
let stream = match resource_id(stream) {
@@ -327,9 +324,7 @@ pub extern "C" fn easytier_data_plane_tcp_read_submit(
}
};
submit_operation(handle, output_operation, |instance| {
instance.submit_data_plane(|session| {
session.submit_tcp_read(stream, max_len as usize, timeout(timeout_ms))
})
instance.submit_data_plane(|session| session.submit_tcp_read(stream, max_len as usize))
})
}
@@ -339,7 +334,6 @@ pub extern "C" fn easytier_data_plane_tcp_write_submit(
stream: u64,
data_pointer: u32,
data_length: u32,
timeout_ms: u64,
output_operation: u32,
) -> i32 {
let stream = match resource_id(stream) {
@@ -357,9 +351,7 @@ pub extern "C" fn easytier_data_plane_tcp_write_submit(
}
};
submit_operation(handle, output_operation, |instance| {
instance.submit_data_plane(|session| {
session.submit_tcp_write(stream, data, timeout(timeout_ms))
})
instance.submit_data_plane(|session| session.submit_tcp_write(stream, data))
})
}
@@ -388,7 +380,6 @@ pub extern "C" fn easytier_data_plane_udp_receive_submit(
handle: u64,
socket: u64,
max_len: u32,
timeout_ms: u64,
output_operation: u32,
) -> i32 {
let socket = match resource_id(socket) {
@@ -399,9 +390,7 @@ pub extern "C" fn easytier_data_plane_udp_receive_submit(
}
};
submit_operation(handle, output_operation, |instance| {
instance.submit_data_plane(|session| {
session.submit_udp_receive(socket, max_len as usize, timeout(timeout_ms))
})
instance.submit_data_plane(|session| session.submit_udp_receive(socket, max_len as usize))
})
}
@@ -412,7 +401,6 @@ pub extern "C" fn easytier_data_plane_udp_send_submit(
peer_address: u32,
data_pointer: u32,
data_length: u32,
timeout_ms: u64,
output_operation: u32,
) -> i32 {
let socket = match resource_id(socket) {
@@ -437,9 +425,36 @@ pub extern "C" fn easytier_data_plane_udp_send_submit(
}
};
submit_operation(handle, output_operation, |instance| {
instance.submit_data_plane(|session| session.submit_udp_send(socket, peer_address, data))
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_resource_deadline_set(
handle: u64,
resource: u64,
direction: u32,
timeout_ms: u64,
) -> i32 {
let resource = match resource_id(resource) {
Ok(resource) => resource,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
let read = direction & DATA_PLANE_DEADLINE_READ != 0;
let write = direction & DATA_PLANE_DEADLINE_WRITE != 0;
if direction == 0 || direction & !(DATA_PLANE_DEADLINE_READ | DATA_PLANE_DEADLINE_WRITE) != 0 {
let error = invalid_input(format!("invalid deadline direction {direction}"));
set_instance_error(handle, error.message());
return error_status(error.kind());
}
data_plane_call(handle, |instance| {
instance.submit_data_plane(|session| {
session.submit_udp_send(socket, peer_address, data, timeout(timeout_ms))
})
session.set_resource_deadline(resource, read, write, timeout(timeout_ms))
})?;
Ok(0)
})
}
+62 -11
View File
@@ -41,12 +41,14 @@ impl RuntimeDriver {
}
}
pub(super) fn drive(&self, runtime: &Runtime) -> RuntimeDriveOutcome {
// First give the timer driver a non-blocking turn. The quiescence hook
// stays disabled here so an expired timer can wake its task.
runtime.block_on(async {
tokio::time::sleep(Duration::ZERO).await;
});
pub(super) fn drive(&self, runtime: &Runtime, advance_timers: bool) -> RuntimeDriveOutcome {
if advance_timers {
// Give the timer driver a turn before enabling the quiescence hook
// so an expired timer can wake its task.
runtime.block_on(async {
tokio::time::sleep(Duration::ZERO).await;
});
}
let _active = RuntimeDriverGuard::activate(self.state.as_ref());
runtime.block_on(async {
@@ -101,9 +103,10 @@ impl Drop for RuntimeDriverGuard<'_> {
mod tests {
use std::{future::poll_fn, sync::Arc, task::Poll};
use tokio::{runtime::Builder, sync::Notify};
use tokio::{runtime::Builder, sync::Notify, time::Duration};
use super::{RuntimeDriveOutcome, RuntimeDriver};
use crate::wasi::time::{enter_domain, next_deadline_millis};
fn runtime(driver: &RuntimeDriver) -> tokio::runtime::Runtime {
let park_driver = driver.clone();
@@ -124,10 +127,13 @@ mod tests {
Poll::<()>::Pending
}));
assert_eq!(driver.drive(&runtime), RuntimeDriveOutcome::BudgetExhausted);
assert_eq!(
driver.drive(&runtime, false),
RuntimeDriveOutcome::BudgetExhausted
);
task.abort();
while driver.drive(&runtime) == RuntimeDriveOutcome::BudgetExhausted {}
while driver.drive(&runtime, false) == RuntimeDriveOutcome::BudgetExhausted {}
assert!(task.is_finished());
}
@@ -141,11 +147,56 @@ mod tests {
task_notify.notified().await;
});
assert_eq!(driver.drive(&runtime), RuntimeDriveOutcome::Quiescent);
assert_eq!(
driver.drive(&runtime, false),
RuntimeDriveOutcome::Quiescent
);
assert!(!task.is_finished());
notify.notify_one();
while driver.drive(&runtime) == RuntimeDriveOutcome::BudgetExhausted {}
while driver.drive(&runtime, false) == RuntimeDriveOutcome::BudgetExhausted {}
assert!(task.is_finished());
}
#[test]
fn advances_expired_timers_only_when_requested() {
let driver = RuntimeDriver::default();
let runtime = runtime(&driver);
let task = runtime.spawn(async {
tokio::time::sleep(Duration::ZERO).await;
});
assert_eq!(
driver.drive(&runtime, false),
RuntimeDriveOutcome::Quiescent
);
assert!(!task.is_finished());
assert_eq!(driver.drive(&runtime, true), RuntimeDriveOutcome::Quiescent);
assert!(task.is_finished());
}
#[test]
fn tracked_expired_timer_requests_advancement() {
let _domain = enter_domain(7);
let driver = RuntimeDriver::default();
let runtime = runtime(&driver);
let task = runtime.spawn(async {
crate::foundation::time::sleep(Duration::ZERO).await;
});
assert_eq!(
driver.drive(&runtime, false),
RuntimeDriveOutcome::Quiescent
);
assert_eq!(next_deadline_millis(7), Some(0));
let advance_timers = next_deadline_millis(7) == Some(0);
assert_eq!(
driver.drive(&runtime, advance_timers),
RuntimeDriveOutcome::Quiescent
);
assert!(task.is_finished());
assert_eq!(next_deadline_millis(7), None);
}
}
+1 -1
View File
@@ -230,7 +230,7 @@ mod tracked {
}
}
pub use tracked::{Duration, Instant, Interval, error, interval, sleep, timeout};
pub use tracked::{Duration, Instant, Interval, error, interval, sleep, sleep_until, timeout};
pub(crate) use tracked::{clear_domain, enter_domain, next_deadline_millis};