Reference RPC Implementation: Turmuxยถ
Overviewยถ
Turmux is the reference RPC implementation for Bunker-Centric Architecture. It sits on top of vsock and provides method routing, service registration, and a synchronous request/response model. All of the language bindings (Python, C/C++, Rust) are wrappers around the same Turmux core, ensuring consistent protocol behaviour regardless of the implementation language.
Turmux daemon implements a RPC Router that allows RPC calls between multiple Turmux RPC clients, connected together in a star topology network, where the daemon is the central node.
Each client can connect to the daemon and expose RPC services by registering its methods using a special RPC call implemented in the daemon. During normal operation, when the daemon receives an RPC request, it redirects the request to the client that has previously registered the corresponding method and it will forward back the response to the client that originated the RPC request.
Protocol Buffers are used for serialization. Every message has a typed schema defined in a .proto file. This guarantees that the data format is contractually enforced at both ends, and that adding new fields to a service message does not break existing clients.
Designยถ
The design is intentionally minimal: the daemon does nothing but accept connections, maintain a method registry, and forward framed protobuf messages. All service logic lives in the providers - separate processes or threads that register methods and respond to incoming requests. This separation means the daemon can be audited independently of any service, and a misbehaving service cannot crash the daemon or affect other connections.
Turmux is inspired by the design philosophy of Android Binder - a structured IPC mechanism with typed messages, method routing, and explicit capability registration - adapted for the Linux VM-to-VM context where vsock replaces the Binder kernel driver. Its footprint is small by design: the daemon binary is suitable for constrained embedded environments with limited RAM and storage.
Architectureยถ
vsock operates over the virtio-vsock kernel device. According to BCA, Open World runs as a guest VM and Bunker OS acts as the trusted host. Applications in Open World open vsock connections to the Turmux router running in the Core.
graph TD
subgraph ML["Open World โ Guest VM (Untrusted)"]
A1[App 1]
A2[App 2]
A3[App 3]
VSOCK["AF_VSOCK / SOCK_STREAM"]
A1 --> VSOCK
A2 --> VSOCK
A3 --> VSOCK
end
VSOCK -->|virtio-vsock| HV["virtio-vsock Transport (Hypervisor)"]
subgraph LC["Bunker OS โ Host VM (Trusted)"]
Router["Turmux RPC Router"]
Reg["Service Registry"]
Disp["Message Dispatcher"]
Svc["Core Services"]
S1[Logger]
S2[Storage]
S3[Monitor]
S4[Custom...]
HV --> Router
Router --> Reg
Router --> Disp
Router --> Svc
Svc --> S1
Svc --> S2
Svc --> S3
Svc --> S4
end
Traffic flows from guest applications down through the virtio device layer in the hypervisor and back up into the Coreโs network stack โ all without involving IP routing or a physical network interface.
The Turmux workspace is organised as a set of Rust crates, each with a single responsibility:
graph TD
subgraph Workspace["Turmux โ Cargo Workspace"]
Proto["turmux-proto โ Protobuf definitions + framing"]
SDK["turmux-sdk โ Rust client/provider SDK"]
Daemon["turmux-daemon โ Router"]
Python["turmux-python โ Python bindings (PyO3)"]
C["turmux-c โ C/C++ bindings (FFI)"]
Proto --> SDK
Proto --> Daemon
SDK --> Daemon
SDK --> Python
SDK --> C
end
turmux-proto defines the wire format. It contains the .proto files for Envelope, Request, and Response, along with the frame encoder/decoder and the size-validation logic. Every other crate in the workspace depends on this one.
turmux-sdk is the core Rust library. It exposes both a client API (connect and call a method) and a provider API (register a method and handle incoming requests). It manages connection state, serializes and deserializes frames, and is thread-safe through internal mutexes. Unix domain sockets and vsock are both supported as transports.
turmux-daemon is the standalone router binary. It listens for incoming connections, maintains the method registry, and dispatches requests to the registered provider for each method. It does not interpret message payloads โ it only reads the method field from the Request envelope and forwards the opaque payload bytes to whichever provider registered that method name.
turmux-python wraps the SDK using PyO3, providing a Python-native API. Both the caller and provider patterns are exposed as methods on a single TurmuxClient class. The vsock feature flag enables vsock transport at build time; without it, only Unix domain sockets are available, which is useful for CI environments without a hypervisor.
turmux-c exposes a minimal C API as a shared library (libturmux_c.so) and a static library (libturmux_c.a). All functions accept and return plain C types; protobuf serialization of the service-level payload is the callerโs responsibility. C++ applications can use the same header without any adaptation.
Protocol Specificationยถ
Message Envelopeยถ
All RPC messages are wrapped in an Envelope protobuf:
syntax = "proto3";
package turmux.v1;
message Envelope {
oneof body {
Request request = 1;
Response response = 2;
}
}
message Request {
string method = 1; // Full method name (e.g., "logger.store")
bytes payload = 2; // Service-specific message (opaque)
}
message Response {
int32 msg_id = 1; // Correlates with request
bool success = 2; // Success flag
string error = 3; // Error message (if success=false)
bytes payload = 4; // Response data (if success=true)
}
``$/register`` โ declares a method to the router. Once registered, any client that calls that method name will have its request forwarded to this connection. A single connection can register multiple methods, enabling a process to act as a multi-method service.
message RegisterRequest { string method_name = 1; }
message RegisterResponse { string status = 1; } // "registered"
``$/setMaxMsgSize`` โ negotiates a higher per-client message size limit. By default every connection is capped at 1 MiB per message. Services that transfer large binary payloads (model weights, file chunks, camera frames) should call this method immediately after connecting and before sending any large message. The router validates the new limit server-side, so clients cannot exceed what they explicitly negotiated.
message SetMaxMsgSizeRequest { uint64 max_size = 1; }
message SetMaxMsgSizeResponse { uint64 agreed_size = 1; }
``$/reset`` โ un-registers all methods provided by this connection without closing the socket. Useful for implementing graceful service restarts: a provider can clear its registry entry, drain in-flight requests, and re-register once the new service instance is ready.
message ResetRequest {}
message ResetResponse { uint32 methods_unregistered = 1; }
Full Message Flowยถ
The sequence below shows both the provider setup phase and a client calling a registered method. The router assigns a monotonically increasing msg_id to each request; the provider echoes this ID in its response, and the router uses it to route the response back to the correct caller. From the client perspective, call() is fully synchronous โ it blocks until the matching response arrives.
sequenceDiagram
participant C as Client
participant R as Router (turmux-daemon)
participant P as Provider
P->>R: connect()
P->>R: $/register { method_name: "my.method" }
R-->>P: RegisterResponse { status: "registered" }
C->>R: connect()
C->>R: Request { method: "my.method", payload: <request_bytes> }
R->>P: Request { method: "my.method", payload: <request_bytes> }
P-->>R: Response { msg_id: N, success: true, payload: <response_bytes> }
R-->>C: Response { msg_id: N, success: true, payload: <response_bytes> }
C->>R: disconnect()
P->>R: $/reset (optional graceful unregister)
Language Bindingsยถ
Python Bindingsยถ
The turmux-python crate provides Python bindings via PyO3.
Installationยถ
# Install from source
cd crates/turmux-python
maturin develop # Unix sockets only
maturin develop --features vsock # With Vsock support
API Referenceยถ
Header: turmux.h
import turmux_python
# Constructors
client = turmux_python.TurmuxClient(socket_path)
client = turmux_python.TurmuxClient.connect_unix(socket_path)
client = turmux_python.TurmuxClient.connect_vsock(cid=1, port=5555)
# Methods
client.register(method_name: str) -> None
client.call(method: str, payload: bytes) -> bytes
client.send_request(method: str, payload: bytes) -> int
client.read_request() -> RequestMessage
client.send_response(msg_id: int, payload: bytes) -> None
client.send_error(msg_id: int, error: str) -> None
client.read_response() -> ResponseMessage
Python Example: Ping Serviceยถ
Provider (server):
import turmux_python
from ping_pb2 import PingRequest, PingResponse
SOCKET_PATH = "/tmp/turmux.sock"
def main():
client = turmux_python.TurmuxClient(SOCKET_PATH)
client.register("ping")
print("Registered method: ping")
while True:
# Wait for incoming request
req = client.read_request()
print(f"Got request: method={req.method}, msg_id={req.msg_id}")
try:
# Deserialize request
ping_req = PingRequest()
ping_req.ParseFromString(req.payload)
# Create response
ping_resp = PingResponse(
number=ping_req.number,
flag=ping_req.flag,
)
# Send response
client.send_response(req.msg_id, ping_resp.SerializeToString())
except Exception as e:
client.send_error(req.msg_id, str(e))
if __name__ == "__main__":
main()
Client (caller):
import turmux_python
from ping_pb2 import PingRequest, PingResponse
SOCKET_PATH = "/tmp/turmux.sock"
def main():
client = turmux_python.TurmuxClient(SOCKET_PATH)
# Create request
ping_req = PingRequest(number=42, flag=True)
# Make RPC call
response_bytes = client.call("ping", ping_req.SerializeToString())
# Deserialize response
ping_resp = PingResponse()
ping_resp.ParseFromString(response_bytes)
print(f"Got response: number={ping_resp.number}, flag={ping_resp.flag}")
if __name__ == "__main__":
main()
Using Vsock (Linux VMs):
# Provider on Core (CID 1, port 5555)
client = turmux_python.TurmuxClient.connect_vsock(cid=1, port=5555)
client.register("ping")
# Client in Open World (connect to Core)
client = turmux_python.TurmuxClient.connect_vsock(cid=1, port=5555)
response = client.call("ping", request_bytes)
C/C++ Bindingsยถ
The turmux-c crate provides minimal C FFI bindings.
Buildยถ
# Build dynamic library
cd crates/turmux-c
cargo build --release # Unix sockets only
cargo build --features vsock --release # With Vsock
# Outputs:
# target/release/libturmux_c.so (Linux)
# target/release/libturmux_c.dylib (macOS)
# target/release/libturmux_c.a (Static)
API Referenceยถ
Class: TurmuxClient
// Opaque client handle
typedef struct turmux_client turmux_client_t;
// Connection
turmux_client_t* turmux_connect_default(); // /var/run/turmuxd.sock
turmux_client_t* turmux_connect(const char* path);
turmux_client_t* turmux_connect_vsock(uint32_t cid, uint32_t port);
void turmux_close(turmux_client_t* client);
// RPC Call (synchronous)
int turmux_call(
turmux_client_t* client,
const char* method,
const uint8_t* request,
uint32_t request_len,
uint8_t** response, // Output: allocated by library
uint32_t* response_len
);
// Provider (register method and handle requests)
int turmux_register(turmux_client_t* client, const char* method);
int turmux_read_request(
turmux_client_t* client,
char* method_buf, // Output method name
uint32_t method_buf_len,
uint8_t** payload, // Output: allocated by library
uint32_t* payload_len,
uint32_t* msg_id // Output: message ID for response
);
int turmux_send_response(
turmux_client_t* client,
uint32_t msg_id,
const uint8_t* response,
uint32_t response_len
);
int turmux_send_error(
turmux_client_t* client,
uint32_t msg_id,
const char* error
);
// Cleanup
void turmux_free(uint8_t* ptr);
// Return codes
#define TURMUX_OK 0
#define TURMUX_EREMOTE 1 // Service error
#define TURMUX_EINVAL 2 // Invalid argument
#define TURMUX_ECONNECT 3 // Connection failed
#define TURMUX_ETIMEOUT 4 // Request timeout
#define TURMUX_EMEM 5 // Memory error
C/C++ Exampleยถ
#include "turmux.h"
#include "ping.pb-c.h" // Generated protobuf
// Client example
int main() {
turmux_client_t* client = turmux_connect_default();
if (!client) return 1;
// Create request
Ping__PingRequest req = PING__PING_REQUEST__INIT;
req.number = 42;
req.flag = 1;
// Serialize
size_t req_len = ping__ping_request__get_packed_size(&req);
uint8_t* req_buf = malloc(req_len);
ping__ping_request__pack(&req, req_buf);
// Make call
uint8_t* resp_buf = NULL;
uint32_t resp_len = 0;
int rc = turmux_call(client, "ping", req_buf, req_len,
&resp_buf, &resp_len);
if (rc == TURMUX_OK) {
Ping__PingResponse* resp = ping__ping_response__unpack(
NULL, resp_len, resp_buf
);
printf("Got response: number=%u\n", resp->number);
ping__ping_response__free_unpacked(resp, NULL);
}
// Cleanup
turmux_free(resp_buf);
free(req_buf);
turmux_close(client);
return 0;
}
Rust SDKยถ
The native Rust SDK is in crates/turmux-sdk.
Usageยถ
use turmux_sdk::{client::Client, provider::Provider};
use std::path::Path;
// Client
#[tokio::main]
async fn main() -> Result<()> {
let mut client = Client::connect_unix("/tmp/turmux.sock").await?;
let request = PingRequest { number: 42, flag: true };
let response: PingResponse = client.call("ping", request).await?;
println!("Response: {:?}", response);
Ok(())
}
// Provider (service)
#[tokio::main]
async fn main() -> Result<()> {
let mut provider = Provider::listen_unix("/tmp/turmux.sock").await?;
provider.register("ping", |req: PingRequest| async {
Ok(PingResponse {
number: req.number,
flag: req.flag,
})
}).await?;
provider.serve().await?;
Ok(())
}
Writing Custom Servicesยถ
A custom service requires four steps: define the message schema in Protobuf, generate language-specific serialization code from that schema, implement the provider event loop, and wire the service into the Bunkers image (see Custom Service Development for the image integration step). The first two steps are pure protocol design; the third is where the actual service logic lives.
Step 1: Define Protocolยถ
Create a .proto file for your service messages. Using a versioned package name (e.g., mycompany.myservice.v1) makes it straightforward to evolve the API without breaking existing clients.
File: ``myservice.proto``
syntax = "proto3";
package mycompany.myservice.v1;
message GetStatusRequest {}
message GetStatusResponse {
string status = 1;
uint32 uptime_seconds = 2;
repeated string errors = 3;
}
message RebootRequest {
uint32 delay_seconds = 1;
}
message RebootResponse {
bool success = 1;
string message = 2;
}
Step 2: Generate Codeยถ
With the schema defined, use protoc or its language-specific wrappers to generate the serialization stubs. These generated files are the only protocol-coupling between the provider and the caller: both sides must be built from the same .proto file.
Python:
python -m grpc_tools.protoc \
-I. \
--python_out=. \
--pyi_out=. \
myservice.proto
# Generates: myservice_pb2.py, myservice_pb2.pyi
C:
protoc --c_out=. myservice.proto
# Generates: myservice.pb-c.c, myservice.pb-c.h
Step 3: Implement the Providerยถ
The provider connects to the router, registers its method names, and runs a blocking event loop. Each iteration reads the next incoming request, dispatches to the appropriate handler, and sends back either a response or an error. Always call send_error rather than silently dropping a request โ the caller will block indefinitely until a response arrives.
import turmux_python
import myservice_pb2
import time
class StatusService:
def __init__(self):
self.start_time = time.time()
def get_status(self, request_bytes):
"""Handle GetStatusRequest"""
request = myservice_pb2.GetStatusRequest()
request.ParseFromString(request_bytes)
uptime = int(time.time() - self.start_time)
response = myservice_pb2.GetStatusResponse(
status="running",
uptime_seconds=uptime,
errors=[] # No errors currently
)
return response.SerializeToString()
def reboot(self, request_bytes):
"""Handle RebootRequest"""
request = myservice_pb2.RebootRequest()
request.ParseFromString(request_bytes)
response = myservice_pb2.RebootResponse(
success=True,
message=f"Rebooting in {request.delay_seconds} seconds"
)
# Schedule reboot (simplified)
# os.system(f"shutdown -r +{request.delay_seconds}")
return response.SerializeToString()
def main():
service = StatusService()
client = turmux_python.TurmuxClient("/tmp/turmux.sock")
# Register methods
client.register("myservice.get_status")
client.register("myservice.reboot")
print("Service registered")
# Event loop
while True:
req = client.read_request()
print(f"Handling: {req.method}")
try:
if req.method == "myservice.get_status":
response = service.get_status(req.payload)
elif req.method == "myservice.reboot":
response = service.reboot(req.payload)
else:
raise ValueError(f"Unknown method: {req.method}")
client.send_response(req.msg_id, response)
except Exception as e:
print(f"Error: {e}")
client.send_error(req.msg_id, str(e))
if __name__ == "__main__":
main()
C Implementation:
#include "turmux.h"
#include "myservice.pb-c.h"
#include <time.h>
time_t start_time;
void handle_get_status(turmux_client_t* client, uint32_t msg_id) {
time_t uptime = time(NULL) - start_time;
Mycompany__Myservice__V1__GetStatusResponse resp =
MYCOMPANY__MYSERVICE__V1__GET_STATUS_RESPONSE__INIT;
resp.status = "running";
resp.uptime_seconds = (uint32_t)uptime;
size_t resp_len =
mycompany__myservice__v1__get_status_response__get_packed_size(&resp);
uint8_t* resp_buf = malloc(resp_len);
mycompany__myservice__v1__get_status_response__pack(&resp, resp_buf);
turmux_send_response(client, msg_id, resp_buf, resp_len);
free(resp_buf);
}
int main() {
start_time = time(NULL);
turmux_client_t* client = turmux_connect_default();
turmux_register(client, "myservice.get_status");
turmux_register(client, "myservice.reboot");
while (1) {
char method[256];
uint8_t* payload = NULL;
uint32_t payload_len = 0;
uint32_t msg_id = 0;
int rc = turmux_read_request(client, method, sizeof(method),
&payload, &payload_len, &msg_id);
if (rc != TURMUX_OK) continue;
if (strcmp(method, "myservice.get_status") == 0) {
handle_get_status(client, msg_id);
}
turmux_free(payload);
}
turmux_close(client);
return 0;
}
Step 4: Testยถ
Run the daemon, provider, and caller in three separate terminals. The daemon must be started first; providers and callers can connect in any order after that.
Terminal 1: start the router
python provider.py
# Output: Service registered
Terminal 2: Call service
python client.py
# Output:
# Status: running
# Uptime: 42 seconds
Examples & Servicesยถ
Ping (crates/turmux-python/examples/ping/) is the canonical minimal example. It covers the full round-trip over both Unix sockets and vsock, shows how to handle errors gracefully, and has a documented README that explains the transport differences. This is the right starting point for anyone writing their first Turmux service.
TFLite Inference demonstrates a machine-learning inference service. It loads a TensorFlow Lite model at startup and exposes an inference method over RPC. The reference implementation takes advantage on $/setMaxMsgSize to negotiate a large enough limit for transferring model inputs and outputs.
AuthFS is a file system service implemented directly in Rust using the native SDK.
Securityยถ
DoS Preventionยถ
The default 1 MiB per-message limit means a rogue client cannot exhaust the routerโs memory by sending arbitrarily large messages. Clients that need larger limits must negotiate them explicitly with $/setMaxMsgSize, and the server applies the limit before any allocation occurs. If a frame header announces a size larger than the negotiated limit, the connection is dropped before the payload is read.
Method Registration and Isolationยถ
The router only dispatches a request to a provider that explicitly registered the corresponding method. A provider cannot intercept requests for methods it did not register, and two providers cannot register the same method name โ the second registration is rejected. This prevents accidental or malicious method shadowing between services.
Connection State Isolationยถ
Every connection maintains independent state: its own method registry entries, its own message-size limit, and its own I/O buffers. A provider that crashes or sends malformed data affects only its own connection; other providers and callers continue operating normally. The router enforces resource cleanup on disconnection, so there are no dangling registry entries or buffer leaks after a connection closes.
Debuggingยถ
Enable verbose logging with the RUST_LOG environment variable when running the daemon:
RUST_LOG=turmux_sdk=debug,turmux_daemon=debug cargo run -p turmux-daemon
For Python providers and callers, standard logging output can be combined with the daemon logs to trace the full request lifecycle.
Performance Tipsยถ
Where the calling pattern allows it, batching multiple operations into a single request reduces round-trips and serialization overhead. Keep individual message payloads as small as possible; if a service naturally deals in large data, consider streaming it in chunks rather than as a single giant payload. Set request timeouts on the caller side so that a stalled provider cannot block a caller indefinitely.
Whatโs Nextยถ
For a lower-level understanding of vsock and framing, see Communication Layer (vsock).
For integrating Turmux services into Bunkers, see Custom Service Development and Services API Reference.