go9p is a Go implementation of the 9P2000 protocol (Plan 9 file protocol). It contains:
- Protocol definitions + packing/unpacking in
p/(package p) - A client in
p/clnt(package clnt) - A server framework in
p/srv(package srv) - A reference Unix filesystem server in
p/srv/ufs(package ufs) - Small example programs in
p/clnt/examplesandp/srv/examples
This repository is the upstream github.com/lionkov/go9p.
- Protocol: 9P2000 with optional 9P2000.u fields (see
Dotuusage in server/client code). - Go: This forked branch adds a Go module and is intended to work with modern Go toolchains.
- 9P2000: the “base” protocol.
- 9P2000.u: an extension that adds (among other things) numeric uid/gid fields and Unix-y metadata (see
Dir.Uidnum,Dir.Gidnum, and related fields inpackage p).
In this codebase you’ll see a boolean called Dotu on both client and server types. In practice:
- Server:
Srv.Dotuindicates the server can speak 9P2000.u. - Client:
Clnt.Dotuindicates the client wants to speak 9P2000.u. - The negotiated connection behavior is exposed as
Conn.Dotu(server side) based on theTversion/Rversionhandshake.
If you’re targeting the Linux kernel 9p client, it most commonly uses the 9p2000.L family (a different dialect from 9P2000.u). This repository’s code supports 9P2000 and 9P2000.u; the QEMU kernel-client harness in this fork validates kernel-client behavior against QEMU’s virtio-9p server rather than validating dialect parity with go9p itself.
This repository is now module-enabled:
go get github.com/lionkov/go9p@latestThe UFS server exports a local directory tree over 9P:
go run ./p/srv/examples/ufs -addr 127.0.0.1:5640List files from a 9P server:
go run ./p/clnt/examples/ls -addr <network address>The example programs have their own flags; run them with -h to see usage.
In one terminal, run a server exporting a local directory tree:
go run ./p/srv/examples/ufs -addr 127.0.0.1:5640 -root .In another terminal, list the root directory via 9P:
go run ./p/clnt/examples/ls -addr 127.0.0.1:5640 /Expected output is one name per line, for example:
.git
LICENSE
p
README.md
More detailed documentation for the example programs lives alongside the code:
- Server examples:
p/srv/examples/README.md - Client examples:
p/clnt/examples/README.md
This repo includes netfs, a synthetic filesystem that models a small (and still evolving) subset of
Plan 9’s /net interfaces (ip(3), ether(3), bridge(3)) using go9p under Linux.
It provides a working /net/tcp conversation interface (clone + per-connection ctl/data), plus
additional stubbed entry points you can expand.
Run it:
go run ./p/srv/examples/netfs -addr 127.0.0.1:5640This repo’s p/srv package lets you serve a synthetic filesystem by building a tree of srv.File
nodes and attaching behavior to nodes by implementing methods (e.g. Read, Write, Create, Wstat).
If you’re looking for working reference implementations, see:
p/srv/examples/ramfs(in-memory file tree withCreate+Wstat)p/srv/examples/timefs(read-only, synthetic time files)p/srv/examples/clonefs(Plan 9 “clone” control-file pattern)
Most synthetic servers embed srv.File into a custom type so the type can both:
- carry state (bytes, counters, timestamps, config), and
- implement node operations (read/write/create/wstat/etc.).
Example sketch:
type MyFile struct {
srv.File
data []byte
}If you need a directory that can create children dynamically, you typically use the same pattern but implement Create.
You construct a tree of nodes by calling Add(parent, name, user, group, perm, impl).
parent:nilmeans “this is the root”perm: includes file bits and optionallyp.DMDIRfor directoriesimpl: usuallyyourNode(the receiver implementing methods); can benilfor a “plain” node
Example tree:
/
├── hello (read/write)
└── ctl (control file; write triggers side effects)
In code (high-level sketch):
root := new(srv.File)
_ = root.Add(nil, "/", user, nil, p.DMDIR|0555, nil)
hello := new(MyFile)
_ = hello.Add(root, "hello", user, nil, 0666, hello)The common methods you’ll implement (depending on your needs):
Read(fid, buf, offset): fillbufwith bytes starting atoffset, returnn.- Design choice: return EOF via
n=0,nil(common) vs a real error (rare). - Must respect
offsetfor “normal” files; for “streaming” files you may intentionally ignore it.
- Design choice: return EOF via
Write(fid, data, offset): store bytes atoffset, returnn.- For in-memory files, this usually means “grow to
offset+len(data)then copy”.
- For in-memory files, this usually means “grow to
Create(fid, name, perm)(directories): create a child node andAddit under the directory.- Pattern: directories are themselves nodes;
Createis implemented on the directory node type.
- Pattern: directories are themselves nodes;
Wstat(fid, dir): apply metadata changes (rename, chmod, truncate, uid/gid).- Typical minimal subset:
dir.Name→ renamedir.Mode→ chmod (permissions)dir.Length→ truncate
- Typical minimal subset:
Remove(fid): handle unlink semantics.- Many examples stub this out; production servers usually remove the node from its parent.
The easiest way to get this right is to mimic the example servers and add functionality incrementally.
Expose configuration or commands via a file whose writes trigger side effects.
- Read: show current config/state (
"debug=1\nmsize=8192\n"). - Write: parse commands (
"debug=0\n","reset\n","mk foo\n").
This maps well to Plan 9 style interfaces and keeps the surface small.
This is useful for session allocation (think: allocate a new channel/endpoint).
- Reading
/cloneat offset 0 returns a freshly allocated name. - The server creates a new child node named with a unique id.
See p/srv/examples/clonefs and the notes in p/srv/examples/README.md.
If a file logically represents a stream (time, random bytes, logs), you may choose to:
- ignore
offset, and/or - never return EOF.
Be explicit in documentation because some clients/tools read until EOF.
See timefs’s /inftime.
For large files, using fixed-size blocks can be simpler than continuously resizing a single byte slice.
- lazily allocate blocks on first write
- treat missing blocks as zero-filled
- maintain a separate authoritative length
See ramfs for a working example.
Synthetic servers often implement just enough metadata to satisfy clients:
- store mode bits on the node (
File.Mode) - treat truncation as data resize
- implement rename as changing a node’s name in its parent
Be clear about what you do not implement (ACLs, xattrs, hardlinks, etc.).
Once you have a root node, you create and start an Fsrv:
srv := srv.NewFileSrv(root)
srv.Dotu = true // enable 9P2000.u fields when clients request it
srv.Start(srv)
_ = srv.StartNetListener("tcp", "127.0.0.1:5640")Notes:
Dotu: set this based on whether you want to support 9P2000.u extensions (numeric ids, Unix-y metadata).- Concurrency: reads/writes may happen concurrently; guard mutable node state with a mutex where appropriate.
- Permissions: examples are permissive; real servers should validate users/groups and enforce access checks.
go test ./...Run the full test suite under Linux from macOS/Windows:
docker build -t go9p:test --target test .
docker run --rm go9p:testOptional race run:
docker build -t go9p:race --target race .
docker run --rm go9p:raceThis boots an upstream Linux kernel in QEMU, mounts a virtio-9p export using the kernel 9p client, and runs a smoke test against that mount.
docker build -f Dockerfile.kernel9p-qemu --target kernel9p-test .Pin kernel version and/or architecture:
docker build -f Dockerfile.kernel9p-qemu --target kernel9p-test \
--build-arg LINUX_VERSION=7.0 \
--build-arg KERNEL_ARCH=amd64 \
.This fork also includes a github.com/v9fs/test-style harness that runs inside the prebuilt
ghcr.io/v9fs/docker:v2.0.0 image (no custom Dockerfile) and uses a u-root initrd + chroot flow:
docker run --rm --privileged --platform linux/arm64 \
-v "$PWD:/opt/v9fs/go9p" -w /opt/v9fs/go9p \
ghcr.io/v9fs/docker:v2.0.0 \
bash /opt/v9fs/go9p/scripts/v9fs/ci-e2e-fs.sh ufsYou can also run the kernel-client harness against other example servers:
docker run --rm --privileged --platform linux/arm64 \
-v "$PWD:/opt/v9fs/go9p" -w /opt/v9fs/go9p \
ghcr.io/v9fs/docker:v2.0.0 \
bash /opt/v9fs/go9p/scripts/v9fs/ci-e2e-fs.sh ramfsSupported ci-e2e-fs.sh filesystem arguments: ufs, ramfs, clonefs, timefs, netfs.
Note: tlsramfs is TLS-only and is exercised via userspace (Go) tests rather than a Linux kernel mount.
p/: core protocol + helpers (package p)p/clnt/: client implementationp/srv/: server frameworkp/srv/ufs/: Unix filesystem servercmd/kernel9p-smoke/: guest-side smoke test used by the QEMU kernel-client harness
BSD-style license; see LICENSE.