A production-grade Rust implementation of the 9P2000.L protocol — the Linux-specific dialect of the Plan 9 file protocol.
The suite provides both a client and server, each with dual I/O backends (synchronous std::net and asynchronous tokio), structured as a Cargo workspace of six crates. It enables a mountable 9P file server usable from Linux guests (via v9fs) against a macOS host filesystem, plus a client library for programmatic 9P access.
| Crate | Description |
|---|---|
| ninep-proto | Wire protocol types, serialization, and error mapping. All 29 T/R message pairs with zero-copy encode/decode via bytes::Bytes. |
| ninep-client | Sync and async 9P2000.L client with tag multiplexer and FID allocator. |
| ninep-server | Sync and async 9P2000.L multi-client server with pluggable Filesystem / AsyncFilesystem traits. |
| ninep-hostfs | Host filesystem provider (HostFs / AsyncHostFs) that maps 9P operations to OS syscalls. |
| ninep-serve | CLI tool to export a local directory over 9P2000.L (sync and async modes). |
| ninep-uniffi | Cross-language FFI bindings via Mozilla UniFFI (Swift, Kotlin, Python). |
┌─────────────────────────────────────────────────┐
│ ninep-uniffi │ UniFFI bindings (Swift/Kotlin/Python)
└────────────┬───────────────────┬────────────────┘
│ │
┌───────▼───────┐ ┌───────▼───────┐
│ ninep-client │ │ ninep-serve │ CLI binary
│ (sync/async) │ │ (sync/async) │
└───────┬────────┘ └───────┬────────┘
│ │
│ ┌───────▼───────┐
│ │ ninep-hostfs │ HostFs / AsyncHostFs
│ └───────┬────────┘
│ │
│ ┌───────▼───────┐
│ │ ninep-server │ Pluggable Filesystem traits
│ │ (sync/async) │
│ └───────┬────────┘
│ │
┌───▼───────────────────▼───┐
│ ninep-proto │ Wire protocol (messages, codec, qid, stat)
└───────────────────────────┘
- Zero-copy I/O: Read/write payloads use
bytes::Bytesfor zero-copy slicing of receive buffers. - Dual I/O backends: Every component has both a
std::net(blocking) andtokio(async) variant, gated behind thetokioCargo feature. - Thread-safe state: FID maps use
DashMapfor lock-free concurrent access; tag allocation is TOCTOU-free viaDashMap::entry(). - Pluggable filesystems: The server's
Filesystem/AsyncFilesystemtraits let you swap in any backend —ninep-hostfsprovidesHostFs/AsyncHostFswhich maps to OS syscalls. - macOS portability:
Tgetattrmetadata anderrnovalues are translated between macOS and Linux representations.
- Rust 1.75+
- For async features: tokio 1.x
cargo build --workspaceStart a sync server exporting a directory:
mkdir -p /tmp/shared
cargo run --example unpfs_server -- /tmp/sharedConnect with the sync client:
cargo run --example basic_client -- 127.0.0.1:5640 /tmp/sharedStart an async server:
mkdir -p /tmp/shared
cargo run --example async_server -- /tmp/sharedConnect with the async client:
cargo run --example async_client -- 127.0.0.1:5640 /tmp/sharedThe primary use case is mounting a macOS directory from within a Linux guest (VM, container, etc.) using the kernel's built-in v9fs client.
sudo mount -t 9p -o trans=tcp,port=5640,version=9p2000.L,msize=8192 \
<host-ip> /mnt/9p| Option | Description |
|---|---|
trans=tcp |
Use TCP transport |
port=5640 |
Server port (default 9P port is 564) |
version=9p2000.L |
Protocol version (must be 9p2000.L) |
msize=8192 |
Maximum message size in bytes |
cache=none |
Disable client-side caching (safest for shared access) |
cache=loose |
Loose caching (better performance, eventual consistency) |
access=user |
Per-user access control |
uname=nobody |
User name for the 9P attach |
When running a Linux VM with QEMU, you can use virtio-9p for shared folders:
# Start the 9P server on the host
cargo run --example unpfs_server -- /path/to/shared
# In QEMU, forward port 5640 and mount inside the guest
# Or use QEMU's built-in 9pfs passthrough instead:
qemu-system-x86_64 \
-virtfs local,path=/path/to/shared,mount_tag=hostshare,security_model=mapped-xattr \
...
# Inside the guest:
sudo mount -t 9p -o trans=virtio,version=9p2000.L hostshare /mnt/shareduse ninep_client::{NineClient, SyncNineClient, TcpTransport};
use ninep_proto::wire::NOFID;
let transport = TcpTransport::connect("127.0.0.1:5640")?;
let mut client = SyncNineClient::new(Box::new(transport));
// Handshake
let msize = client.version(8192)?;
// Attach to root
let root_fid = 0;
client.attach(root_fid, NOFID, "nobody", "/tmp/shared", u32::MAX)?;
// Walk to a file
let (file_fid, _) = client.walk_auto(root_fid, &["myfile.txt".into()])?;
// Open and read
client.open(file_fid, 0)?;
let data = client.read(file_fid, 0, 4096)?;
// Cleanup
client.clunk(file_fid)?;
client.clunk(root_fid)?;
client.shutdown()?;use ninep_client::{AsyncNineClient, AsyncTcpTransport};
use ninep_proto::wire::NOFID;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let transport = AsyncTcpTransport::connect("127.0.0.1:5640").await?;
let mut client = AsyncNineClient::new(transport);
let msize = client.version(8192).await?;
client.attach(0, NOFID, "nobody", "/tmp/shared", u32::MAX).await?;
let (fid, _) = client.walk_auto(0, &["myfile.txt".into()]).await?;
client.open(fid, 0).await?;
let data = client.read(fid, 0, 4096).await?;
client.clunk(fid).await?;
client.shutdown().await?;
Ok(())
}use std::sync::Arc;
use std::sync::atomic::Ordering;
use ninep_server::{NineServer, Filesystem, new_shutdown_token};
use ninep_hostfs::HostFs;
let shutdown = new_shutdown_token();
let listener = std::net::TcpListener::bind("127.0.0.1:5640")?;
let mut server = NineServer::new(shutdown.clone());
// serve_tcp blocks until shutdown
server.serve_tcp(listener, || Arc::new(HostFs::new(8192)) as Arc<dyn Filesystem>)?;use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use ninep_server::{AsyncNineServer, AsyncFilesystem};
use ninep_hostfs::AsyncHostFs;
let cancel = CancellationToken::new();
let listener = tokio::net::TcpListener::bind("127.0.0.1:5640").await?;
let server = AsyncNineServer::new(cancel.clone());
server.serve_tcp(listener, || {
Arc::new(AsyncHostFs::new(8192)) as Arc<dyn AsyncFilesystem>
}).await?;Generate the full API documentation:
cargo doc --workspace --no-deps --open| Crate | Feature | Description |
|---|---|---|
| ninep-client | tokio |
Enable async client (AsyncNineClient, AsyncTcpTransport) |
| ninep-server | tokio |
Enable async server (AsyncNineServer, AsyncFilesystem) |
| ninep-hostfs | tokio |
Enable async host filesystem provider (AsyncHostFs) |
All three crates default to sync-only. Enable the tokio feature for async support:
[dependencies]
ninep-client = { path = "crates/ninep-client", features = ["tokio"] }
ninep-server = { path = "crates/ninep-server", features = ["tokio"] }
ninep-hostfs = { path = "crates/ninep-hostfs", features = ["tokio"] }This project is dual-licensed:
- Open Source — GNU Affero General Public License v3.0 (AGPL-3.0)
- Commercial — A proprietary commercial license is available for use cases where the AGPL is not suitable.