refactor(core): separate portable core from native runtime (#2451)

Create easytier-core as the portable owner of configuration,
connectivity, tunnels, peer and routing state, gateways, management,
the data plane, and instance lifecycle. Keep operating-system
integration, native protocol engines, process startup, and presentation
in easytier behind explicit Host capability adapters.

Create easytier-proto to own schemas, generated RPC types, descriptors,
and feature-scoped protocol slices. Remove runtime protobuf reflection
from core while preserving unknown route-peer fields across forwarding.

Normalize instance construction through CoreInstance, CoreHostAdapters,
CoreProcessRuntime, and InstanceManager. Make the runtime config store
the only authoritative mutable configuration after startup.

Move the portable TCP/UDP data plane into core and extract a generic
OperationBroker for completion, cancellation, disposal, and capacity
accounting. Expose the session-based FFI v2 completion API and keep the
WASI guest ABI, wire schemas, and adapters with core.

Migrate CLI, GUI, web, FFI, Android JNI, OHOS, uptime, and mobile
consumers to the shared manager and core state. Add explicit user/web
config ownership and revision-aware web reconciliation.

Preserve configuration, wire, and management behavior while fixing
regressions discovered by the full platform and integration matrix:

- inherit advertised relay capabilities in foreign networks;
- refresh OSPF peer state immediately after runtime config changes;
- restore CLI GlobalCtx event output without forcing GUI logging;
- retain legacy encryption names and standalone RPC tunnel metadata;
- restore ICMP host composition and fragmented UDP handling;
- use portable 64-bit atomics on 32-bit MIPS targets; and
- retain discarded operations until late cancellation completes.

Validate the refactor across 45 GitHub checks, including Linux, macOS,
Windows, FreeBSD, web, GUI, Android, OHOS, feature profiles, and
three-node and subnet-proxy integration tests.

BREAKING CHANGE: internal Rust module paths are not preserved. Legacy
native data-plane APIs are replaced by the session-based FFI v2 API.
The dedicated Android data-plane wrapper is removed.
This commit is contained in:
KKRainbow
2026-07-26 15:41:55 +08:00
committed by GitHub
parent 346f32d3d0
commit 021f523431
523 changed files with 102785 additions and 67067 deletions
+85
View File
@@ -0,0 +1,85 @@
//! Stable ABI contract between the EasyTier WASI guest and its runtime.
//!
//! A runtime must provide every function in the `easytier_host` import module
//! and call the guest lifecycle exports listed in [`GUEST_EXPORTS`]. The raw
//! import declarations live in the target-only `imports` Module.
//! Rust visibility does not define this cross-language contract; the imported
//! and exported WebAssembly symbol names and signatures do.
//!
//! Every `u32` pointer is an offset in wasm32 guest linear memory, never a
//! native host pointer. The runtime must copy input bytes before an import
//! returns and may write result bytes only during the matching `take_*` call.
//! An operation ID belongs to core until a terminal `take_*` call consumes it
//! or the runtime receives the `cancel_operation` import.
/// WebAssembly import module a WASI runtime must implement.
pub const HOST_IMPORT_MODULE: &str = "easytier_host";
/// 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;
/// The guest exposes an instance-scoped data-plane operation broker.
pub const DATA_PLANE_CAPABILITY: u64 = 1 << 0;
/// The guest data plane supports TCP streams and listeners.
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;
/// Guest exports a WASI runtime calls to manage a core instance.
///
/// Buffer allocation precedes config, packet, and error-copy calls. Instance
/// creation and lifecycle use the returned instance handle. A runtime drives
/// all asynchronous guest work through `easytier_instance_drive` and host
/// completion notifications.
pub const GUEST_EXPORTS: &[&str] = &[
// Guest-memory buffers.
"easytier_buffer_alloc",
"easytier_buffer_free",
// Instance lifecycle and external runtime driving.
"easytier_instance_create",
"easytier_instance_start",
"easytier_instance_stop",
"easytier_instance_drive",
"easytier_instance_notify_completions",
"easytier_instance_state",
"easytier_instance_next_deadline_millis",
// Raw IP packet ingress and error retrieval.
"easytier_instance_send_packet",
"easytier_instance_drop",
"easytier_instance_error_len",
"easytier_instance_error_copy",
];
/// Guest exports present when the core is built with the smoltcp data plane.
#[cfg(feature = "proxy-smoltcp-stack")]
pub const DATA_PLANE_GUEST_EXPORTS: &[&str] = &[
// ABI discovery.
"easytier_data_plane_abi_version",
"easytier_data_plane_capabilities",
// Data-plane operation submission.
"easytier_data_plane_tcp_connect_submit",
"easytier_data_plane_tcp_bind_submit",
"easytier_data_plane_tcp_accept_submit",
"easytier_data_plane_tcp_read_submit",
"easytier_data_plane_tcp_write_submit",
"easytier_data_plane_udp_bind_submit",
"easytier_data_plane_udp_receive_submit",
"easytier_data_plane_udp_send_submit",
// Completion, result, and resource lifecycle.
"easytier_data_plane_completion_drain",
"easytier_data_plane_result_size",
"easytier_data_plane_tcp_connect_result_take",
"easytier_data_plane_tcp_bind_result_take",
"easytier_data_plane_tcp_accept_result_take",
"easytier_data_plane_tcp_read_result_take",
"easytier_data_plane_tcp_write_result_take",
"easytier_data_plane_udp_bind_result_take",
"easytier_data_plane_udp_receive_result_take",
"easytier_data_plane_udp_send_result_take",
"easytier_data_plane_operation_cancel",
"easytier_data_plane_operation_free",
"easytier_data_plane_resource_close",
];
+121
View File
@@ -0,0 +1,121 @@
use std::{io, net::IpAddr, task::Poll};
use crate::{
host::{
dns::{DnsQuery, DnsSrvRecord, HostDnsIo},
socket::HostOperationId,
},
wasi::{
imports::{
HOST_PENDING, cancel_operation, start_dns_resolve, start_dns_srv, start_dns_txt,
take_dns_resolve, take_dns_srv, take_dns_txt,
},
wire::{
common::host_error,
dns::{decode_addresses, decode_srv, decode_txt, encode_query},
},
},
};
const MAX_DNS_RESULT_LEN: usize = 1024 * 1024;
#[derive(Default)]
pub struct WasiHostDnsIo;
impl HostDnsIo for WasiHostDnsIo {
fn submit_resolve(&self, operation: HostOperationId, query: &DnsQuery) -> io::Result<()> {
submit_query("start_dns_resolve", operation, query, start_dns_resolve)
}
fn take_resolve(&self, operation: HostOperationId) -> Poll<io::Result<Vec<IpAddr>>> {
take_result(
"take_dns_resolve",
operation,
take_dns_resolve,
decode_addresses,
)
}
fn submit_txt(&self, operation: HostOperationId, query: &DnsQuery) -> io::Result<()> {
submit_query("start_dns_txt", operation, query, start_dns_txt)
}
fn take_txt(&self, operation: HostOperationId) -> Poll<io::Result<String>> {
take_result("take_dns_txt", operation, take_dns_txt, decode_txt)
}
fn submit_srv(&self, operation: HostOperationId, query: &DnsQuery) -> io::Result<()> {
submit_query("start_dns_srv", operation, query, start_dns_srv)
}
fn take_srv(&self, operation: HostOperationId) -> Poll<io::Result<Vec<DnsSrvRecord>>> {
take_result("take_dns_srv", operation, take_dns_srv, decode_srv)
}
fn cancel_operation(&self, operation: HostOperationId) -> io::Result<()> {
host_status("cancel_operation", unsafe { cancel_operation(operation.0) })
}
}
fn submit_query(
name: &'static str,
operation: HostOperationId,
query: &DnsQuery,
submit: unsafe extern "C" fn(u64, u32, u32) -> i32,
) -> io::Result<()> {
let encoded = encode_query(query)?;
let length = u32::try_from(encoded.len())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "DNS query is too long"))?;
host_status(name, unsafe {
submit(operation.0, encoded.as_ptr() as u32, length)
})
}
fn take_result<T>(
name: &'static str,
operation: HostOperationId,
take: unsafe extern "C" fn(u64, u32, u32) -> i32,
decode: fn(&[u8]) -> io::Result<T>,
) -> Poll<io::Result<T>> {
let required = unsafe { take(operation.0, 0, 0) };
if required == HOST_PENDING {
return Poll::Pending;
}
if required <= 0 {
return Poll::Ready(Err(host_error(name, required)));
}
let required = usize::try_from(required).expect("positive i32 fits usize");
if required > MAX_DNS_RESULT_LEN {
cancel_probed_result(operation);
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("host {name} result exceeds {MAX_DNS_RESULT_LEN} bytes"),
)));
}
let mut encoded = vec![0_u8; required];
let capacity = u32::try_from(required).expect("DNS result limit fits u32");
let copied = unsafe { take(operation.0, encoded.as_mut_ptr() as u32, capacity) };
if copied != i32::try_from(required).expect("positive DNS result length fits i32") {
cancel_probed_result(operation);
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("host {name} changed result length from {required} to {copied}"),
)));
}
Poll::Ready(decode(&encoded))
}
fn cancel_probed_result(operation: HostOperationId) {
// The size probe does not consume host state. A second take may have
// consumed it before returning a malformed status, so this best-effort
// ownership cleanup must not hide the original protocol error.
let _ = unsafe { cancel_operation(operation.0) };
}
fn host_status(name: &'static str, result: i32) -> io::Result<()> {
if result == 0 {
Ok(())
} else {
Err(host_error(name, result))
}
}
@@ -0,0 +1,76 @@
//! WASI imports for connector environment operations.
use std::{io, net::SocketAddr, task::Poll};
use crate::socket::SocketContext;
use crate::{
host::{environment::HostConnectorEnvironmentIo, socket::HostOperationId},
wasi::{
imports::{
HOST_PENDING, cancel_operation, start_local_addr_for_remote, take_local_addr_for_remote,
},
wire::{
common::{host_error, status},
options::encode_socket_context,
socket::{SOCKET_ADDRESS_LEN, decode_socket_address, encode_socket_address},
},
},
};
#[derive(Default)]
pub struct WasiHostConnectorEnvironmentIo;
impl HostConnectorEnvironmentIo for WasiHostConnectorEnvironmentIo {
fn submit_local_addr_for_remote(
&self,
operation: HostOperationId,
remote_addr: SocketAddr,
context: &SocketContext,
) -> io::Result<()> {
let encoded = encode_socket_address(remote_addr);
let encoded_context = encode_socket_context(context)?;
status("start_local_addr_for_remote", unsafe {
start_local_addr_for_remote(
operation.0,
encoded.as_ptr() as u32,
SOCKET_ADDRESS_LEN as u32,
encoded_context.as_ptr() as u32,
encoded_context.len() as u32,
)
})
}
fn take_local_addr_for_remote(
&self,
operation: HostOperationId,
) -> Poll<io::Result<SocketAddr>> {
take_address(
"take_local_addr_for_remote",
operation,
take_local_addr_for_remote,
)
}
fn cancel_operation(&self, operation: HostOperationId) -> io::Result<()> {
status("cancel_operation", unsafe { cancel_operation(operation.0) })
}
}
fn take_address(
name: &'static str,
operation: HostOperationId,
take: unsafe extern "C" fn(u64, u32, u32) -> i32,
) -> Poll<io::Result<SocketAddr>> {
let mut encoded = [0_u8; SOCKET_ADDRESS_LEN];
match unsafe {
take(
operation.0,
encoded.as_mut_ptr() as u32,
SOCKET_ADDRESS_LEN as u32,
)
} {
HOST_PENDING => Poll::Pending,
0 => Poll::Ready(decode_socket_address(&encoded)),
value => Poll::Ready(Err(host_error(name, value))),
}
}
+6
View File
@@ -0,0 +1,6 @@
//! Concrete implementations of portable Host capability seams for WASI.
pub mod dns;
pub mod environment;
pub mod packet;
pub mod socket;
+59
View File
@@ -0,0 +1,59 @@
use std::{io, task::Poll};
use crate::{
host::{
packet::{HostPacketIo, HostPacketSinkHandle},
socket::HostOperationId,
},
wasi::{
imports::{
HOST_PENDING, HOST_WOULD_BLOCK, cancel_operation, start_packet_write_ready,
take_packet_write_ready, try_packet_write,
},
wire::common::{host_error, status},
},
};
const MAX_HOST_PACKET_LEN: usize = 1024 * 1024;
#[derive(Default)]
pub struct WasiHostPacketIo;
impl HostPacketIo for WasiHostPacketIo {
fn try_write_packet(&self, handle: HostPacketSinkHandle, packet: &[u8]) -> io::Result<()> {
if packet.len() > MAX_HOST_PACKET_LEN {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("host packet exceeds {MAX_HOST_PACKET_LEN} bytes"),
));
}
let length = u32::try_from(packet.len()).expect("host packet limit fits u32");
match unsafe { try_packet_write(handle.0, packet.as_ptr() as u32, length) } {
0 => Ok(()),
HOST_WOULD_BLOCK => Err(io::ErrorKind::WouldBlock.into()),
value => Err(host_error("try_packet_write", value)),
}
}
fn submit_write_ready(
&self,
handle: HostPacketSinkHandle,
operation: HostOperationId,
) -> io::Result<()> {
status("start_packet_write_ready", unsafe {
start_packet_write_ready(handle.0, operation.0)
})
}
fn take_write_ready(&self, operation: HostOperationId) -> Poll<io::Result<()>> {
match unsafe { take_packet_write_ready(operation.0) } {
HOST_PENDING => Poll::Pending,
0 => Poll::Ready(Ok(())),
value => Poll::Ready(Err(host_error("take_packet_write_ready", value))),
}
}
fn cancel_operation(&self, operation: HostOperationId) -> io::Result<()> {
status("cancel_operation", unsafe { cancel_operation(operation.0) })
}
}
@@ -0,0 +1,266 @@
use std::{io, task::Poll};
use crate::host::socket::{
HostOperationId, HostSocketHandle, HostSocketIo, HostTcpIo,
factory::{HostSocketFactoryIo, HostTcpConnectResult, HostUdpBindResult},
listener::{HostTcpBindResult, HostTcpListenerIo},
udp::{HostUdpDatagram, HostUdpIo},
};
use crate::socket::{
tcp::{TcpConnectOptions, TcpListenOptions},
udp::{UdpBindOptions, UdpSocketSendMeta},
};
use crate::wasi::{
imports::{
HOST_PENDING, cancel_operation, close, start_tcp_accept, start_tcp_bind, start_tcp_connect,
start_udp_bind, take_tcp_accept, take_tcp_bind, take_tcp_connect, take_udp_bind,
},
wire::{
common::{host_error, status, tcp_connect_error},
options::{
BOUND_SOCKET_RESULT_LEN, TCP_SOCKET_RESULT_LEN, decode_tcp_bind_result,
decode_tcp_socket_result, decode_udp_bind_result, encode_tcp_connect_options,
encode_tcp_listen_options, encode_udp_bind_options,
},
},
};
use super::{WasiHostTcpIo, udp::WasiHostUdpIo};
#[derive(Default)]
pub struct WasiHostSocketBackend {
tcp: WasiHostTcpIo,
udp: WasiHostUdpIo,
}
impl WasiHostSocketBackend {
fn decode_transferred<T>(&self, encoded: &[u8], decoded: io::Result<T>) -> io::Result<T> {
match decoded {
Ok(result) => Ok(result),
Err(decode_error) => {
let handle = HostSocketHandle(u64::from_be_bytes(encoded[..8].try_into().unwrap()));
match self.close(handle) {
Ok(()) => Err(decode_error),
Err(close_error) => Err(io::Error::new(
decode_error.kind(),
format!(
"{decode_error}; additionally failed to close malformed host result: {close_error}"
),
)),
}
}
}
}
}
impl HostSocketIo for WasiHostSocketBackend {
fn cancel_operation(&self, operation: HostOperationId) -> io::Result<()> {
self.tcp.forget_operation(operation);
self.udp.forget_operation(operation);
status("cancel_operation", unsafe { cancel_operation(operation.0) })
}
fn close(&self, handle: HostSocketHandle) -> io::Result<()> {
status("close", unsafe { close(handle.0) })
}
}
impl HostTcpIo for WasiHostSocketBackend {
fn submit_read(
&self,
handle: HostSocketHandle,
operation: HostOperationId,
capacity: usize,
) -> io::Result<()> {
self.tcp.submit_read(handle, operation, capacity)
}
fn take_read(&self, operation: HostOperationId) -> Poll<io::Result<Vec<u8>>> {
self.tcp.take_read(operation)
}
fn submit_write(
&self,
handle: HostSocketHandle,
operation: HostOperationId,
source: &[u8],
) -> io::Result<()> {
self.tcp.submit_write(handle, operation, source)
}
fn take_write(&self, operation: HostOperationId) -> Poll<io::Result<()>> {
self.tcp.take_write(operation)
}
}
impl HostUdpIo for WasiHostSocketBackend {
fn submit_recv(
&self,
handle: HostSocketHandle,
operation: HostOperationId,
capacity: usize,
) -> io::Result<()> {
self.udp.submit_recv(handle, operation, capacity)
}
fn take_recv(&self, operation: HostOperationId) -> Poll<io::Result<HostUdpDatagram>> {
self.udp.take_recv(operation)
}
fn try_send(
&self,
handle: HostSocketHandle,
source: &[u8],
peer_addr: std::net::SocketAddr,
meta: UdpSocketSendMeta,
) -> io::Result<()> {
self.udp.try_send(handle, source, peer_addr, meta)
}
fn submit_send_ready(
&self,
handle: HostSocketHandle,
operation: HostOperationId,
) -> io::Result<()> {
self.udp.submit_send_ready(handle, operation)
}
fn take_send_ready(&self, operation: HostOperationId) -> Poll<io::Result<()>> {
self.udp.take_send_ready(operation)
}
}
impl HostSocketFactoryIo for WasiHostSocketBackend {
fn submit_tcp_connect(
&self,
operation: HostOperationId,
options: &TcpConnectOptions,
) -> io::Result<()> {
let encoded = encode_tcp_connect_options(options)?;
status("start_tcp_connect", unsafe {
start_tcp_connect(
operation.0,
encoded.as_ptr() as u32,
encoded_len("TCP connect options", &encoded)?,
)
})
}
fn take_tcp_connect(
&self,
operation: HostOperationId,
) -> Poll<io::Result<HostTcpConnectResult>> {
let mut encoded = [0_u8; TCP_SOCKET_RESULT_LEN];
match unsafe {
take_tcp_connect(
operation.0,
encoded.as_mut_ptr() as u32,
TCP_SOCKET_RESULT_LEN as u32,
)
} {
HOST_PENDING => Poll::Pending,
0 => Poll::Ready(self.decode_transferred(&encoded, decode_tcp_socket_result(&encoded))),
value => Poll::Ready(Err(tcp_connect_error(value))),
}
}
fn submit_udp_bind(
&self,
operation: HostOperationId,
options: &UdpBindOptions,
) -> io::Result<()> {
let encoded = encode_udp_bind_options(options)?;
status("start_udp_bind", unsafe {
start_udp_bind(
operation.0,
encoded.as_ptr() as u32,
encoded_len("UDP bind options", &encoded)?,
)
})
}
fn take_udp_bind(&self, operation: HostOperationId) -> Poll<io::Result<HostUdpBindResult>> {
let mut encoded = [0_u8; BOUND_SOCKET_RESULT_LEN];
match unsafe {
take_udp_bind(
operation.0,
encoded.as_mut_ptr() as u32,
BOUND_SOCKET_RESULT_LEN as u32,
)
} {
HOST_PENDING => Poll::Pending,
0 => Poll::Ready(self.decode_transferred(&encoded, decode_udp_bind_result(&encoded))),
value => Poll::Ready(Err(host_error("take_udp_bind", value))),
}
}
}
impl HostTcpListenerIo for WasiHostSocketBackend {
fn submit_tcp_bind(
&self,
operation: HostOperationId,
options: &TcpListenOptions,
) -> io::Result<()> {
let encoded = encode_tcp_listen_options(options)?;
status("start_tcp_bind", unsafe {
start_tcp_bind(
operation.0,
encoded.as_ptr() as u32,
encoded_len("TCP listen options", &encoded)?,
)
})
}
fn take_tcp_bind(&self, operation: HostOperationId) -> Poll<io::Result<HostTcpBindResult>> {
let mut encoded = [0_u8; BOUND_SOCKET_RESULT_LEN];
match unsafe {
take_tcp_bind(
operation.0,
encoded.as_mut_ptr() as u32,
BOUND_SOCKET_RESULT_LEN as u32,
)
} {
HOST_PENDING => Poll::Pending,
0 => Poll::Ready(self.decode_transferred(&encoded, decode_tcp_bind_result(&encoded))),
value => Poll::Ready(Err(host_error("take_tcp_bind", value))),
}
}
fn submit_tcp_accept(
&self,
handle: HostSocketHandle,
operation: HostOperationId,
) -> io::Result<()> {
status("start_tcp_accept", unsafe {
start_tcp_accept(handle.0, operation.0)
})
}
fn take_tcp_accept(
&self,
operation: HostOperationId,
) -> Poll<io::Result<HostTcpConnectResult>> {
let mut encoded = [0_u8; TCP_SOCKET_RESULT_LEN];
match unsafe {
take_tcp_accept(
operation.0,
encoded.as_mut_ptr() as u32,
TCP_SOCKET_RESULT_LEN as u32,
)
} {
HOST_PENDING => Poll::Pending,
0 => Poll::Ready(self.decode_transferred(&encoded, decode_tcp_socket_result(&encoded))),
value => Poll::Ready(Err(host_error("take_tcp_accept", value))),
}
}
}
fn encoded_len(description: &str, encoded: &[u8]) -> io::Result<u32> {
u32::try_from(encoded.len()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("{description} exceed WASI guest memory"),
)
})
}
@@ -0,0 +1,124 @@
//! WASI TCP adapter behind the portable host socket seams.
//!
//! [`WasiHostTcpIo`] implements the TCP half of the host socket I/O traits on
//! top of the `easytier_host` import module, and
//! [`backend::WasiHostSocketBackend`] composes the TCP, UDP, factory, and
//! listener imports into one host socket backend. Shared status mapping and
//! wire codecs live in [`crate::wasi::wire`].
pub mod backend;
pub mod udp;
use std::{collections::HashMap, io, sync::Mutex, task::Poll};
use crate::{
host::socket::{HostOperationId, HostSocketHandle, HostSocketIo, HostTcpIo},
wasi::{
imports::{
HOST_PENDING, cancel_operation, close, start_read, start_write, take_read, take_write,
},
wire::common::{host_error, status},
},
};
#[derive(Debug, Default)]
pub struct WasiHostTcpIo {
read_buffers: Mutex<HashMap<HostOperationId, Vec<u8>>>,
}
impl WasiHostTcpIo {
pub(super) fn forget_operation(&self, operation: HostOperationId) {
self.read_buffers
.lock()
.expect("WASI read buffer registry poisoned")
.remove(&operation);
}
}
impl HostSocketIo for WasiHostTcpIo {
fn cancel_operation(&self, operation: HostOperationId) -> io::Result<()> {
self.forget_operation(operation);
status("cancel_operation", unsafe { cancel_operation(operation.0) })
}
fn close(&self, handle: HostSocketHandle) -> io::Result<()> {
status("close", unsafe { close(handle.0) })
}
}
impl HostTcpIo for WasiHostTcpIo {
fn submit_read(
&self,
handle: HostSocketHandle,
operation: HostOperationId,
capacity: usize,
) -> io::Result<()> {
let capacity_u32 = u32::try_from(capacity)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "read buffer is too large"))?;
status("start_read", unsafe {
start_read(handle.0, operation.0, capacity_u32)
})?;
self.read_buffers
.lock()
.expect("WASI read buffer registry poisoned")
.insert(operation, vec![0; capacity]);
Ok(())
}
fn take_read(&self, operation: HostOperationId) -> Poll<io::Result<Vec<u8>>> {
let mut buffers = self
.read_buffers
.lock()
.expect("WASI read buffer registry poisoned");
let Some(buffer) = buffers.get_mut(&operation) else {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::NotFound,
"WASI read operation buffer is missing",
)));
};
let result =
unsafe { take_read(operation.0, buffer.as_mut_ptr() as u32, buffer.len() as u32) };
match result {
HOST_PENDING => Poll::Pending,
value if value >= 0 => {
let length = value as usize;
if length > buffer.len() {
buffers.remove(&operation);
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
"host read completion exceeds the submitted capacity",
)));
}
let mut buffer = buffers.remove(&operation).unwrap();
buffer.truncate(length);
Poll::Ready(Ok(buffer))
}
value => {
buffers.remove(&operation);
Poll::Ready(Err(host_error("take_read", value)))
}
}
}
fn submit_write(
&self,
handle: HostSocketHandle,
operation: HostOperationId,
source: &[u8],
) -> io::Result<()> {
let length = u32::try_from(source.len()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "write buffer is too large")
})?;
status("start_write", unsafe {
start_write(handle.0, operation.0, source.as_ptr() as u32, length)
})
}
fn take_write(&self, operation: HostOperationId) -> Poll<io::Result<()>> {
match unsafe { take_write(operation.0) } {
HOST_PENDING => Poll::Pending,
0 => Poll::Ready(Ok(())),
value => Poll::Ready(Err(host_error("take_write", value))),
}
}
}
@@ -0,0 +1,179 @@
use std::{collections::HashMap, io, sync::Mutex, task::Poll};
use crate::socket::udp::{UdpSocketRecvMeta, UdpSocketSendMeta};
use crate::{
host::{
socket::udp::{HostUdpDatagram, HostUdpIo},
socket::{HostOperationId, HostSocketHandle, HostSocketIo},
},
wasi::{
imports::{
HOST_PENDING, HOST_WOULD_BLOCK, cancel_operation, close, start_udp_recv,
start_udp_send_ready, take_udp_recv, take_udp_send_ready, try_udp_send,
},
wire::{
common::{host_error, status},
socket::{UDP_METADATA_LEN, decode_udp_metadata, encode_udp_metadata},
},
},
};
struct WasiUdpRecvBuffer {
data: Vec<u8>,
metadata: [u8; UDP_METADATA_LEN],
}
#[derive(Default)]
pub struct WasiHostUdpIo {
recv_buffers: Mutex<HashMap<HostOperationId, WasiUdpRecvBuffer>>,
}
impl WasiHostUdpIo {
pub(super) fn forget_operation(&self, operation: HostOperationId) {
self.recv_buffers
.lock()
.expect("WASI UDP receive buffer registry poisoned")
.remove(&operation);
}
}
impl HostSocketIo for WasiHostUdpIo {
fn cancel_operation(&self, operation: HostOperationId) -> io::Result<()> {
self.forget_operation(operation);
status("cancel_operation", unsafe { cancel_operation(operation.0) })
}
fn close(&self, handle: HostSocketHandle) -> io::Result<()> {
status("close", unsafe { close(handle.0) })
}
}
impl HostUdpIo for WasiHostUdpIo {
fn submit_recv(
&self,
handle: HostSocketHandle,
operation: HostOperationId,
capacity: usize,
) -> io::Result<()> {
let capacity_u32 = u32::try_from(capacity).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"UDP receive buffer is too large",
)
})?;
status("start_udp_recv", unsafe {
start_udp_recv(handle.0, operation.0, capacity_u32)
})?;
self.recv_buffers
.lock()
.expect("WASI UDP receive buffer registry poisoned")
.insert(
operation,
WasiUdpRecvBuffer {
data: vec![0; capacity],
metadata: [0; UDP_METADATA_LEN],
},
);
Ok(())
}
fn take_recv(&self, operation: HostOperationId) -> Poll<io::Result<HostUdpDatagram>> {
let mut buffers = self
.recv_buffers
.lock()
.expect("WASI UDP receive buffer registry poisoned");
let Some(buffer) = buffers.get_mut(&operation) else {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::NotFound,
"WASI UDP receive operation buffer is missing",
)));
};
let result = unsafe {
take_udp_recv(
operation.0,
buffer.data.as_mut_ptr() as u32,
buffer.data.len() as u32,
buffer.metadata.as_mut_ptr() as u32,
UDP_METADATA_LEN as u32,
)
};
match result {
HOST_PENDING => Poll::Pending,
value if value >= 0 => {
let length = value as usize;
let mut buffer = buffers.remove(&operation).unwrap();
if length > buffer.data.len() {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
"host UDP receive completion exceeds the submitted capacity",
)));
}
buffer.data.truncate(length);
let (peer_addr, dst_ip, _) = match decode_udp_metadata(&buffer.metadata) {
Ok(metadata) => metadata,
Err(error) => return Poll::Ready(Err(error)),
};
Poll::Ready(Ok(HostUdpDatagram {
data: buffer.data,
peer_addr,
meta: UdpSocketRecvMeta { dst_ip },
}))
}
value => {
buffers.remove(&operation);
Poll::Ready(Err(host_error("take_udp_recv", value)))
}
}
}
fn try_send(
&self,
handle: HostSocketHandle,
source: &[u8],
peer_addr: std::net::SocketAddr,
meta: UdpSocketSendMeta,
) -> io::Result<()> {
if meta.src_ifindex.is_some() && !matches!(meta.src_ip, Some(std::net::IpAddr::V6(_))) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"UDP source interface index requires an IPv6 source address",
));
}
let length = u32::try_from(source.len()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "UDP send buffer is too large")
})?;
let metadata = encode_udp_metadata(peer_addr, meta.src_ip, meta.src_ifindex);
match unsafe {
try_udp_send(
handle.0,
source.as_ptr() as u32,
length,
metadata.as_ptr() as u32,
UDP_METADATA_LEN as u32,
)
} {
0 => Ok(()),
HOST_WOULD_BLOCK => Err(io::ErrorKind::WouldBlock.into()),
value => Err(host_error("try_udp_send", value)),
}
}
fn submit_send_ready(
&self,
handle: HostSocketHandle,
operation: HostOperationId,
) -> io::Result<()> {
status("start_udp_send_ready", unsafe {
start_udp_send_ready(handle.0, operation.0)
})
}
fn take_send_ready(&self, operation: HostOperationId) -> Poll<io::Result<()>> {
match unsafe { take_udp_send_ready(operation.0) } {
HOST_PENDING => Poll::Pending,
0 => Poll::Ready(Ok(())),
value => Poll::Ready(Err(host_error("take_udp_send_ready", value))),
}
}
}
+139
View File
@@ -0,0 +1,139 @@
//! Raw `easytier_host` imports, compiled only for the WASI guest.
//!
//! [`crate::wasi::abi`] declares the shared contract metadata; each import
//! below documents its own ownership and completion rules. Concrete adapters
//! call these functions directly.
pub(crate) const HOST_PENDING: i32 = -1;
pub(crate) const HOST_WOULD_BLOCK: i32 = -5;
#[link(wasm_import_module = "easytier_host")]
unsafe extern "C" {
/// Starts one TCP read into a host-owned pending operation.
///
/// The host records at most `capacity` bytes for `operation` and must not
/// write guest memory until [`take_read`] supplies a destination buffer.
pub(crate) fn start_read(handle: u64, operation: u64, capacity: u32) -> i32;
/// Copies a completed TCP read into `destination`, returning its byte count.
///
/// Returns [`HOST_PENDING`] while the operation is incomplete. A completed
/// read, including EOF with a zero length, consumes `operation`.
pub(crate) fn take_read(operation: u64, destination: u32, capacity: u32) -> i32;
/// Starts one TCP write after copying `source[..length]` from guest memory.
pub(crate) fn start_write(handle: u64, operation: u64, source: u32, length: u32) -> i32;
/// Reports completion of a TCP write and consumes `operation` on success or error.
pub(crate) fn take_write(operation: u64) -> i32;
/// Starts receipt of one UDP datagram of at most `capacity` bytes.
pub(crate) fn start_udp_recv(handle: u64, operation: u64, capacity: u32) -> i32;
/// Copies one completed UDP datagram and its metadata into guest memory.
///
/// A non-pending result consumes `operation`; `metadata` has exactly
/// `metadata_len` bytes allocated by core for the socket wire format.
pub(crate) fn take_udp_recv(
operation: u64,
destination: u32,
capacity: u32,
metadata: u32,
metadata_len: u32,
) -> i32;
/// Attempts to enqueue one complete UDP datagram after copying its bytes and metadata.
///
/// [`HOST_WOULD_BLOCK`] means the datagram was not accepted and has no
/// side effects. Any other success means the host owns a complete copy.
pub(crate) fn try_udp_send(
handle: u64,
source: u32,
length: u32,
metadata: u32,
metadata_len: u32,
) -> i32;
/// Starts waiting until another UDP send attempt may succeed.
pub(crate) fn start_udp_send_ready(handle: u64, operation: u64) -> i32;
/// Reports UDP write readiness; readiness never sends a datagram itself.
pub(crate) fn take_udp_send_ready(operation: u64) -> i32;
/// Starts a TCP connection using an encoded `TcpConnectOptions` document.
pub(crate) fn start_tcp_connect(operation: u64, options: u32, options_len: u32) -> i32;
/// Copies the completed TCP connection handle and addresses into `result`.
pub(crate) fn take_tcp_connect(operation: u64, result: u32, result_len: u32) -> i32;
/// Starts a UDP bind using an encoded `UdpBindOptions` document.
pub(crate) fn start_udp_bind(operation: u64, options: u32, options_len: u32) -> i32;
/// Copies the completed UDP socket handle and local address into `result`.
pub(crate) fn take_udp_bind(operation: u64, result: u32, result_len: u32) -> i32;
/// Starts a TCP listener bind using an encoded `TcpListenOptions` document.
pub(crate) fn start_tcp_bind(operation: u64, options: u32, options_len: u32) -> i32;
/// Copies the completed listener handle and local address into `result`.
pub(crate) fn take_tcp_bind(operation: u64, result: u32, result_len: u32) -> i32;
/// Starts accepting one TCP stream from a listener handle.
pub(crate) fn start_tcp_accept(handle: u64, operation: u64) -> i32;
/// Copies the accepted TCP stream handle and addresses into `result`.
pub(crate) fn take_tcp_accept(operation: u64, result: u32, result_len: u32) -> i32;
/// Starts an address-record DNS lookup for an encoded [`crate::host::dns::DnsQuery`].
pub(crate) fn start_dns_resolve(operation: u64, query: u32, query_len: u32) -> i32;
/// Probes or copies the encoded DNS address result for `operation`.
///
/// A zero-capacity call probes the required result length without consuming
/// it; a subsequent call with enough capacity copies and consumes it.
pub(crate) fn take_dns_resolve(operation: u64, result: u32, result_capacity: u32) -> i32;
/// Starts a TXT-record DNS lookup for an encoded query.
pub(crate) fn start_dns_txt(operation: u64, query: u32, query_len: u32) -> i32;
/// Probes or copies the encoded DNS TXT result using the DNS result protocol.
pub(crate) fn take_dns_txt(operation: u64, result: u32, result_capacity: u32) -> i32;
/// Starts an SRV-record DNS lookup for an encoded query.
pub(crate) fn start_dns_srv(operation: u64, query: u32, query_len: u32) -> i32;
/// Probes or copies the encoded DNS SRV result using the DNS result protocol.
pub(crate) fn take_dns_srv(operation: u64, result: u32, result_capacity: u32) -> i32;
/// Starts finding the local address and source context needed to reach `remote_addr`.
pub(crate) fn start_local_addr_for_remote(
operation: u64,
remote_addr: u32,
remote_addr_len: u32,
context: u32,
context_len: u32,
) -> i32;
/// Copies the resolved local socket address into the fixed-size `result` buffer.
pub(crate) fn take_local_addr_for_remote(operation: u64, result: u32, result_len: u32) -> i32;
/// Attempts to deliver one raw IP packet to a host packet sink.
///
/// On success the host owns a complete copy. [`HOST_WOULD_BLOCK`] leaves
/// the packet unaccepted and requires core to wait for write readiness.
pub(crate) fn try_packet_write(handle: u64, packet: u32, packet_len: u32) -> i32;
/// Starts waiting until another packet-sink admission attempt may succeed.
pub(crate) fn start_packet_write_ready(handle: u64, operation: u64) -> i32;
/// Reports packet-sink write readiness; it never accepts a packet itself.
pub(crate) fn take_packet_write_ready(operation: u64) -> i32;
/// Cancels a pending or completed-but-unread operation and releases host state.
///
/// Cancellation must be idempotent when the operation is already absent.
pub(crate) fn cancel_operation(operation: u64) -> i32;
/// Closes a host socket or listener handle. Closing an already-closed handle is valid.
pub(crate) fn close(handle: u64) -> i32;
}
+24
View File
@@ -0,0 +1,24 @@
//! WASI runtime integration for EasyTier core.
//!
//! Portable Host capability seams live in [`crate::host`]. This module owns
//! the concrete WASI adapters, the `easytier_host` import contract, and the
//! guest lifecycle ABI used by an externally driven WASI runtime.
pub mod abi;
/// Concrete adapters a WASI guest uses to connect portable Host seams to the
/// [`abi::HOST_IMPORT_MODULE`] runtime contract.
#[cfg(target_os = "wasi")]
pub mod adapter;
#[cfg(target_os = "wasi")]
pub(crate) mod imports;
#[cfg(target_os = "wasi")]
pub(crate) mod runtime;
#[cfg(any(test, target_os = "wasi"))]
pub(crate) mod runtime_driver;
#[cfg(any(test, target_os = "wasi"))]
pub(crate) mod schema;
#[cfg(any(test, target_os = "wasi"))]
pub(crate) mod time;
#[cfg(any(test, target_os = "wasi"))]
pub(crate) mod wire;
+778
View File
@@ -0,0 +1,778 @@
//! Runtime implementation and lifecycle exports for a WASI core instance.
use crate::{
config::toml::TomlConfig, connectivity::connector_host::HostConnectorEnvironmentSnapshot,
};
pub(super) type WasiCore = crate::instance::CoreInstance<
crate::connectivity::connector_host::ConnectorHost<
crate::wasi::adapter::socket::backend::WasiHostSocketBackend,
crate::wasi::adapter::environment::WasiHostConnectorEnvironmentIo,
>,
>;
pub(super) struct WasiCoreRuntime {
socket_runtime: crate::host::socket::HostSocketRuntime,
core: std::sync::Arc<WasiCore>,
}
impl WasiCoreRuntime {
pub(super) fn core(&self) -> &std::sync::Arc<WasiCore> {
&self.core
}
pub(super) fn notify_host_completions(&self) {
self.socket_runtime.notify_completions();
}
}
pub(super) fn new_wasi_core_runtime(
config: TomlConfig,
process_runtime: std::sync::Arc<crate::process_runtime::CoreProcessRuntime>,
environment_snapshot: HostConnectorEnvironmentSnapshot,
packet_sink: crate::host::packet::HostPacketSinkHandle,
) -> anyhow::Result<WasiCoreRuntime> {
use std::sync::Arc;
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,
},
};
let socket_runtime = HostSocketRuntime::new();
let host = Arc::new(new_connector_host(
socket_runtime.clone(),
Arc::new(WasiHostSocketBackend::default()),
environment_snapshot,
Arc::new(WasiHostConnectorEnvironmentIo),
));
let dns = Arc::new(HostDnsResolver::new(
socket_runtime.clone(),
Arc::new(WasiHostDnsIo),
));
let packet_sink = Arc::new(HostPacketSink::new(
socket_runtime.clone(),
Arc::new(WasiHostPacketIo),
packet_sink,
));
let adapters = CoreHostAdapters::new(host, dns, packet_sink, process_runtime);
let core = CoreInstance::from_toml(config, adapters)?;
Ok(WasiCoreRuntime {
socket_runtime,
core,
})
}
mod abi {
use std::{
cell::RefCell,
collections::BTreeMap,
sync::{Arc, Mutex},
};
use tokio::{runtime::Builder, task::JoinHandle};
use crate::{
config::toml::{ConfigLoader as _, TomlConfig},
foundation::time::{clear_domain, enter_domain, next_deadline_millis},
host::packet::HostPacketSinkHandle,
instance::{
CoreInstanceState,
manager::{InstanceFactory, ManagedInstance},
},
process_runtime::{CoreProcessRuntime, ProtectedTcpPortLease},
wasi::runtime_driver::{RuntimeDriveOutcome, RuntimeDriver},
};
use super::{WasiCoreRuntime, new_wasi_core_runtime};
use crate::wasi::schema::WasiCoreInstanceCreateConfig;
#[cfg(feature = "proxy-smoltcp-stack")]
mod data_plane;
const MAX_CREATE_CONFIG_LEN: usize = 16 * 1024 * 1024;
const MAX_GUEST_BUFFER_LEN: usize = MAX_CREATE_CONFIG_LEN;
const INVALID_HANDLE: i32 = -1;
const INVALID_STATE: i32 = -2;
const INVALID_INPUT: i32 = -3;
const ASYNC_ERROR: i32 = -4;
const BUSY: i32 = -5;
struct WasiAbiState {
next_handle: u64,
handles: BTreeMap<u64, WasiHandleState>,
buffers: BTreeMap<u32, Box<[u8]>>,
active_instance: bool,
global_error: String,
}
struct WasiHandleState {
instance_id: uuid::Uuid,
error: String,
}
impl Default for WasiAbiState {
fn default() -> Self {
Self {
next_handle: 0,
handles: BTreeMap::new(),
buffers: BTreeMap::new(),
active_instance: false,
global_error: String::new(),
}
}
}
struct WasiContext {
factory: WasiInstanceFactory,
instances: RefCell<BTreeMap<uuid::Uuid, Arc<WasiInstance>>>,
abi: RefCell<WasiAbiState>,
}
impl Default for WasiContext {
fn default() -> Self {
let factory = WasiInstanceFactory {
process_runtime: CoreProcessRuntime::new(),
};
Self {
factory,
instances: RefCell::new(BTreeMap::new()),
abi: RefCell::new(WasiAbiState::default()),
}
}
}
thread_local! {
static CONTEXT: WasiContext = WasiContext::default();
}
struct WasiInstanceFactory {
process_runtime: std::sync::Arc<CoreProcessRuntime>,
}
struct WasiCreateContext {
domain: u64,
environment: crate::connectivity::connector_host::HostConnectorEnvironmentSnapshot,
packet_sink: HostPacketSinkHandle,
}
struct WasiInstance {
instance_id: uuid::Uuid,
domain: u64,
core: WasiCoreRuntime,
execution: Mutex<WasiExecution>,
_protected_tcp_port_leases: Vec<ProtectedTcpPortLease>,
}
struct WasiExecution {
runtime: tokio::runtime::Runtime,
runtime_driver: RuntimeDriver,
drive_again: bool,
start_task: Option<JoinHandle<anyhow::Result<()>>>,
stop_task: Option<JoinHandle<()>>,
}
impl ManagedInstance for WasiInstance {
fn instance_id(&self) -> uuid::Uuid {
self.instance_id
}
}
impl InstanceFactory for WasiInstanceFactory {
type Instance = WasiInstance;
type CreateContext = WasiCreateContext;
type Error = anyhow::Error;
fn create(
&self,
config: TomlConfig,
context: Self::CreateContext,
) -> Result<Arc<Self::Instance>, Self::Error> {
let instance_id = config.get_id();
let protected_tcp_ports = context
.environment
.protected_tcp_ports
.iter()
.copied()
.map(|port| self.process_runtime.protect_tcp_port(port))
.collect();
let runtime_driver = RuntimeDriver::default();
let park_driver = runtime_driver.clone();
let runtime = Builder::new_current_thread()
.enable_time()
.on_thread_park(move || park_driver.on_thread_park())
.build()?;
let core = {
let _domain = enter_domain(context.domain);
let _runtime = runtime.enter();
new_wasi_core_runtime(
config,
self.process_runtime.clone(),
context.environment,
context.packet_sink,
)?
};
Ok(Arc::new(WasiInstance {
instance_id,
domain: context.domain,
core,
execution: Mutex::new(WasiExecution {
runtime,
runtime_driver,
drive_again: false,
start_task: None,
stop_task: None,
}),
_protected_tcp_port_leases: protected_tcp_ports,
}))
}
}
impl WasiAbiState {
fn allocate_handle(&mut self) -> Result<u64, i32> {
if self.active_instance {
self.set_global_error("core instance lifecycle call is not reentrant");
return Err(BUSY);
}
loop {
self.next_handle = self.next_handle.wrapping_add(1);
if self.next_handle != 0 && !self.handles.contains_key(&self.next_handle) {
return Ok(self.next_handle);
}
}
}
fn set_global_error(&mut self, error: impl ToString) {
self.global_error = error.to_string();
}
fn set_handle_error(&mut self, handle: u64, error: impl ToString) {
if let Some(state) = self.handles.get_mut(&handle) {
state.error = error.to_string();
} else {
self.set_global_error(error);
}
}
fn error_for_handle(&self, handle: u64) -> &str {
self.handles
.get(&handle)
.map(|state| state.error.as_str())
.unwrap_or(self.global_error.as_str())
}
fn begin_instance_call(&mut self, handle: u64) -> Result<uuid::Uuid, i32> {
if self.active_instance {
self.set_handle_error(handle, "core instance lifecycle call is not reentrant");
return Err(BUSY);
}
let Some(instance_id) = self.handles.get(&handle).map(|state| state.instance_id) else {
self.set_global_error(format!("unknown core instance handle: {handle}"));
return Err(INVALID_HANDLE);
};
self.active_instance = true;
Ok(instance_id)
}
fn begin_instance_drop(&mut self, handle: u64) -> Result<uuid::Uuid, i32> {
self.begin_instance_call(handle)
}
fn finish_instance_call(&mut self) {
debug_assert!(self.active_instance);
self.active_instance = false;
}
fn finish_instance_drop(&mut self, handle: u64) {
debug_assert!(self.active_instance);
self.handles.remove(&handle);
self.active_instance = false;
}
fn read_buffer(&self, pointer: u32, length: usize) -> anyhow::Result<Vec<u8>> {
let buffer = self
.buffers
.get(&pointer)
.ok_or_else(|| anyhow::anyhow!("unknown guest buffer: {pointer}"))?;
if length > buffer.len() {
anyhow::bail!(
"guest buffer length {length} exceeds allocation {}",
buffer.len()
);
}
Ok(buffer[..length].to_vec())
}
}
impl WasiInstance {
fn start(&self) -> anyhow::Result<()> {
let mut execution = self.execution.lock().unwrap();
if execution.start_task.is_some()
|| execution.stop_task.is_some()
|| self.core.core().state() != CoreInstanceState::Created
{
anyhow::bail!("core instance cannot schedule start from its current state");
}
let instance = self.core.core().clone();
execution.start_task = Some(
execution
.runtime
.spawn(async move { instance.start().await }),
);
Ok(())
}
fn stop(&self) {
let mut execution = self.execution.lock().unwrap();
if execution.stop_task.is_some()
|| self.core.core().state() == CoreInstanceState::Stopped
{
return;
}
let instance = self.core.core().clone();
execution.stop_task = Some(execution.runtime.spawn(async move {
instance.stop().await;
}));
}
fn drive(&self) -> anyhow::Result<()> {
let _domain = enter_domain(self.domain);
let mut execution = self.execution.lock().unwrap();
execution.drive_again = execution.runtime_driver.drive(&execution.runtime)
== RuntimeDriveOutcome::BudgetExhausted;
if execution
.start_task
.as_ref()
.is_some_and(JoinHandle::is_finished)
{
let task = execution.start_task.take().unwrap();
match execution.runtime.block_on(task) {
Ok(Ok(())) => {}
Ok(Err(error)) => return Err(error),
Err(error) => anyhow::bail!("core instance start task failed: {error}"),
}
}
if execution
.stop_task
.as_ref()
.is_some_and(JoinHandle::is_finished)
{
let task = execution.stop_task.take().unwrap();
if let Err(error) = execution.runtime.block_on(task) {
anyhow::bail!("core instance stop task failed: {error}");
}
}
Ok(())
}
fn state_code(&self) -> i32 {
let execution = self.execution.lock().unwrap();
if execution.stop_task.is_some() {
return 3;
}
if execution.start_task.is_some() {
return 1;
}
match self.core.core().state() {
CoreInstanceState::Created => 0,
CoreInstanceState::Starting => 1,
CoreInstanceState::Running => 2,
CoreInstanceState::Stopping => 3,
CoreInstanceState::Stopped => 4,
}
}
fn next_wait_millis(&self) -> Option<u64> {
let execution = self.execution.lock().unwrap();
if execution.drive_again {
Some(0)
} else {
next_deadline_millis(self.domain)
}
}
fn send_packet(&self, packet: Vec<u8>) {
let packet_plane = self.core.core().packet_plane();
self.execution.lock().unwrap().runtime.spawn(async move {
if let Err(error) = packet_plane.send_ip_packet(packet).await {
tracing::warn!(?error, "host packet ingress failed");
}
});
}
}
fn decode_create_config(encoded: &[u8]) -> anyhow::Result<WasiCoreInstanceCreateConfig> {
if encoded.is_empty() || encoded.len() > MAX_CREATE_CONFIG_LEN {
anyhow::bail!("invalid host core instance config buffer");
}
let config: WasiCoreInstanceCreateConfig = serde_json::from_slice(encoded)?;
config.validate()?;
Ok(config)
}
fn with_instance(
handle: u64,
operation: impl FnOnce(&WasiInstance) -> anyhow::Result<i32>,
) -> i32 {
let instance_id =
match CONTEXT.with(|context| context.abi.borrow_mut().begin_instance_call(handle)) {
Ok(instance_id) => instance_id,
Err(status) => return status,
};
let instance = manager_get(instance_id);
let status = match instance {
Some(instance) => match operation(&instance) {
Ok(status) => status,
Err(error) => {
CONTEXT.with(|context| {
context.abi.borrow_mut().set_handle_error(handle, error);
});
ASYNC_ERROR
}
},
None => {
CONTEXT.with(|context| {
context.abi.borrow_mut().set_handle_error(
handle,
format!("core instance {instance_id} is not registered"),
);
});
INVALID_STATE
}
};
CONTEXT.with(|context| context.abi.borrow_mut().finish_instance_call());
status
}
fn with_abi_state<T>(operation: impl FnOnce(&WasiAbiState) -> T) -> T {
CONTEXT.with(|context| operation(&context.abi.borrow()))
}
fn with_abi_state_mut<T>(operation: impl FnOnce(&mut WasiAbiState) -> T) -> T {
CONTEXT.with(|context| operation(&mut context.abi.borrow_mut()))
}
fn set_abi_error(error: impl ToString) {
with_abi_state_mut(|state| state.set_global_error(error));
}
fn set_instance_error(handle: u64, error: impl ToString) {
with_abi_state_mut(|state| state.set_handle_error(handle, error));
}
fn manager_get(instance_id: uuid::Uuid) -> Option<std::sync::Arc<WasiInstance>> {
CONTEXT.with(|context| context.instances.borrow().get(&instance_id).cloned())
}
fn manager_remove(instance_id: uuid::Uuid) -> Option<std::sync::Arc<WasiInstance>> {
CONTEXT.with(|context| context.instances.borrow_mut().remove(&instance_id))
}
fn manager_create(
config: TomlConfig,
context: WasiCreateContext,
) -> Result<
std::sync::Arc<WasiInstance>,
crate::instance::manager::InstanceCreateError<anyhow::Error>,
> {
CONTEXT.with(|wasi| {
let instance = wasi
.factory
.create(config, context)
.map_err(crate::instance::manager::InstanceCreateError::Factory)?;
let instance_id = instance.instance_id();
let mut instances = wasi.instances.borrow_mut();
match instances.entry(instance_id) {
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(instance.clone());
Ok(instance)
}
std::collections::btree_map::Entry::Occupied(_) => Err(
crate::instance::manager::InstanceCreateError::AlreadyExists { instance_id },
),
}
})
}
fn read_guest_buffer(pointer: u32, length: u32, maximum: usize) -> anyhow::Result<Vec<u8>> {
let length = usize::try_from(length).expect("u32 fits usize on wasm32");
if pointer == 0 || length == 0 || length > maximum {
anyhow::bail!("invalid guest buffer reference");
}
with_abi_state(|state| state.read_buffer(pointer, length))
}
#[unsafe(no_mangle)]
/// Allocates a guest-owned ABI buffer and returns its linear-memory offset.
///
/// The runtime may write at most `length` bytes through wasm memory, then
/// pass the pointer to another lifecycle export and eventually free it.
pub extern "C" fn easytier_buffer_alloc(length: u32) -> u32 {
let length = usize::try_from(length).expect("u32 fits usize on wasm32");
if length == 0 || length > MAX_GUEST_BUFFER_LEN {
set_abi_error("invalid guest buffer length");
return 0;
}
let mut buffer = vec![0_u8; length].into_boxed_slice();
let pointer = buffer.as_mut_ptr() as u32;
if pointer == 0 {
set_abi_error("guest buffer allocation failed");
return 0;
}
with_abi_state_mut(|state| {
if state.buffers.contains_key(&pointer) {
state.set_global_error("guest buffer allocation collided with a live buffer");
0
} else {
state.buffers.insert(pointer, buffer);
pointer
}
})
}
#[unsafe(no_mangle)]
/// Releases a buffer previously returned by [`easytier_buffer_alloc`].
pub extern "C" fn easytier_buffer_free(pointer: u32) -> i32 {
with_abi_state_mut(|state| {
if state.buffers.remove(&pointer).is_some() {
0
} else {
state.set_global_error(format!("unknown guest buffer: {pointer}"));
INVALID_INPUT
}
})
}
#[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.
/// 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,
) -> u64 {
let encoded = match read_guest_buffer(config_pointer, config_length, MAX_CREATE_CONFIG_LEN)
{
Ok(encoded) => encoded,
Err(error) => {
set_abi_error(error);
return 0;
}
};
let create_config = match decode_create_config(&encoded) {
Ok(config) => config,
Err(error) => {
set_abi_error(error);
return 0;
}
};
let config = match create_config.parse_config() {
Ok(config) => config,
Err(error) => {
set_abi_error(error);
return 0;
}
};
let handle = match with_abi_state_mut(WasiAbiState::allocate_handle) {
Ok(handle) => handle,
Err(_) => return 0,
};
let instance = manager_create(
config,
WasiCreateContext {
domain: handle,
environment: create_config.environment,
packet_sink: HostPacketSinkHandle(packet_sink_handle),
},
);
let instance = match instance {
Ok(instance) => instance,
Err(error) => {
clear_domain(handle);
set_abi_error(error);
return 0;
}
};
with_abi_state_mut(|state| {
let previous = state.handles.insert(
handle,
WasiHandleState {
instance_id: instance.instance_id(),
error: String::new(),
},
);
debug_assert!(previous.is_none());
handle
})
}
#[unsafe(no_mangle)]
/// Schedules instance startup. Completion is advanced by subsequent drive calls.
pub extern "C" fn easytier_instance_start(handle: u64) -> i32 {
with_instance(handle, |instance| match instance.start() {
Ok(()) => Ok(0),
Err(error) => {
set_instance_error(handle, error);
Ok(INVALID_STATE)
}
})
}
#[unsafe(no_mangle)]
/// Requests graceful instance shutdown. Completion is advanced by drive calls.
pub extern "C" fn easytier_instance_stop(handle: u64) -> i32 {
with_instance(handle, |instance| {
instance.stop();
Ok(0)
})
}
#[unsafe(no_mangle)]
/// Runs one bounded turn of the instance's externally driven Tokio runtime.
///
/// The return value is the current lifecycle state code, or a negative ABI
/// status on failure. Call after a timer deadline or host completion.
pub extern "C" fn easytier_instance_drive(handle: u64) -> i32 {
with_instance(handle, |instance| {
instance.drive()?;
Ok(instance.state_code())
})
}
#[unsafe(no_mangle)]
/// Wakes tasks whose host I/O operation may have completed.
///
/// The runtime calls this after finishing one or more `easytier_host`
/// operations; it does not itself consume a host completion.
pub extern "C" fn easytier_instance_notify_completions(handle: u64) -> i32 {
with_instance(handle, |instance| {
instance.core.notify_host_completions();
Ok(0)
})
}
#[unsafe(no_mangle)]
/// Returns the current lifecycle state code without running the instance.
pub extern "C" fn easytier_instance_state(handle: u64) -> i32 {
with_instance(handle, |instance| Ok(instance.state_code()))
}
/// Returns milliseconds until the next required drive, rounded up. A zero
/// means lifecycle work remains locally runnable. `i64::MAX` means core is
/// waiting only for a host completion; negative values are ABI status codes.
#[unsafe(no_mangle)]
pub extern "C" fn easytier_instance_next_deadline_millis(handle: u64) -> i64 {
let instance_id = match with_abi_state_mut(|state| state.begin_instance_call(handle)) {
Ok(instance_id) => instance_id,
Err(status) => return i64::from(status),
};
let result = match manager_get(instance_id) {
Some(instance) => instance
.next_wait_millis()
.map(|millis| i64::try_from(millis).unwrap_or(i64::MAX))
.unwrap_or(i64::MAX),
None => {
set_instance_error(
handle,
format!("core instance {instance_id} is not registered"),
);
i64::from(INVALID_STATE)
}
};
with_abi_state_mut(WasiAbiState::finish_instance_call);
result
}
#[unsafe(no_mangle)]
/// Copies a raw IP packet from a guest ABI buffer into EasyTier ingress.
///
/// Packet processing is asynchronous; success only means the ingress task
/// was scheduled. The caller retains and may free the source buffer once
/// this function returns.
pub extern "C" fn easytier_instance_send_packet(
handle: u64,
packet_pointer: u32,
packet_length: u32,
) -> i32 {
with_instance(handle, |instance| {
let packet = match read_guest_buffer(packet_pointer, packet_length, 1024 * 1024) {
Ok(packet) => packet,
Err(error) => {
set_instance_error(handle, error);
return Ok(INVALID_INPUT);
}
};
instance.send_packet(packet);
Ok(0)
})
}
#[unsafe(no_mangle)]
/// Destroys an instance and releases its lifecycle, timer, and runtime state.
pub extern "C" fn easytier_instance_drop(handle: u64) -> i32 {
let instance_id = match with_abi_state_mut(|state| state.begin_instance_drop(handle)) {
Ok(instance_id) => instance_id,
Err(status) => return status,
};
let Some(instance) = manager_remove(instance_id) else {
set_instance_error(
handle,
format!("core instance {instance_id} is not registered"),
);
with_abi_state_mut(WasiAbiState::finish_instance_call);
return INVALID_STATE;
};
let domain = instance.domain;
{
let _domain = enter_domain(domain);
drop(instance);
}
clear_domain(domain);
with_abi_state_mut(|state| state.finish_instance_drop(handle));
0
}
#[unsafe(no_mangle)]
/// Returns the byte length of the most recent lifecycle error for `handle`.
pub extern "C" fn easytier_instance_error_len(handle: u64) -> u32 {
with_abi_state(|state| {
u32::try_from(state.error_for_handle(handle).len()).unwrap_or(u32::MAX)
})
}
#[unsafe(no_mangle)]
/// Copies the most recent lifecycle error into a caller-owned ABI buffer.
///
/// `capacity` must contain the whole error; on success returns the copied
/// byte count, otherwise a negative ABI status code.
pub extern "C" fn easytier_instance_error_copy(
handle: u64,
destination: u32,
capacity: u32,
) -> i32 {
with_abi_state_mut(|state| {
let error = state.error_for_handle(handle).as_bytes().to_vec();
let capacity = usize::try_from(capacity).expect("u32 fits usize on wasm32");
let Some(destination) = state.buffers.get_mut(&destination) else {
return INVALID_INPUT;
};
if capacity > destination.len() || capacity < error.len() {
return INVALID_INPUT;
}
destination[..error.len()].copy_from_slice(&error);
i32::try_from(error.len()).unwrap_or(INVALID_INPUT)
})
}
}
@@ -0,0 +1,737 @@
//! Public data-plane guest exports backed by the instance operation broker.
use std::{net::SocketAddr, time::Duration};
use crate::{
gateway::{
DataPlaneError, DataPlaneErrorKind, DataPlaneOperationId, DataPlaneOperationKind,
DataPlaneOperationResult, DataPlaneResourceId, DataPlaneSession,
},
wasi::{
abi::{
DATA_PLANE_ABI_VERSION, DATA_PLANE_CAPABILITY, DATA_PLANE_TCP_CAPABILITY,
DATA_PLANE_UDP_CAPABILITY,
},
wire::{
data_plane::{
COMPLETION_LEN, TCP_ACCEPT_RESULT_LEN, TCP_BIND_RESULT_LEN, TCP_CONNECT_RESULT_LEN,
TCP_READ_METADATA_LEN, UDP_BIND_RESULT_LEN, UDP_RECEIVE_METADATA_LEN,
decode_ipv4_socket_address, encode_completion, encode_resource_and_address,
encode_stream_addresses, encode_tcp_read_metadata, encode_udp_receive_metadata,
error_status, normalize_call_status,
},
socket::SOCKET_ADDRESS_LEN,
},
},
};
use super::{WasiInstance, set_instance_error, with_abi_state, with_abi_state_mut, with_instance};
const OPERATION_ID_LEN: usize = 8;
const MAX_WRITE_LEN: usize = 1024 * 1024;
type WasiDataPlaneSession = DataPlaneSession<
crate::connectivity::connector_host::ConnectorHost<
crate::wasi::adapter::socket::backend::WasiHostSocketBackend,
crate::wasi::adapter::environment::WasiHostConnectorEnvironmentIo,
>,
>;
impl WasiInstance {
fn data_plane_session(&self) -> std::sync::Arc<WasiDataPlaneSession> {
self.core.core().data_plane_session()
}
fn submit_data_plane(
&self,
submit: impl FnOnce(
&std::sync::Arc<WasiDataPlaneSession>,
) -> Result<DataPlaneOperationId, DataPlaneError>,
) -> Result<DataPlaneOperationId, DataPlaneError> {
let execution = self.execution.lock().unwrap();
let _domain = crate::foundation::time::enter_domain(self.domain);
let _runtime = execution.runtime.enter();
submit(&self.data_plane_session())
}
}
fn error(kind: DataPlaneErrorKind, message: impl Into<String>) -> DataPlaneError {
DataPlaneError::new(kind, message)
}
fn invalid_input(message: impl Into<String>) -> DataPlaneError {
error(DataPlaneErrorKind::Io, message)
}
fn timeout(timeout_ms: u64) -> Option<Duration> {
(timeout_ms != u64::MAX).then(|| Duration::from_millis(timeout_ms))
}
fn operation_id(raw: u64) -> Result<DataPlaneOperationId, DataPlaneError> {
DataPlaneOperationId::from_raw(raw)
.ok_or_else(|| error(DataPlaneErrorKind::HandleClosed, "invalid operation ID"))
}
fn resource_id(raw: u64) -> Result<DataPlaneResourceId, DataPlaneError> {
DataPlaneResourceId::from_raw(raw)
.ok_or_else(|| error(DataPlaneErrorKind::HandleClosed, "invalid resource ID"))
}
fn validate_local_port(raw: u32) -> Result<u16, DataPlaneError> {
u16::try_from(raw).map_err(|_| invalid_input(format!("invalid local port {raw}")))
}
fn data_plane_call(
handle: u64,
operation: impl FnOnce(&WasiInstance) -> Result<i32, DataPlaneError>,
) -> i32 {
let mut entered = false;
let status = with_instance(handle, |instance| {
entered = true;
Ok(match operation(instance) {
Ok(status) => status,
Err(error) => {
let status = error_status(error.kind());
set_instance_error(handle, error.message());
status
}
})
});
normalize_call_status(entered, status)
}
fn validate_output(pointer: u32, capacity: usize, required: usize) -> Result<(), DataPlaneError> {
if required == 0 && capacity == 0 && pointer == 0 {
return Ok(());
}
if pointer == 0 {
return Err(invalid_input("guest output buffer pointer is zero"));
}
with_abi_state(|state| {
let buffer = state
.buffers
.get(&pointer)
.ok_or_else(|| invalid_input(format!("unknown guest buffer: {pointer}")))?;
if capacity > buffer.len() {
return Err(invalid_input(format!(
"guest output capacity {capacity} exceeds allocation {}",
buffer.len()
)));
}
if required > capacity {
return Err(error(
DataPlaneErrorKind::BufferTooSmall,
format!("result requires {required} bytes, buffer has {capacity}"),
));
}
Ok(())
})
}
fn validate_fixed_output(pointer: u32, required: usize) -> Result<(), DataPlaneError> {
validate_output(pointer, required, required)
}
fn write_output(pointer: u32, bytes: &[u8]) -> Result<(), DataPlaneError> {
if bytes.is_empty() && pointer == 0 {
return Ok(());
}
with_abi_state_mut(|state| {
let buffer = state
.buffers
.get_mut(&pointer)
.ok_or_else(|| invalid_input(format!("unknown guest buffer: {pointer}")))?;
if bytes.len() > buffer.len() {
return Err(error(
DataPlaneErrorKind::BufferTooSmall,
format!(
"result requires {} bytes, allocation has {}",
bytes.len(),
buffer.len()
),
));
}
buffer[..bytes.len()].copy_from_slice(bytes);
Ok(())
})
}
fn read_input(pointer: u32, length: u32, maximum: usize) -> Result<Vec<u8>, DataPlaneError> {
let length = usize::try_from(length).expect("u32 fits usize on wasm32");
if length == 0 {
return Ok(Vec::new());
}
if pointer == 0 || length > maximum {
return Err(invalid_input("invalid guest input buffer reference"));
}
with_abi_state(|state| {
state
.read_buffer(pointer, length)
.map_err(DataPlaneError::from)
})
}
fn read_ipv4_address(pointer: u32) -> Result<SocketAddr, DataPlaneError> {
let encoded = read_input(pointer, SOCKET_ADDRESS_LEN as u32, SOCKET_ADDRESS_LEN)?;
decode_ipv4_socket_address(&encoded).map_err(DataPlaneError::from)
}
fn write_operation_id(pointer: u32, operation: DataPlaneOperationId) -> Result<(), DataPlaneError> {
write_output(pointer, &operation.get().to_be_bytes())
}
fn submit_operation(
handle: u64,
output: u32,
submit: impl FnOnce(&WasiInstance) -> Result<DataPlaneOperationId, DataPlaneError>,
) -> i32 {
data_plane_call(handle, |instance| {
validate_fixed_output(output, OPERATION_ID_LEN)?;
let operation = submit(instance)?;
if let Err(error) = write_operation_id(output, operation) {
instance.data_plane_session().free_operation(operation);
return Err(error);
}
Ok(0)
})
}
fn take_result<T>(
session: &WasiDataPlaneSession,
operation: DataPlaneOperationId,
expected: DataPlaneOperationKind,
take: impl FnOnce(&DataPlaneOperationResult) -> Result<T, DataPlaneError>,
) -> Result<T, DataPlaneError> {
let actual = session.operation_kind(operation)?;
if actual != expected {
return Err(invalid_input(format!(
"operation kind mismatch: expected {expected:?}, got {actual:?}"
)));
}
let mut extraction_error = None;
let result = session.take_result_with(operation, |outcome| {
Some(match outcome {
Ok(result) => match take(result) {
Ok(value) => Ok(value),
Err(error) => {
extraction_error = Some(error);
return None;
}
},
Err(kind) => Err(error(
*kind,
format!("data-plane operation failed with {kind:?}"),
)),
})
})?;
if let Some(error) = extraction_error {
return Err(error);
}
result.ok_or_else(|| invalid_input("data-plane result could not be consumed"))?
}
fn require_ipv4(address: SocketAddr) -> Result<SocketAddr, DataPlaneError> {
address.is_ipv4().then_some(address).ok_or_else(|| {
error(
DataPlaneErrorKind::AddressFamilyUnsupported,
"data-plane ABI v2 supports IPv4 only",
)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_abi_version() -> u32 {
DATA_PLANE_ABI_VERSION
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_capabilities() -> u64 {
DATA_PLANE_CAPABILITY | DATA_PLANE_TCP_CAPABILITY | DATA_PLANE_UDP_CAPABILITY
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_tcp_connect_submit(
handle: u64,
peer_address: u32,
timeout_ms: u64,
output_operation: u32,
) -> i32 {
let peer_address = match read_ipv4_address(peer_address) {
Ok(address) => address,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
submit_operation(handle, output_operation, |instance| {
instance.submit_data_plane(|session| {
session.submit_tcp_connect(peer_address, timeout(timeout_ms))
})
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_tcp_bind_submit(
handle: u64,
local_port: u32,
timeout_ms: u64,
output_operation: u32,
) -> i32 {
let local_port = match validate_local_port(local_port) {
Ok(port) => port,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
submit_operation(handle, output_operation, |instance| {
instance
.submit_data_plane(|session| session.submit_tcp_bind(local_port, timeout(timeout_ms)))
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_tcp_accept_submit(
handle: u64,
listener: u64,
timeout_ms: u64,
output_operation: u32,
) -> i32 {
let listener = match resource_id(listener) {
Ok(listener) => listener,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
submit_operation(handle, output_operation, |instance| {
instance
.submit_data_plane(|session| session.submit_tcp_accept(listener, timeout(timeout_ms)))
})
}
#[unsafe(no_mangle)]
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) {
Ok(stream) => stream,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
submit_operation(handle, output_operation, |instance| {
instance.submit_data_plane(|session| {
session.submit_tcp_read(stream, max_len as usize, timeout(timeout_ms))
})
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_tcp_write_submit(
handle: u64,
stream: u64,
data_pointer: u32,
data_length: u32,
timeout_ms: u64,
output_operation: u32,
) -> i32 {
let stream = match resource_id(stream) {
Ok(stream) => stream,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
let data = match read_input(data_pointer, data_length, MAX_WRITE_LEN) {
Ok(data) => data,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
submit_operation(handle, output_operation, |instance| {
instance.submit_data_plane(|session| {
session.submit_tcp_write(stream, data, timeout(timeout_ms))
})
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_udp_bind_submit(
handle: u64,
local_port: u32,
timeout_ms: u64,
output_operation: u32,
) -> i32 {
let local_port = match validate_local_port(local_port) {
Ok(port) => port,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
submit_operation(handle, output_operation, |instance| {
instance
.submit_data_plane(|session| session.submit_udp_bind(local_port, timeout(timeout_ms)))
})
}
#[unsafe(no_mangle)]
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) {
Ok(socket) => socket,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
submit_operation(handle, output_operation, |instance| {
instance.submit_data_plane(|session| {
session.submit_udp_receive(socket, max_len as usize, timeout(timeout_ms))
})
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_udp_send_submit(
handle: u64,
socket: u64,
peer_address: u32,
data_pointer: u32,
data_length: u32,
timeout_ms: u64,
output_operation: u32,
) -> i32 {
let socket = match resource_id(socket) {
Ok(socket) => socket,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
let peer_address = match read_ipv4_address(peer_address) {
Ok(address) => address,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
let data = match read_input(data_pointer, data_length, MAX_WRITE_LEN) {
Ok(data) => data,
Err(error) => {
set_instance_error(handle, error.message());
return error_status(error.kind());
}
};
submit_operation(handle, output_operation, |instance| {
instance.submit_data_plane(|session| {
session.submit_udp_send(socket, peer_address, data, timeout(timeout_ms))
})
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_operation_cancel(handle: u64, operation: u64) -> i32 {
data_plane_call(handle, |instance| {
instance
.data_plane_session()
.cancel_operation(operation_id(operation)?);
Ok(0)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_operation_free(handle: u64, operation: u64) -> i32 {
data_plane_call(handle, |instance| {
instance
.data_plane_session()
.free_operation(operation_id(operation)?);
Ok(0)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_resource_close(handle: u64, resource: u64) -> i32 {
data_plane_call(handle, |instance| {
instance
.data_plane_session()
.close_resource(resource_id(resource)?);
Ok(0)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_completion_drain(
handle: u64,
output: u32,
capacity: u32,
) -> i32 {
data_plane_call(handle, |instance| {
let capacity = usize::try_from(capacity).expect("u32 fits usize on wasm32");
let required = capacity
.checked_mul(COMPLETION_LEN)
.ok_or_else(|| invalid_input("completion output size overflow"))?;
validate_output(output, required, required)?;
let completions = instance.data_plane_session().drain_completions(capacity);
let mut encoded = Vec::with_capacity(completions.len() * COMPLETION_LEN);
for completion in completions {
encoded.extend_from_slice(&encode_completion(completion));
}
write_output(output, &encoded)?;
i32::try_from(encoded.len() / COMPLETION_LEN)
.map_err(|_| invalid_input("completion count exceeds i32"))
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_result_size(handle: u64, operation: u64) -> i32 {
data_plane_call(handle, |instance| {
let size = instance
.data_plane_session()
.result_payload_bytes(operation_id(operation)?)?;
i32::try_from(size).map_err(|_| invalid_input("data-plane result size exceeds i32"))
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_tcp_connect_result_take(
handle: u64,
operation: u64,
output: u32,
) -> i32 {
data_plane_call(handle, |instance| {
validate_fixed_output(output, TCP_CONNECT_RESULT_LEN)?;
let session = instance.data_plane_session();
let wire = take_result(
&session,
operation_id(operation)?,
DataPlaneOperationKind::TcpConnect,
|result| match result {
DataPlaneOperationResult::TcpConnected {
stream,
local_addr,
peer_addr,
} => Ok(encode_stream_addresses(
stream.get(),
require_ipv4(*local_addr)?,
require_ipv4(*peer_addr)?,
)),
_ => Err(invalid_input("TCP connect result variant mismatch")),
},
)?;
write_output(output, &wire)?;
Ok(0)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_tcp_bind_result_take(
handle: u64,
operation: u64,
output: u32,
) -> i32 {
data_plane_call(handle, |instance| {
validate_fixed_output(output, TCP_BIND_RESULT_LEN)?;
let session = instance.data_plane_session();
let wire = take_result(
&session,
operation_id(operation)?,
DataPlaneOperationKind::TcpBind,
|result| match result {
DataPlaneOperationResult::TcpBound {
listener,
local_addr,
} => Ok(encode_resource_and_address(
listener.get(),
require_ipv4(*local_addr)?,
)),
_ => Err(invalid_input("TCP bind result variant mismatch")),
},
)?;
write_output(output, &wire)?;
Ok(0)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_tcp_accept_result_take(
handle: u64,
operation: u64,
output: u32,
) -> i32 {
data_plane_call(handle, |instance| {
validate_fixed_output(output, TCP_ACCEPT_RESULT_LEN)?;
let session = instance.data_plane_session();
let wire = take_result(
&session,
operation_id(operation)?,
DataPlaneOperationKind::TcpAccept,
|result| match result {
DataPlaneOperationResult::TcpAccepted {
stream,
local_addr,
peer_addr,
} => Ok(encode_stream_addresses(
stream.get(),
require_ipv4(*local_addr)?,
require_ipv4(*peer_addr)?,
)),
_ => Err(invalid_input("TCP accept result variant mismatch")),
},
)?;
write_output(output, &wire)?;
Ok(0)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_tcp_read_result_take(
handle: u64,
operation: u64,
data_output: u32,
data_capacity: u32,
metadata_output: u32,
) -> i32 {
data_plane_call(handle, |instance| {
let session = instance.data_plane_session();
let operation = operation_id(operation)?;
let required = session.result_payload_bytes(operation)?;
validate_output(data_output, data_capacity as usize, required)?;
validate_fixed_output(metadata_output, TCP_READ_METADATA_LEN)?;
take_result(
&session,
operation,
DataPlaneOperationKind::TcpRead,
|result| match result {
DataPlaneOperationResult::TcpRead { data, eof } => {
write_output(data_output, data)?;
write_output(metadata_output, &encode_tcp_read_metadata(*eof))?;
i32::try_from(data.len())
.map_err(|_| invalid_input("TCP read result exceeds i32"))
}
_ => Err(invalid_input("TCP read result variant mismatch")),
},
)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_tcp_write_result_take(handle: u64, operation: u64) -> i32 {
data_plane_call(handle, |instance| {
let session = instance.data_plane_session();
let len = take_result(
&session,
operation_id(operation)?,
DataPlaneOperationKind::TcpWrite,
|result| match result {
DataPlaneOperationResult::TcpWritten { len } => Ok(*len),
_ => Err(invalid_input("TCP write result variant mismatch")),
},
)?;
i32::try_from(len).map_err(|_| invalid_input("TCP write result exceeds i32"))
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_udp_bind_result_take(
handle: u64,
operation: u64,
output: u32,
) -> i32 {
data_plane_call(handle, |instance| {
validate_fixed_output(output, UDP_BIND_RESULT_LEN)?;
let session = instance.data_plane_session();
let wire = take_result(
&session,
operation_id(operation)?,
DataPlaneOperationKind::UdpBind,
|result| match result {
DataPlaneOperationResult::UdpBound { socket, local_addr } => Ok(
encode_resource_and_address(socket.get(), require_ipv4(*local_addr)?),
),
_ => Err(invalid_input("UDP bind result variant mismatch")),
},
)?;
write_output(output, &wire)?;
Ok(0)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_udp_receive_result_take(
handle: u64,
operation: u64,
data_output: u32,
data_capacity: u32,
metadata_output: u32,
) -> i32 {
data_plane_call(handle, |instance| {
let session = instance.data_plane_session();
let operation = operation_id(operation)?;
let required = session.result_payload_bytes(operation)?;
validate_output(data_output, data_capacity as usize, required)?;
validate_fixed_output(metadata_output, UDP_RECEIVE_METADATA_LEN)?;
take_result(
&session,
operation,
DataPlaneOperationKind::UdpReceive,
|result| match result {
DataPlaneOperationResult::UdpReceived {
data,
peer_addr,
truncated,
} => {
write_output(data_output, data)?;
write_output(
metadata_output,
&encode_udp_receive_metadata(require_ipv4(*peer_addr)?, *truncated),
)?;
i32::try_from(data.len())
.map_err(|_| invalid_input("UDP receive result exceeds i32"))
}
_ => Err(invalid_input("UDP receive result variant mismatch")),
},
)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn easytier_data_plane_udp_send_result_take(handle: u64, operation: u64) -> i32 {
data_plane_call(handle, |instance| {
let session = instance.data_plane_session();
let len = take_result(
&session,
operation_id(operation)?,
DataPlaneOperationKind::UdpSend,
|result| match result {
DataPlaneOperationResult::UdpSent { len } => Ok(*len),
_ => Err(invalid_input("UDP send result variant mismatch")),
},
)?;
i32::try_from(len).map_err(|_| invalid_input("UDP send result exceeds i32"))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn infinite_timeout_sentinel_is_distinct_from_zero() {
assert_eq!(timeout(u64::MAX), None);
assert_eq!(timeout(0), Some(Duration::ZERO));
}
}
+151
View File
@@ -0,0 +1,151 @@
//! Bounded current-thread Tokio turns for externally driven runtimes.
use std::{
future::{Future, poll_fn},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
task::{Poll, Waker},
time::Duration,
};
use tokio::runtime::Runtime;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum RuntimeDriveOutcome {
Quiescent,
BudgetExhausted,
}
#[derive(Clone, Default)]
pub(super) struct RuntimeDriver {
state: Arc<RuntimeDriverState>,
}
#[derive(Default)]
struct RuntimeDriverState {
active: AtomicBool,
quiescent: AtomicBool,
waker: Mutex<Option<Waker>>,
}
impl RuntimeDriver {
pub(super) fn on_thread_park(&self) {
if !self.state.active.load(Ordering::SeqCst) {
return;
}
self.state.quiescent.store(true, Ordering::SeqCst);
if let Some(waker) = self.state.waker.lock().unwrap().take() {
waker.wake();
}
}
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;
});
let _active = RuntimeDriverGuard::activate(self.state.as_ref());
runtime.block_on(async {
let budget = tokio::time::sleep(Duration::ZERO);
tokio::pin!(budget);
poll_fn(|context| {
if self.state.poll_quiescent(context.waker()) {
return Poll::Ready(RuntimeDriveOutcome::Quiescent);
}
if budget.as_mut().poll(context).is_ready() {
return Poll::Ready(RuntimeDriveOutcome::BudgetExhausted);
}
Poll::Pending
})
.await
})
}
}
impl RuntimeDriverState {
fn poll_quiescent(&self, waker: &Waker) -> bool {
if self.quiescent.load(Ordering::SeqCst) {
return true;
}
*self.waker.lock().unwrap() = Some(waker.clone());
self.quiescent.load(Ordering::SeqCst)
}
}
struct RuntimeDriverGuard<'a> {
state: &'a RuntimeDriverState,
}
impl<'a> RuntimeDriverGuard<'a> {
fn activate(state: &'a RuntimeDriverState) -> Self {
state.quiescent.store(false, Ordering::SeqCst);
*state.waker.lock().unwrap() = None;
state.active.store(true, Ordering::SeqCst);
Self { state }
}
}
impl Drop for RuntimeDriverGuard<'_> {
fn drop(&mut self) {
self.state.active.store(false, Ordering::SeqCst);
self.state.quiescent.store(false, Ordering::SeqCst);
*self.state.waker.lock().unwrap() = None;
}
}
#[cfg(test)]
mod tests {
use std::{future::poll_fn, sync::Arc, task::Poll};
use tokio::{runtime::Builder, sync::Notify};
use super::{RuntimeDriveOutcome, RuntimeDriver};
fn runtime(driver: &RuntimeDriver) -> tokio::runtime::Runtime {
let park_driver = driver.clone();
Builder::new_current_thread()
.enable_time()
.event_interval(3)
.on_thread_park(move || park_driver.on_thread_park())
.build()
.unwrap()
}
#[test]
fn reports_budget_exhaustion_for_a_continuously_runnable_task() {
let driver = RuntimeDriver::default();
let runtime = runtime(&driver);
let task = runtime.spawn(poll_fn(|context| {
context.waker().wake_by_ref();
Poll::<()>::Pending
}));
assert_eq!(driver.drive(&runtime), RuntimeDriveOutcome::BudgetExhausted);
task.abort();
while driver.drive(&runtime) == RuntimeDriveOutcome::BudgetExhausted {}
assert!(task.is_finished());
}
#[test]
fn reports_quiescence_while_waiting_for_an_external_wake() {
let driver = RuntimeDriver::default();
let runtime = runtime(&driver);
let notify = Arc::new(Notify::new());
let task_notify = notify.clone();
let task = runtime.spawn(async move {
task_notify.notified().await;
});
assert_eq!(driver.drive(&runtime), RuntimeDriveOutcome::Quiescent);
assert!(!task.is_finished());
notify.notify_one();
while driver.drive(&runtime) == RuntimeDriveOutcome::BudgetExhausted {}
assert!(task.is_finished());
}
}
+34
View File
@@ -0,0 +1,34 @@
//! Versioned, serialized inputs accepted by the WASI instance lifecycle ABI.
use serde::{Deserialize, Serialize};
use crate::{
config::toml::TomlConfig, connectivity::connector_host::HostConnectorEnvironmentSnapshot,
};
pub(crate) const WASI_CORE_INSTANCE_CONFIG_VERSION: u32 =
crate::wasi::abi::CORE_INSTANCE_CONFIG_VERSION;
/// Versioned payload accepted by host-driven instance frontends.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct WasiCoreInstanceCreateConfig {
pub version: u32,
pub config: String,
pub environment: HostConnectorEnvironmentSnapshot,
}
impl WasiCoreInstanceCreateConfig {
pub fn validate(&self) -> anyhow::Result<()> {
if self.version != WASI_CORE_INSTANCE_CONFIG_VERSION {
anyhow::bail!(
"unsupported host core instance config version: {}",
self.version
);
}
Ok(())
}
pub fn parse_config(&self) -> anyhow::Result<TomlConfig> {
TomlConfig::new_from_str_with_source("WASI create config", &self.config)
}
}
+318
View File
@@ -0,0 +1,318 @@
//! Deadline-tracking Tokio time implementation for externally driven WASI runtimes.
mod tracked {
use std::{
cell::{Cell, RefCell},
collections::BTreeMap,
future::{Future, IntoFuture},
pin::Pin,
task::{Context, Poll},
};
pub use tokio::time::{Duration, Instant, MissedTickBehavior, error};
thread_local! {
static CURRENT_DOMAIN: Cell<Option<u64>> = const { Cell::new(None) };
static DEADLINES: RefCell<DeadlineRegistry> = RefCell::new(DeadlineRegistry::default());
}
#[derive(Default)]
struct DeadlineRegistry {
next_token: u64,
entries: BTreeMap<u64, (u64, Instant)>,
}
impl DeadlineRegistry {
fn insert(&mut self, domain: u64, deadline: Instant) -> u64 {
loop {
self.next_token = self.next_token.wrapping_add(1);
if self.next_token != 0 && !self.entries.contains_key(&self.next_token) {
self.entries.insert(self.next_token, (domain, deadline));
return self.next_token;
}
}
}
}
pub(crate) struct TimerDomainGuard(Option<u64>);
impl Drop for TimerDomainGuard {
fn drop(&mut self) {
CURRENT_DOMAIN.set(self.0);
}
}
pub(crate) fn enter_domain(domain: u64) -> TimerDomainGuard {
TimerDomainGuard(CURRENT_DOMAIN.replace(Some(domain)))
}
pub(crate) fn clear_domain(domain: u64) {
DEADLINES.with_borrow_mut(|registry| {
registry
.entries
.retain(|_, (entry_domain, _)| *entry_domain != domain);
});
}
pub(crate) fn next_deadline_millis(domain: u64) -> Option<u64> {
let deadline = DEADLINES.with_borrow(|registry| {
registry
.entries
.values()
.filter_map(|(entry_domain, deadline)| {
(*entry_domain == domain).then_some(*deadline)
})
.min()
})?;
let duration = deadline.saturating_duration_since(Instant::now());
let nanos = duration.as_nanos();
Some(u64::try_from(nanos.div_ceil(1_000_000)).unwrap_or(u64::MAX))
}
struct Registration {
token: Option<u64>,
deadline: Instant,
}
impl Registration {
fn new(deadline: Instant) -> Self {
let mut registration = Self {
token: None,
deadline,
};
registration.ensure();
registration
}
fn ensure(&mut self) {
if self.token.is_some() {
return;
}
let Some(domain) = CURRENT_DOMAIN.get() else {
return;
};
self.token =
Some(DEADLINES.with_borrow_mut(|registry| registry.insert(domain, self.deadline)));
}
fn reset(&mut self, deadline: Instant) {
self.remove();
self.deadline = deadline;
self.ensure();
}
fn remove(&mut self) {
if let Some(token) = self.token.take() {
DEADLINES.with_borrow_mut(|registry| {
registry.entries.remove(&token);
});
}
}
}
impl Drop for Registration {
fn drop(&mut self) {
self.remove();
}
}
pub struct Sleep {
inner: Pin<Box<tokio::time::Sleep>>,
registration: Registration,
}
impl Sleep {
pub fn reset(mut self: Pin<&mut Self>, deadline: Instant) {
let this = self.as_mut().get_mut();
this.inner.as_mut().reset(deadline);
this.registration.reset(deadline);
}
}
impl Future for Sleep {
type Output = ();
fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().get_mut();
this.registration.ensure();
let result = this.inner.as_mut().poll(context);
if result.is_ready() {
this.registration.remove();
}
result
}
}
pub fn sleep(duration: Duration) -> Sleep {
sleep_until(Instant::now() + duration)
}
pub fn sleep_until(deadline: Instant) -> Sleep {
Sleep {
inner: Box::pin(tokio::time::sleep_until(deadline)),
registration: Registration::new(deadline),
}
}
pub struct Interval {
inner: Pin<Box<tokio::time::Sleep>>,
period: Duration,
next_deadline: Instant,
missed_tick_behavior: MissedTickBehavior,
registration: Registration,
}
impl Interval {
pub async fn tick(&mut self) -> Instant {
self.registration.ensure();
let tick = self.next_deadline;
self.inner.as_mut().await;
let now = Instant::now();
self.next_deadline = if now > tick + Duration::from_millis(5) {
next_interval_deadline(self.missed_tick_behavior, tick, now, self.period)
} else {
tick + self.period
};
self.inner.as_mut().reset(self.next_deadline);
self.registration.reset(self.next_deadline);
tick
}
}
pub(super) fn next_interval_deadline(
behavior: MissedTickBehavior,
tick: Instant,
now: Instant,
period: Duration,
) -> Instant {
match behavior {
MissedTickBehavior::Burst => tick + period,
MissedTickBehavior::Delay => now + period,
MissedTickBehavior::Skip => {
now + period
- Duration::from_nanos(
((now - tick).as_nanos() % period.as_nanos())
.try_into()
.expect("too much time has elapsed since the interval tick"),
)
}
}
}
pub fn interval(period: Duration) -> Interval {
interval_at(Instant::now(), period)
}
pub fn interval_at(start: Instant, period: Duration) -> Interval {
assert!(period > Duration::ZERO, "`period` must be non-zero.");
Interval {
inner: Box::pin(tokio::time::sleep_until(start)),
period,
next_deadline: start,
missed_tick_behavior: MissedTickBehavior::Burst,
registration: Registration::new(start),
}
}
pub async fn timeout<F>(duration: Duration, future: F) -> Result<F::Output, error::Elapsed>
where
F: IntoFuture,
{
timeout_at(Instant::now() + duration, future).await
}
pub async fn timeout_at<F>(deadline: Instant, future: F) -> Result<F::Output, error::Elapsed>
where
F: IntoFuture,
{
let _registration = Registration::new(deadline);
tokio::time::timeout_at(deadline, future).await
}
}
pub use tracked::{Duration, Instant, Interval, error, interval, sleep, timeout};
pub(crate) use tracked::{clear_domain, enter_domain, next_deadline_millis};
#[cfg(test)]
mod tests {
use super::tracked::MissedTickBehavior;
use super::*;
#[tokio::test]
async fn tracks_reset_completion_and_drop_per_domain() {
let _domain = enter_domain(7);
{
let sleep = sleep(Duration::from_millis(50));
tokio::pin!(sleep);
assert!(matches!(next_deadline_millis(7), Some(1..=50)));
assert_eq!(next_deadline_millis(8), None);
sleep
.as_mut()
.reset(Instant::now() + Duration::from_millis(20));
assert!(matches!(next_deadline_millis(7), Some(1..=20)));
}
assert_eq!(next_deadline_millis(7), None);
let pending = sleep(Duration::from_secs(1));
assert!(matches!(next_deadline_millis(7), Some(999..=1000)));
drop(pending);
assert_eq!(next_deadline_millis(7), None);
sleep(Duration::ZERO).await;
assert_eq!(next_deadline_millis(7), None);
}
#[tokio::test]
async fn tracks_interval_and_timeout_lifetimes() {
let _domain = enter_domain(9);
let mut ticker = interval(Duration::from_millis(40));
assert_eq!(next_deadline_millis(9), Some(0));
ticker.tick().await;
assert!(matches!(next_deadline_millis(9), Some(1..=40)));
drop(ticker);
assert_eq!(next_deadline_millis(9), None);
let timeout = tokio::spawn(timeout(
Duration::from_millis(60),
std::future::pending::<()>(),
));
tokio::task::yield_now().await;
assert!(matches!(next_deadline_millis(9), Some(1..=60)));
timeout.abort();
let _ = timeout.await;
assert_eq!(next_deadline_millis(9), None);
}
#[tokio::test]
async fn clears_all_deadlines_for_a_domain() {
let _domain = enter_domain(11);
let _timer = sleep(Duration::from_secs(1));
assert!(next_deadline_millis(11).is_some());
clear_domain(11);
assert_eq!(next_deadline_millis(11), None);
}
#[test]
fn computes_missed_interval_deadlines_like_tokio() {
let tick = Instant::now();
let now = tick + Duration::from_millis(250);
let period = Duration::from_millis(100);
assert_eq!(
tracked::next_interval_deadline(MissedTickBehavior::Burst, tick, now, period),
tick + period
);
assert_eq!(
tracked::next_interval_deadline(MissedTickBehavior::Delay, tick, now, period),
now + period
);
assert_eq!(
tracked::next_interval_deadline(MissedTickBehavior::Skip, tick, now, period),
tick + Duration::from_millis(300)
);
}
}
+53
View File
@@ -0,0 +1,53 @@
use std::io;
pub(crate) fn status(operation: &str, result: i32) -> io::Result<()> {
if result == 0 {
Ok(())
} else {
Err(host_error(operation, result))
}
}
pub(crate) fn host_error(operation: &str, code: i32) -> io::Error {
io::Error::other(format!("host {operation} failed with code {code}"))
}
pub(crate) fn tcp_connect_error(code: i32) -> io::Error {
let kind = match code {
-6 => io::ErrorKind::ConnectionRefused,
-7 => io::ErrorKind::ConnectionAborted,
-8 => io::ErrorKind::ConnectionReset,
-9 => io::ErrorKind::NotConnected,
_ => return host_error("take_tcp_connect", code),
};
io::Error::new(kind, format!("host TCP connect failed with code {code}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tcp_connect_status_preserves_error_kinds() {
assert_eq!(
tcp_connect_error(-6).kind(),
io::ErrorKind::ConnectionRefused
);
assert_eq!(
tcp_connect_error(-7).kind(),
io::ErrorKind::ConnectionAborted
);
assert_eq!(tcp_connect_error(-8).kind(), io::ErrorKind::ConnectionReset);
assert_eq!(tcp_connect_error(-9).kind(), io::ErrorKind::NotConnected);
assert_eq!(tcp_connect_error(-3).kind(), io::ErrorKind::Other);
}
#[test]
fn status_preserves_success_and_host_error_context() {
assert!(status("start_read", 0).is_ok());
assert_eq!(
status("start_read", -3).unwrap_err().kind(),
io::ErrorKind::Other
);
}
}
+161
View File
@@ -0,0 +1,161 @@
//! Explicit big-endian wire records returned by data-plane guest exports.
use std::{io, net::SocketAddr};
use crate::gateway::{DataPlaneCompletionDescriptor, DataPlaneErrorKind};
use super::socket::{SOCKET_ADDRESS_LEN, decode_socket_address, encode_socket_address};
pub(crate) const COMPLETION_LEN: usize = 12;
pub(crate) const TCP_CONNECT_RESULT_LEN: usize = 8 + SOCKET_ADDRESS_LEN * 2;
pub(crate) const TCP_BIND_RESULT_LEN: usize = 8 + SOCKET_ADDRESS_LEN;
pub(crate) const TCP_ACCEPT_RESULT_LEN: usize = TCP_CONNECT_RESULT_LEN;
pub(crate) const UDP_BIND_RESULT_LEN: usize = TCP_BIND_RESULT_LEN;
pub(crate) const TCP_READ_METADATA_LEN: usize = 1;
pub(crate) const UDP_RECEIVE_METADATA_LEN: usize = SOCKET_ADDRESS_LEN + 1;
pub(crate) fn decode_ipv4_socket_address(wire: &[u8]) -> io::Result<SocketAddr> {
let wire = <&[u8; SOCKET_ADDRESS_LEN]>::try_from(wire).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "invalid socket address length")
})?;
let address = decode_socket_address(wire)?;
if !address.is_ipv4() {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"data-plane ABI v2 supports IPv4 only",
));
}
Ok(address)
}
pub(crate) fn encode_completion(completion: DataPlaneCompletionDescriptor) -> [u8; COMPLETION_LEN] {
let mut wire = [0; COMPLETION_LEN];
wire[..8].copy_from_slice(&completion.operation_id.get().to_be_bytes());
wire[8..10].copy_from_slice(&(completion.kind as u16).to_be_bytes());
wire[10..12].copy_from_slice(&completion.status.code().to_be_bytes());
wire
}
pub(crate) fn encode_resource_and_address(
resource: u64,
address: SocketAddr,
) -> [u8; TCP_BIND_RESULT_LEN] {
let mut wire = [0; TCP_BIND_RESULT_LEN];
wire[..8].copy_from_slice(&resource.to_be_bytes());
wire[8..].copy_from_slice(&encode_socket_address(address));
wire
}
pub(crate) fn encode_stream_addresses(
stream: u64,
local_addr: SocketAddr,
peer_addr: SocketAddr,
) -> [u8; TCP_CONNECT_RESULT_LEN] {
let mut wire = [0; TCP_CONNECT_RESULT_LEN];
wire[..8].copy_from_slice(&stream.to_be_bytes());
wire[8..8 + SOCKET_ADDRESS_LEN].copy_from_slice(&encode_socket_address(local_addr));
wire[8 + SOCKET_ADDRESS_LEN..].copy_from_slice(&encode_socket_address(peer_addr));
wire
}
pub(crate) fn encode_tcp_read_metadata(eof: bool) -> [u8; TCP_READ_METADATA_LEN] {
[u8::from(eof)]
}
pub(crate) fn encode_udp_receive_metadata(
peer_addr: SocketAddr,
truncated: bool,
) -> [u8; UDP_RECEIVE_METADATA_LEN] {
let mut wire = [0; UDP_RECEIVE_METADATA_LEN];
wire[..SOCKET_ADDRESS_LEN].copy_from_slice(&encode_socket_address(peer_addr));
wire[SOCKET_ADDRESS_LEN] = u8::from(truncated);
wire
}
pub(crate) fn error_status(kind: DataPlaneErrorKind) -> i32 {
-(kind as i32)
}
pub(crate) fn normalize_call_status(entered: bool, status: i32) -> i32 {
if entered {
status
} else {
error_status(DataPlaneErrorKind::HandleClosed)
}
}
#[cfg(test)]
mod tests {
use crate::gateway::{DataPlaneCompletionStatus, DataPlaneOperationId, DataPlaneOperationKind};
use super::*;
#[test]
fn completion_record_has_no_native_padding() {
let completion = DataPlaneCompletionDescriptor {
operation_id: DataPlaneOperationId::from_raw(0x0102_0304_0506_0708).unwrap(),
kind: DataPlaneOperationKind::UdpReceive,
status: DataPlaneCompletionStatus::Error(DataPlaneErrorKind::BufferTooSmall),
};
assert_eq!(
encode_completion(completion),
[1, 2, 3, 4, 5, 6, 7, 8, 0, 7, 0, 13]
);
}
#[test]
fn operation_result_records_have_stable_layouts() {
let local_addr = "192.0.2.1:1234".parse().unwrap();
let peer_addr = "198.51.100.2:4321".parse().unwrap();
let resource = 0x0102_0304_0506_0708;
let resource_and_address = encode_resource_and_address(resource, local_addr);
assert_eq!(resource_and_address.len(), UDP_BIND_RESULT_LEN);
assert_eq!(&resource_and_address[..8], &resource.to_be_bytes());
assert_eq!(
&resource_and_address[8..],
&encode_socket_address(local_addr)
);
let stream_addresses = encode_stream_addresses(resource, local_addr, peer_addr);
assert_eq!(stream_addresses.len(), TCP_ACCEPT_RESULT_LEN);
assert_eq!(&stream_addresses[..8], &resource.to_be_bytes());
assert_eq!(
&stream_addresses[8..8 + SOCKET_ADDRESS_LEN],
&encode_socket_address(local_addr)
);
assert_eq!(
&stream_addresses[8 + SOCKET_ADDRESS_LEN..],
&encode_socket_address(peer_addr)
);
assert_eq!(encode_tcp_read_metadata(false), [0]);
assert_eq!(encode_tcp_read_metadata(true), [1]);
let udp_metadata = encode_udp_receive_metadata(peer_addr, true);
assert_eq!(
&udp_metadata[..SOCKET_ADDRESS_LEN],
&encode_socket_address(peer_addr)
);
assert_eq!(udp_metadata[SOCKET_ADDRESS_LEN], 1);
}
#[test]
fn public_address_decoder_rejects_ipv6() {
let address = "[2001:db8::1]:80".parse().unwrap();
let error = decode_ipv4_socket_address(&encode_socket_address(address)).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::Unsupported);
}
#[test]
fn lifecycle_failures_use_data_plane_status_codes() {
assert_eq!(
normalize_call_status(false, -1),
error_status(DataPlaneErrorKind::HandleClosed)
);
assert_eq!(
normalize_call_status(true, error_status(DataPlaneErrorKind::Cancelled)),
error_status(DataPlaneErrorKind::Cancelled)
);
}
}
+248
View File
@@ -0,0 +1,248 @@
use std::{io, net::IpAddr};
use crate::host::dns::{DnsQuery, DnsSrvRecord};
use crate::socket::IpVersion;
const DNS_WIRE_VERSION: u8 = 1;
pub(crate) fn encode_query(query: &DnsQuery) -> io::Result<Vec<u8>> {
let host = query.host.as_bytes();
let netns = query
.context
.netns
.as_ref()
.map(|netns| netns.token().as_bytes());
let host_len = encoded_len("DNS host", host.len())?;
let netns_len = encoded_len("DNS netns token", netns.map_or(0, <[u8]>::len))?;
let mut encoded = Vec::with_capacity(16 + host.len() + netns.map_or(0, <[u8]>::len));
encoded.push(DNS_WIRE_VERSION);
encoded.push(match query.context.ip_version {
IpVersion::V4 => 4,
IpVersion::V6 => 6,
IpVersion::Both => 0,
});
encoded.push(u8::from(query.context.socket_mark.is_some()));
encoded.extend_from_slice(&query.context.socket_mark.unwrap_or_default().to_be_bytes());
encoded.push(u8::from(netns.is_some()));
encoded.extend_from_slice(&netns_len.to_be_bytes());
if let Some(netns) = netns {
encoded.extend_from_slice(netns);
}
encoded.extend_from_slice(&host_len.to_be_bytes());
encoded.extend_from_slice(host);
Ok(encoded)
}
pub(crate) fn decode_addresses(encoded: &[u8]) -> io::Result<Vec<IpAddr>> {
let mut decoder = Decoder::new(encoded);
let count = decoder.take_count("DNS address count")?;
let mut addresses = Vec::with_capacity(count.min(64));
for _ in 0..count {
let family = decoder.take_u8("DNS address family")?;
let address = match family {
4 => IpAddr::V4(decoder.take_array::<4>("IPv4 address")?.into()),
6 => IpAddr::V6(decoder.take_array::<16>("IPv6 address")?.into()),
_ => return Err(invalid_data("invalid DNS address family")),
};
addresses.push(address);
}
decoder.finish()?;
Ok(addresses)
}
pub(crate) fn decode_txt(encoded: &[u8]) -> io::Result<String> {
let mut decoder = Decoder::new(encoded);
let length = decoder.take_count("DNS TXT length")?;
let text = decoder.take(length, "DNS TXT value")?;
decoder.finish()?;
String::from_utf8(text.to_vec()).map_err(|_| invalid_data("DNS TXT is not UTF-8"))
}
pub(crate) fn decode_srv(encoded: &[u8]) -> io::Result<Vec<DnsSrvRecord>> {
let mut decoder = Decoder::new(encoded);
let count = decoder.take_count("DNS SRV count")?;
let mut records = Vec::with_capacity(count.min(64));
for _ in 0..count {
let priority = decoder.take_u16("DNS SRV priority")?;
let weight = decoder.take_u16("DNS SRV weight")?;
let port = decoder.take_u16("DNS SRV port")?;
let target_len = decoder.take_count("DNS SRV target length")?;
let target = String::from_utf8(decoder.take(target_len, "DNS SRV target")?.to_vec())
.map_err(|_| invalid_data("DNS SRV target is not UTF-8"))?;
records.push(DnsSrvRecord {
priority,
weight,
port,
target,
});
}
decoder.finish()?;
Ok(records)
}
fn encoded_len(description: &str, length: usize) -> io::Result<u32> {
u32::try_from(length).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("{description} is too long"),
)
})
}
fn invalid_data(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message)
}
struct Decoder<'a> {
encoded: &'a [u8],
offset: usize,
}
impl<'a> Decoder<'a> {
fn new(encoded: &'a [u8]) -> Self {
Self { encoded, offset: 0 }
}
fn take(&mut self, length: usize, description: &'static str) -> io::Result<&'a [u8]> {
let end = self
.offset
.checked_add(length)
.ok_or_else(|| invalid_data("DNS result length overflow"))?;
let value = self
.encoded
.get(self.offset..end)
.ok_or_else(|| invalid_data(description))?;
self.offset = end;
Ok(value)
}
fn take_u8(&mut self, description: &'static str) -> io::Result<u8> {
Ok(self.take(1, description)?[0])
}
fn take_u16(&mut self, description: &'static str) -> io::Result<u16> {
Ok(u16::from_be_bytes(self.take_array::<2>(description)?))
}
fn take_count(&mut self, description: &'static str) -> io::Result<usize> {
usize::try_from(u32::from_be_bytes(self.take_array::<4>(description)?))
.map_err(|_| invalid_data("DNS result count exceeds guest usize"))
}
fn take_array<const N: usize>(&mut self, description: &'static str) -> io::Result<[u8; N]> {
self.take(N, description)?
.try_into()
.map_err(|_| invalid_data(description))
}
fn finish(self) -> io::Result<()> {
if self.offset == self.encoded.len() {
Ok(())
} else {
Err(invalid_data("DNS result has trailing bytes"))
}
}
}
#[cfg(test)]
mod tests {
use crate::socket::{NetNamespace, SocketContext};
use super::*;
#[test]
fn query_encoding_has_stable_versioned_layout() {
let query = DnsQuery::new(
"peer.example",
SocketContext {
ip_version: IpVersion::V6,
socket_mark: Some(0x01020304),
netns: Some(NetNamespace::new("netns0")),
},
);
let encoded = encode_query(&query).unwrap();
let mut expected = vec![DNS_WIRE_VERSION, 6, 1, 1, 2, 3, 4, 1];
expected.extend_from_slice(&6_u32.to_be_bytes());
expected.extend_from_slice(b"netns0");
expected.extend_from_slice(&12_u32.to_be_bytes());
expected.extend_from_slice(b"peer.example");
assert_eq!(encoded, expected);
let without_optional = encode_query(&DnsQuery::new(
"v4.example",
SocketContext {
ip_version: IpVersion::V4,
socket_mark: None,
netns: None,
},
))
.unwrap();
assert_eq!(
&without_optional[..12],
&[1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
);
assert_eq!(&without_optional[12..16], &10_u32.to_be_bytes());
assert_eq!(&without_optional[16..], b"v4.example");
}
#[test]
fn decodes_owned_address_txt_and_srv_results() {
let mut addresses = 2_u32.to_be_bytes().to_vec();
addresses.push(4);
addresses.extend_from_slice(&[192, 0, 2, 1]);
addresses.push(6);
addresses.extend_from_slice(
&"2001:db8::1"
.parse::<std::net::Ipv6Addr>()
.unwrap()
.octets(),
);
assert_eq!(
decode_addresses(&addresses).unwrap(),
vec![
"192.0.2.1".parse::<IpAddr>().unwrap(),
"2001:db8::1".parse::<IpAddr>().unwrap(),
]
);
let mut txt = 12_u32.to_be_bytes().to_vec();
txt.extend_from_slice(b"tcp://peer:1");
assert_eq!(decode_txt(&txt).unwrap(), "tcp://peer:1");
let mut srv = 1_u32.to_be_bytes().to_vec();
srv.extend_from_slice(&10_u16.to_be_bytes());
srv.extend_from_slice(&20_u16.to_be_bytes());
srv.extend_from_slice(&11010_u16.to_be_bytes());
srv.extend_from_slice(&13_u32.to_be_bytes());
srv.extend_from_slice(b"peer.example.");
assert_eq!(
decode_srv(&srv).unwrap(),
vec![DnsSrvRecord {
priority: 10,
weight: 20,
port: 11010,
target: "peer.example.".to_owned(),
}]
);
}
#[test]
fn rejects_malformed_results() {
assert!(decode_addresses(&[0, 0, 0]).is_err());
assert!(decode_addresses(&[0, 0, 0, 1, 9]).is_err());
assert!(decode_addresses(&[0, 0, 0, 0, 1]).is_err());
let mut invalid_txt = 1_u32.to_be_bytes().to_vec();
invalid_txt.push(0xff);
assert!(decode_txt(&invalid_txt).is_err());
let mut truncated_srv = 1_u32.to_be_bytes().to_vec();
truncated_srv.extend_from_slice(&10_u16.to_be_bytes());
assert!(decode_srv(&truncated_srv).is_err());
let mut trailing_srv = 0_u32.to_be_bytes().to_vec();
trailing_srv.push(0);
assert!(decode_srv(&trailing_srv).is_err());
}
}
+8
View File
@@ -0,0 +1,8 @@
//! WASI host ABI codecs shared by concrete adapters.
pub(crate) mod common;
#[cfg(feature = "proxy-smoltcp-stack")]
pub(crate) mod data_plane;
pub(crate) mod dns;
pub(crate) mod options;
pub(crate) mod socket;
+394
View File
@@ -0,0 +1,394 @@
use std::io;
use crate::host::socket::{
HostSocketHandle,
factory::{HostTcpConnectResult, HostUdpBindResult},
listener::HostTcpBindResult,
};
use crate::socket::{
IpVersion, SocketContext,
tcp::{TcpConnectOptions, TcpListenOptions, TcpListenPurpose, TcpSocketPurpose},
udp::{UdpBindOptions, UdpSocketPurpose},
};
use super::socket::{SOCKET_ADDRESS_LEN, decode_socket_address, encode_socket_address};
const OPTIONS_VERSION: u8 = 2;
pub(crate) const TCP_SOCKET_RESULT_LEN: usize = 8 + SOCKET_ADDRESS_LEN * 2;
pub(crate) const BOUND_SOCKET_RESULT_LEN: usize = 8 + SOCKET_ADDRESS_LEN;
pub(crate) fn encode_tcp_connect_options(options: &TcpConnectOptions) -> io::Result<Vec<u8>> {
let mut encoded = Vec::with_capacity(
75 + context_variable_len(&options.bind.context)
+ bind_device_len(&options.bind.bind_device),
);
encoded.push(OPTIONS_VERSION);
encoded.extend_from_slice(&encode_socket_address(options.remote_addr));
encode_optional_address(&mut encoded, options.bind.local_addr);
encode_context(&mut encoded, &options.bind.context)?;
encoded.push(match options.bind.reuse_addr {
None => 0,
Some(false) => 1,
Some(true) => 2,
});
encoded.push(u8::from(options.bind.reuse_port));
encoded.push(u8::from(options.bind.only_v6));
encoded.push(match options.purpose {
TcpSocketPurpose::DirectConnect => 0,
TcpSocketPurpose::FakeTcp => 1,
TcpSocketPurpose::HolePunch => 2,
TcpSocketPurpose::ManualConnect => 3,
TcpSocketPurpose::ProxyNat => 4,
TcpSocketPurpose::StunProbe => 5,
TcpSocketPurpose::Socks5 => 6,
TcpSocketPurpose::PortForward => 7,
TcpSocketPurpose::DataPlane => 8,
});
encode_bind_device(&mut encoded, &options.bind.bind_device)?;
Ok(encoded)
}
pub(crate) fn encode_udp_bind_options(options: &UdpBindOptions) -> io::Result<Vec<u8>> {
let mut encoded = Vec::with_capacity(
48 + context_variable_len(&options.context) + bind_device_len(&options.bind_device),
);
encoded.push(OPTIONS_VERSION);
encode_optional_address(&mut encoded, options.local_addr);
encode_context(&mut encoded, &options.context)?;
encoded.push(u8::from(options.reuse_addr));
encoded.push(u8::from(options.reuse_port));
encoded.push(u8::from(options.only_v6));
encoded.push(match options.purpose {
UdpSocketPurpose::HolePunchControl => 0,
UdpSocketPurpose::HolePunchCandidate => 1,
UdpSocketPurpose::DirectConnect => 2,
UdpSocketPurpose::PortBoundListener => 3,
UdpSocketPurpose::ProxyNat => 4,
UdpSocketPurpose::StunProbe => 5,
UdpSocketPurpose::Socks5 => 6,
UdpSocketPurpose::PortForward => 7,
UdpSocketPurpose::PortLease => 8,
});
encode_bind_device(&mut encoded, &options.bind_device)?;
Ok(encoded)
}
pub(crate) fn encode_tcp_listen_options(options: &TcpListenOptions) -> io::Result<Vec<u8>> {
let mut encoded = Vec::with_capacity(
48 + context_variable_len(&options.bind.context)
+ bind_device_len(&options.bind.bind_device),
);
encoded.push(OPTIONS_VERSION);
encode_optional_address(&mut encoded, options.bind.local_addr);
encode_context(&mut encoded, &options.bind.context)?;
encoded.push(match options.bind.reuse_addr {
None => 0,
Some(false) => 1,
Some(true) => 2,
});
encoded.push(u8::from(options.bind.reuse_port));
encoded.push(u8::from(options.bind.only_v6));
encoded.push(match options.purpose {
TcpListenPurpose::DirectConnect => 0,
TcpListenPurpose::HolePunch => 1,
TcpListenPurpose::ManualConnect => 2,
TcpListenPurpose::ProxyNat => 3,
TcpListenPurpose::Socks5 => 4,
TcpListenPurpose::PortForward => 5,
TcpListenPurpose::PortLease => 6,
});
encode_bind_device(&mut encoded, &options.bind.bind_device)?;
Ok(encoded)
}
pub(crate) fn decode_tcp_socket_result(
encoded: &[u8; TCP_SOCKET_RESULT_LEN],
) -> io::Result<HostTcpConnectResult> {
let local = <[u8; SOCKET_ADDRESS_LEN]>::try_from(&encoded[8..8 + SOCKET_ADDRESS_LEN]).unwrap();
let peer = <[u8; SOCKET_ADDRESS_LEN]>::try_from(&encoded[8 + SOCKET_ADDRESS_LEN..]).unwrap();
Ok(HostTcpConnectResult {
handle: HostSocketHandle(u64::from_be_bytes(encoded[..8].try_into().unwrap())),
local_addr: decode_socket_address(&local)?,
peer_addr: decode_socket_address(&peer)?,
transport_label: None,
})
}
pub(crate) fn decode_udp_bind_result(
encoded: &[u8; BOUND_SOCKET_RESULT_LEN],
) -> io::Result<HostUdpBindResult> {
Ok(HostUdpBindResult {
handle: decode_bound_handle(encoded),
local_addr: decode_bound_address(encoded)?,
})
}
pub(crate) fn decode_tcp_bind_result(
encoded: &[u8; BOUND_SOCKET_RESULT_LEN],
) -> io::Result<HostTcpBindResult> {
Ok(HostTcpBindResult {
handle: decode_bound_handle(encoded),
local_addr: decode_bound_address(encoded)?,
})
}
fn decode_bound_handle(encoded: &[u8; BOUND_SOCKET_RESULT_LEN]) -> HostSocketHandle {
HostSocketHandle(u64::from_be_bytes(encoded[..8].try_into().unwrap()))
}
fn decode_bound_address(
encoded: &[u8; BOUND_SOCKET_RESULT_LEN],
) -> io::Result<std::net::SocketAddr> {
let address = <[u8; SOCKET_ADDRESS_LEN]>::try_from(&encoded[8..]).unwrap();
decode_socket_address(&address)
}
fn encode_optional_address(encoded: &mut Vec<u8>, address: Option<std::net::SocketAddr>) {
encoded.extend_from_slice(
&address
.map(encode_socket_address)
.unwrap_or([0; SOCKET_ADDRESS_LEN]),
);
}
fn encode_mark(encoded: &mut Vec<u8>, mark: Option<u32>) {
encoded.push(u8::from(mark.is_some()));
encoded.extend_from_slice(&mark.unwrap_or_default().to_be_bytes());
}
fn encode_context(encoded: &mut Vec<u8>, context: &SocketContext) -> io::Result<()> {
encoded.push(match context.ip_version {
IpVersion::V4 => 0,
IpVersion::V6 => 1,
IpVersion::Both => 2,
});
encode_mark(encoded, context.socket_mark);
let netns = context.netns.as_ref().map(|netns| netns.token().as_bytes());
encoded.push(u8::from(netns.is_some()));
let length = u32::try_from(netns.map_or(0, <[u8]>::len))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "netns token is too long"))?;
encoded.extend_from_slice(&length.to_be_bytes());
if let Some(netns) = netns {
encoded.extend_from_slice(netns);
}
Ok(())
}
pub(crate) fn encode_socket_context(context: &SocketContext) -> io::Result<Vec<u8>> {
let mut encoded = Vec::with_capacity(11 + context_variable_len(context));
encode_context(&mut encoded, context)?;
Ok(encoded)
}
fn encode_bind_device(encoded: &mut Vec<u8>, device: &Option<String>) -> io::Result<()> {
let bytes = device.as_deref().unwrap_or_default().as_bytes();
encoded.push(u8::from(device.is_some()));
let length = u32::try_from(bytes.len())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "bind device is too long"))?;
encoded.extend_from_slice(&length.to_be_bytes());
encoded.extend_from_slice(bytes);
Ok(())
}
fn bind_device_len(device: &Option<String>) -> usize {
device.as_ref().map_or(0, String::len)
}
fn context_variable_len(context: &SocketContext) -> usize {
context
.netns
.as_ref()
.map_or(0, |netns| netns.token().len())
}
#[cfg(test)]
mod tests {
use crate::socket::{NetNamespace, tcp::TcpBindOptions};
use super::*;
#[test]
fn encodes_tcp_connect_options_with_stable_offsets() {
let options = TcpConnectOptions {
remote_addr: "192.0.2.2:11013".parse().unwrap(),
bind: TcpBindOptions::default()
.with_local_addr(Some("[2001:db8::1]:22026".parse().unwrap()))
.with_socket_mark(Some(0x01020304))
.with_bind_device(Some("device0".to_owned()))
.with_reuse_addr(true)
.with_reuse_port(true)
.with_only_v6(true),
purpose: TcpSocketPurpose::ManualConnect,
};
let encoded = encode_tcp_connect_options(&options).unwrap();
assert_eq!(encoded.len(), 82);
assert_eq!(encoded[0], OPTIONS_VERSION);
assert_eq!(&encoded[55..66], &[2, 1, 1, 2, 3, 4, 0, 0, 0, 0, 0]);
assert_eq!(&encoded[66..70], &[2, 1, 1, 3]);
assert_eq!(encoded[70], 1);
assert_eq!(&encoded[71..75], &7_u32.to_be_bytes());
assert_eq!(&encoded[75..], b"device0");
}
#[test]
fn bind_device_presence_distinguishes_none_empty_and_named() {
let remote = "192.0.2.2:11013".parse().unwrap();
let none = encode_tcp_connect_options(&TcpConnectOptions::direct_connect(remote)).unwrap();
let empty = encode_tcp_connect_options(
&TcpConnectOptions::direct_connect(remote)
.with_bind(TcpBindOptions::default().with_bind_device(Some(String::new()))),
)
.unwrap();
assert_eq!(&none[70..75], &[0, 0, 0, 0, 0]);
assert_eq!(&empty[70..75], &[1, 0, 0, 0, 0]);
let udp_none = encode_udp_bind_options(&UdpBindOptions::direct_connect()).unwrap();
let udp_empty = encode_udp_bind_options(
&UdpBindOptions::direct_connect().with_bind_device(Some(String::new())),
)
.unwrap();
assert_eq!(&udp_none[43..48], &[0, 0, 0, 0, 0]);
assert_eq!(&udp_empty[43..48], &[1, 0, 0, 0, 0]);
let listen_none = encode_tcp_listen_options(&TcpListenOptions::direct_connect(
"192.0.2.1:11013".parse().unwrap(),
))
.unwrap();
let listen_empty = encode_tcp_listen_options(
&TcpListenOptions::direct_connect("192.0.2.1:11013".parse().unwrap())
.with_bind(TcpBindOptions::default().with_bind_device(Some(String::new()))),
)
.unwrap();
assert_eq!(&listen_none[43..48], &[0, 0, 0, 0, 0]);
assert_eq!(&listen_empty[43..48], &[1, 0, 0, 0, 0]);
}
#[test]
fn encodes_socket_context_netns_and_zero_mark() {
let bind = TcpBindOptions::default().with_context(
SocketContext::default()
.with_ip_version(IpVersion::V6)
.with_socket_mark(Some(0))
.with_netns(Some(NetNamespace::new("instance-a"))),
);
let encoded = encode_tcp_connect_options(
&TcpConnectOptions::direct_connect("[2001:db8::2]:11013".parse().unwrap())
.with_bind(bind),
)
.unwrap();
assert_eq!(encoded[55], 1);
assert_eq!(&encoded[56..61], &[1, 0, 0, 0, 0]);
assert_eq!(encoded[61], 1);
assert_eq!(&encoded[62..66], &10_u32.to_be_bytes());
assert_eq!(&encoded[66..76], b"instance-a");
}
#[test]
fn encodes_standalone_socket_context() {
let context = SocketContext::default().with_ip_version(IpVersion::V6);
assert_eq!(
encode_socket_context(&context).unwrap(),
vec![1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
);
}
#[test]
fn encodes_udp_proxy_nat_purpose() {
let encoded = encode_udp_bind_options(&UdpBindOptions::proxy_nat()).unwrap();
assert_eq!(encoded[42], 4);
}
#[test]
fn encodes_udp_socks5_purpose() {
let encoded = encode_udp_bind_options(&UdpBindOptions::socks5()).unwrap();
assert_eq!(encoded[42], 6);
}
#[test]
fn encodes_tcp_proxy_nat_purposes() {
let remote = "192.0.2.2:11013".parse().unwrap();
let connect = encode_tcp_connect_options(&TcpConnectOptions::proxy_nat(remote)).unwrap();
assert_eq!(connect[69], 4);
let local = "0.0.0.0:0".parse().unwrap();
let listen = encode_tcp_listen_options(&TcpListenOptions::proxy_nat(local)).unwrap();
assert_eq!(listen[42], 3);
}
#[test]
fn encodes_stun_probe_purposes() {
let remote = "192.0.2.2:3478".parse().unwrap();
let local = "0.0.0.0:0".parse().unwrap();
let tcp =
encode_tcp_connect_options(&TcpConnectOptions::stun_probe(remote, local)).unwrap();
assert_eq!(tcp[69], 5);
let udp = encode_udp_bind_options(&UdpBindOptions::stun_probe()).unwrap();
assert_eq!(udp[42], 5);
}
#[test]
fn encodes_gateway_purposes_with_stable_values() {
let remote = "192.0.2.2:443".parse().unwrap();
assert_eq!(
encode_tcp_connect_options(&TcpConnectOptions::socks5(remote)).unwrap()[69],
6
);
assert_eq!(
encode_tcp_connect_options(&TcpConnectOptions::port_forward(remote)).unwrap()[69],
7
);
assert_eq!(
encode_tcp_connect_options(&TcpConnectOptions::data_plane(remote)).unwrap()[69],
8
);
let local = "0.0.0.0:0".parse().unwrap();
assert_eq!(
encode_tcp_listen_options(&TcpListenOptions::socks5(local)).unwrap()[42],
4
);
assert_eq!(
encode_tcp_listen_options(&TcpListenOptions::port_forward(local)).unwrap()[42],
5
);
assert_eq!(
encode_tcp_listen_options(&TcpListenOptions::port_lease(local)).unwrap()[42],
6
);
assert_eq!(
encode_udp_bind_options(&UdpBindOptions::port_forward(local)).unwrap()[42],
7
);
assert_eq!(
encode_udp_bind_options(&UdpBindOptions::port_lease(local)).unwrap()[42],
8
);
}
#[test]
fn decodes_fixed_socket_results() {
let mut tcp = [0_u8; TCP_SOCKET_RESULT_LEN];
tcp[..8].copy_from_slice(&41_u64.to_be_bytes());
tcp[8..35].copy_from_slice(&encode_socket_address("192.0.2.1:40100".parse().unwrap()));
tcp[35..].copy_from_slice(&encode_socket_address("192.0.2.2:11013".parse().unwrap()));
let result = decode_tcp_socket_result(&tcp).unwrap();
assert_eq!(result.handle, HostSocketHandle(41));
assert_eq!(result.local_addr, "192.0.2.1:40100".parse().unwrap());
assert_eq!(result.peer_addr, "192.0.2.2:11013".parse().unwrap());
let mut bound = [0_u8; BOUND_SOCKET_RESULT_LEN];
bound[..8].copy_from_slice(&42_u64.to_be_bytes());
bound[8..].copy_from_slice(&encode_socket_address("[::]:22026".parse().unwrap()));
assert_eq!(
decode_udp_bind_result(&bound).unwrap().handle,
HostSocketHandle(42)
);
assert_eq!(
decode_tcp_bind_result(&bound).unwrap().local_addr,
"[::]:22026".parse().unwrap()
);
}
}
+207
View File
@@ -0,0 +1,207 @@
use std::{
io,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
};
pub(crate) const SOCKET_ADDRESS_LEN: usize = 27;
pub(crate) const UDP_METADATA_LEN: usize = 48;
const V4_FAMILY: u8 = 4;
const V6_FAMILY: u8 = 6;
const ADDRESS_FAMILY: usize = 0;
const ADDRESS_BYTES: std::ops::Range<usize> = 1..17;
const PORT_BYTES: std::ops::Range<usize> = 17..19;
const FLOWINFO_BYTES: std::ops::Range<usize> = 19..23;
const SCOPE_ID_BYTES: std::ops::Range<usize> = 23..27;
const OPTIONAL_IP_FAMILY: usize = 27;
const OPTIONAL_IP_BYTES: std::ops::Range<usize> = 28..44;
const OPTIONAL_IFINDEX_BYTES: std::ops::Range<usize> = 44..48;
pub(crate) fn encode_udp_metadata(
peer_addr: SocketAddr,
optional_ip: Option<IpAddr>,
optional_ifindex: Option<u32>,
) -> [u8; UDP_METADATA_LEN] {
let mut wire = [0_u8; UDP_METADATA_LEN];
wire[..SOCKET_ADDRESS_LEN].copy_from_slice(&encode_socket_address(peer_addr));
match optional_ip {
None => {}
Some(IpAddr::V4(ip)) => {
wire[OPTIONAL_IP_FAMILY] = V4_FAMILY;
wire[OPTIONAL_IP_BYTES.start..OPTIONAL_IP_BYTES.start + 4]
.copy_from_slice(&ip.octets());
}
Some(IpAddr::V6(ip)) => {
wire[OPTIONAL_IP_FAMILY] = V6_FAMILY;
wire[OPTIONAL_IP_BYTES].copy_from_slice(&ip.octets());
}
}
if let Some(ifindex) = optional_ifindex {
wire[OPTIONAL_IFINDEX_BYTES].copy_from_slice(&ifindex.to_be_bytes());
}
wire
}
pub(crate) fn encode_socket_address(addr: SocketAddr) -> [u8; SOCKET_ADDRESS_LEN] {
let mut wire = [0_u8; SOCKET_ADDRESS_LEN];
match addr {
SocketAddr::V4(addr) => {
wire[ADDRESS_FAMILY] = V4_FAMILY;
wire[ADDRESS_BYTES.start..ADDRESS_BYTES.start + 4].copy_from_slice(&addr.ip().octets());
wire[PORT_BYTES].copy_from_slice(&addr.port().to_be_bytes());
}
SocketAddr::V6(addr) => {
wire[ADDRESS_FAMILY] = V6_FAMILY;
wire[ADDRESS_BYTES].copy_from_slice(&addr.ip().octets());
wire[PORT_BYTES].copy_from_slice(&addr.port().to_be_bytes());
wire[FLOWINFO_BYTES].copy_from_slice(&addr.flowinfo().to_be_bytes());
wire[SCOPE_ID_BYTES].copy_from_slice(&addr.scope_id().to_be_bytes());
}
}
wire
}
pub(crate) fn decode_udp_metadata(
wire: &[u8; UDP_METADATA_LEN],
) -> io::Result<(SocketAddr, Option<IpAddr>, Option<u32>)> {
let address = <[u8; SOCKET_ADDRESS_LEN]>::try_from(&wire[..SOCKET_ADDRESS_LEN]).unwrap();
let peer_addr = decode_socket_address(&address)?;
let optional_ip = match wire[OPTIONAL_IP_FAMILY] {
0 => {
require_zero(&wire[OPTIONAL_IP_BYTES], "absent optional IP")?;
require_zero(
&wire[OPTIONAL_IFINDEX_BYTES],
"absent optional IP interface index",
)?;
None
}
V4_FAMILY => {
require_zero(
&wire[OPTIONAL_IP_BYTES.start + 4..OPTIONAL_IP_BYTES.end],
"optional IPv4 padding",
)?;
require_zero(
&wire[OPTIONAL_IFINDEX_BYTES],
"optional IPv4 interface index",
)?;
Some(IpAddr::V4(Ipv4Addr::from(
<[u8; 4]>::try_from(&wire[OPTIONAL_IP_BYTES.start..OPTIONAL_IP_BYTES.start + 4])
.unwrap(),
)))
}
V6_FAMILY => Some(IpAddr::V6(Ipv6Addr::from(
<[u8; 16]>::try_from(&wire[OPTIONAL_IP_BYTES]).unwrap(),
))),
family => return Err(invalid_family("optional IP", family)),
};
let optional_ifindex =
match u32::from_be_bytes(wire[OPTIONAL_IFINDEX_BYTES].try_into().unwrap()) {
0 => None,
ifindex => Some(ifindex),
};
Ok((peer_addr, optional_ip, optional_ifindex))
}
pub(crate) fn decode_socket_address(wire: &[u8; SOCKET_ADDRESS_LEN]) -> io::Result<SocketAddr> {
let port = u16::from_be_bytes(wire[PORT_BYTES].try_into().unwrap());
match wire[ADDRESS_FAMILY] {
V4_FAMILY => {
require_zero(
&wire[ADDRESS_BYTES.start + 4..ADDRESS_BYTES.end],
"IPv4 padding",
)?;
require_zero(&wire[FLOWINFO_BYTES], "IPv4 flowinfo")?;
require_zero(&wire[SCOPE_ID_BYTES], "IPv4 scope ID")?;
Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::from(
<[u8; 4]>::try_from(&wire[ADDRESS_BYTES.start..ADDRESS_BYTES.start + 4])
.unwrap(),
),
port,
)))
}
V6_FAMILY => Ok(SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::from(<[u8; 16]>::try_from(&wire[ADDRESS_BYTES]).unwrap()),
port,
u32::from_be_bytes(wire[FLOWINFO_BYTES].try_into().unwrap()),
u32::from_be_bytes(wire[SCOPE_ID_BYTES].try_into().unwrap()),
))),
family => Err(invalid_family("peer address", family)),
}
}
fn require_zero(bytes: &[u8], field: &str) -> io::Result<()> {
if bytes.iter().all(|byte| *byte == 0) {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("host UDP metadata has nonzero {field}"),
))
}
}
fn invalid_family(field: &str, family: u8) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!("host UDP metadata has invalid {field} family {family}"),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_ipv4_address_and_optional_source() {
let peer = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 0, 2, 1), 11013));
let source = Some(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 2)));
let expected = [
0x04, 0xc0, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x2b, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
0xc6, 0x33, 0x64, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
assert_eq!(encode_udp_metadata(peer, source, None), expected);
assert_eq!(
decode_udp_metadata(&expected).unwrap(),
(peer, source, None)
);
}
#[test]
fn round_trips_ipv6_flow_scope_and_optional_destination() {
let peer = SocketAddr::V6(SocketAddrV6::new(
"2001:db8::1".parse().unwrap(),
22026,
7,
11,
));
let destination = Some(IpAddr::V6("2001:db8::2".parse().unwrap()));
assert_eq!(
decode_udp_metadata(&encode_udp_metadata(peer, destination, Some(17))).unwrap(),
(peer, destination, Some(17))
);
}
#[test]
fn rejects_noncanonical_or_unknown_families() {
let mut wire = encode_udp_metadata("192.0.2.1:11013".parse().unwrap(), None, None);
wire[ADDRESS_BYTES.start + 4] = 1;
assert!(decode_udp_metadata(&wire).is_err());
wire = encode_udp_metadata("192.0.2.1:11013".parse().unwrap(), None, None);
wire[OPTIONAL_IP_FAMILY] = 9;
assert!(decode_udp_metadata(&wire).is_err());
wire = encode_udp_metadata(
"192.0.2.1:11013".parse().unwrap(),
Some("192.0.2.2".parse().unwrap()),
None,
);
wire[OPTIONAL_IFINDEX_BYTES.end - 1] = 1;
assert!(decode_udp_metadata(&wire).is_err());
}
}