feat(wasi): expose protobuf RPC request ABI - #2477
Conversation
Add an instance-scoped asynchronous RPC session backed by the shared operation broker. Reuse the existing dispatcher and management handlers. WASI hosts can call PeerManageRpc and ConnectorManageRpc with the same protobuf payloads as easytier-cli. Export ABI version, submit, take, and free functions. Bind selectors to the WASM instance handle and keep method errors in RpcResponse. Enable management RPC explicitly in the Go-host WASM build.
Serialize UDP client admission across forwarding rules so only one eviction can claim and wait for a released semaphore permit. Retry when cleanup concurrently removes the selected client. Add a multithreaded regression test for the permit handoff while the evicted client is still referenced.
Hold the admission guard through client and response-task publication so a concurrent eviction cannot leave an orphan task holding the slot permit. Open the data-plane flow before entering the critical section and extend the multithreaded regression test across the publication window.
Register PeerManageRpc, ConnectorManageRpc, and ConfigRpc in the WASI WebClient registry so config-server requests reach host forwarding. Share descriptor-based forwarding across the supported services and add a regression test for registration and canonical method names.
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This PR adds a versioned, transport-neutral protobuf RPC ABI for WASI core instances, and moves WASI “web client” management calls onto host socket/management adapters while improving UDP port-forward robustness.
Changes:
- Introduces
DirectRpcRequest/RpcResponse-based RPC submission & response-taking guest exports for WASI. - Adds a WASI WebClient runtime backed by host-provided management/socket adapters and forwards instance-management RPC.
- Bounds UDP port-forward clients and adds regression tests for max UDP payload and concurrent admission/eviction behavior.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| script/build-wasi-core.sh | Enables management-rpc feature in the WASI core build script. |
| easytier-proto/proto/common.proto | Adds transport-neutral DirectRpcRequest and HostManagementRequest messages. |
| easytier-proto/proto/api_config.proto | Extends GetConfigResponse with toml_config. |
| easytier-core/src/wasi/web_client.rs | Adds WASI WebClient runtime that forwards management calls to the host. |
| easytier-core/src/wasi/schema.rs | Adds versioned WASI WebClient create-config schema + validation. |
| easytier-core/src/wasi/runtime/abi/web_client.rs | Exposes WebClient lifecycle ABI exports for WASI. |
| easytier-core/src/wasi/runtime/abi/rpc.rs | Adds WASI instance-scoped async RPC request/response ABI exports. |
| easytier-core/src/wasi/runtime.rs | Wires instance runtime host + hooks in management RPC operation session. |
| easytier-core/src/wasi/mod.rs | Exposes WASI web_client module behind management on WASI. |
| easytier-core/src/wasi/imports.rs | Adds host imports for process-level management calls (feature-gated). |
| easytier-core/src/wasi/adapter/mod.rs | Registers WASI management adapter module behind management. |
| easytier-core/src/wasi/adapter/management.rs | Implements host management I/O via WASI imports. |
| easytier-core/src/wasi/abi.rs | Adds ABI versions and export lists for WebClient + RPC ABI. |
| easytier-core/src/rpc/service_registry.rs | Adds method-name resolution support for direct RPC dispatch. |
| easytier-core/src/rpc/server.rs | Refactors server dispatch via shared dispatcher and fixes accepted compression reporting. |
| easytier-core/src/rpc/packet.rs | Adds accepted_compression_algo() helper and updates compression tests. |
| easytier-core/src/rpc/operation.rs | Implements async local RPC operations backed by the operation broker. |
| easytier-core/src/rpc/mod.rs | Wires new dispatch and operation modules. |
| easytier-core/src/rpc/dispatch.rs | Introduces transport-neutral request dispatch helpers. |
| easytier-core/src/rpc/client.rs | Uses accepted_compression_algo() for compression negotiation. |
| easytier-core/src/management/mod.rs | Adds forwarded RPC module and WASI-bound management RPC registration. |
| easytier-core/src/management/instance_rpc/mod.rs | Refactors instance resolution behind a resolver trait; adds bound-instance validation tests. |
| easytier-core/src/management/instance_rpc/full.rs | Extends ConfigRpc GetConfig to also return TOML. |
| easytier-core/src/management/full/web_client.rs | Splits WebClient backend behind a trait; allows WASI host-backed backend. |
| easytier-core/src/management/full/mod.rs | Re-exports WebClientBackend for WASI builds. |
| easytier-core/src/management/forwarded_rpc.rs | Implements forwarding handlers for bound management RPC services. |
| easytier-core/src/host/mod.rs | Adds host management module behind management. |
| easytier-core/src/host/management.rs | Adds host management client abstraction built on HostSocketRuntime ops. |
| easytier-core/src/gateway/port_forward.rs | Bounds UDP clients with semaphore + serialized eviction/handoff; adds concurrency tests. |
| easytier-core/src/gateway/dataplane/tests.rs | Adds regression test for maximum IPv4 UDP payload carriage. |
| easytier-core/src/gateway/dataplane/stack.rs | Configures smoltcp UDP TX buffers/meta sizes. |
| easytier-core/src/foundation/mod.rs | Enables operation broker for WASI management-rpc builds. |
| easytier-core/Cargo.toml | Increases smoltcp fragmentation/reassembly buffer sizes to support large payloads. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let response = tokio::select! { | ||
| _ = cancellation.cancelled() => None, | ||
| result = async { | ||
| let descriptor = registry | ||
| .resolve_method("", &request.full_method_name) | ||
| .ok_or_else(|| { | ||
| error::Error::InvalidServiceKey( | ||
| request.full_method_name.clone(), | ||
| String::new(), | ||
| ) | ||
| })?; | ||
| dispatch_payload( | ||
| registry.as_ref(), | ||
| descriptor, | ||
| request.request.into(), | ||
| request | ||
| .timeout_ms | ||
| .map(std::time::Duration::from_millis), | ||
| None, | ||
| ) | ||
| .await | ||
| } => { | ||
| Some(encode_response( | ||
| result, | ||
| started.elapsed().as_micros() as u64, | ||
| max_response_len, | ||
| )) | ||
| } | ||
| }; | ||
| let mut state = state.lock().unwrap_or_else(|error| error.into_inner()); | ||
| state | ||
| .broker | ||
| .complete_with(operation_id.0, |_, _| response.unwrap_or_default()); | ||
| }); |
There was a problem hiding this comment.
This cancellation path cannot expose an empty response. free() moves a pending broker entry to Discarding before cancelling its token, while discard_all() removes the entry entirely. When the spawned task later calls complete_with, the broker either absorbs that late completion in Discarding or finds the operation missing, so the closure containing unwrap_or_default() is never invoked and no response can be read. The pending-operation cancellation test covers this lifecycle.
| #[tokio::test] | ||
| async fn compression_negotiation_falls_back_when_zstd_is_unavailable() { | ||
| assert_eq!(accepted_compression_algo(), CompressionAlgoPb::None); | ||
|
|
||
| let (content, algorithm) = compress_packet(CompressionAlgoPb::Zstd, b"rpc body") | ||
| .await | ||
| .unwrap(); |
There was a problem hiding this comment.
This assertion is already compiled only under #[cfg(all(test, not(feature = "zstd")))]. With Zstd enabled the entire test module is excluded, so the assertion cannot fail on a Zstd-capable build.
| dispatch_payload( | ||
| registry, | ||
| descriptor, | ||
| Bytes::from(request.request), | ||
| Some(Duration::from_millis(request.timeout_ms as u64)), | ||
| tunnel_info, | ||
| ) | ||
| .await |
There was a problem hiding this comment.
This is a behavior-preserving extraction from the existing server implementation on main, which already converts RpcRequest.timeout_ms with as u64; the client has the same existing conversion. The new ABI request introduced here uses optional uint64 timeout_ms and does not enter this path. Rejecting negative legacy RpcRequest values is a valid protocol-hardening follow-up, but changing existing wire behavior is outside this ABI patch.
Summary
Testing