p9

package module
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 5 Imported by: 10

README

p9

CI Release Go Reference

Go Plan 9 9P2000 9P2000.u Filesystem Network Protocol

A pure-Go implementation of the Plan 9 filesystem protocol, 9P2000 (the original 1992 spec), with no dependencies beyond the standard library. Symlinks are also supported as an opt-in 9P2000.u extension — see Symlinks (9P2000.u) below; the .L (Linux-specific) extension isn't implemented.

go get github.com/sandgorgon/9p

Packages

Package What it is
p9 Wire encoding: message types, Marshal/Unmarshal, Qid, Stat, Mode. No I/O policy. Optionally speaks 9P2000.u (VersionU) for symlinks.
p9/client A 9P2000 client: dial or wrap a connection, Attach, Walk, Open, and a File implementing io.Reader/Writer/ReaderAt/WriterAt/Seeker/Closer. WithUnixExtensions() opts into 9P2000.u and Symlink.
p9/server A 9P2000 server: given a small FileSystem/File backend interface, handles wire encoding, fid bookkeeping, walk batching, and Tflush cancellation. A backend opts into symlink support via the optional SymlinkFile interface.
p9/ns A Plan 9 style namespace: a bind tree (BindFS, BindPath, Unbind) with before/after/replace union directories, read-only binds, and Clone for a private copy. A Namespace is itself a server.FileSystem, so a service can serve its own namespace and graft other 9P services into it with FromFid. See Namespaces.
p9/examples/memfs An in-memory server.FileSystem — a demo backend and the server package's own test fixture. Implements SymlinkFile.
p9/examples/dirfs A server.FileSystem that exports a real local directory tree, with every path resolved through an os.Root so it can't escape the configured root even via a symlink. Implements SymlinkFile.
p9/cmd/9ps Serves a directory (or an empty in-memory filesystem with -mem) over TCP.
p9/cmd/9pc A bare CLI client: ls, cat, get, put.

Quick start

Serve a directory:

fs, err := dirfs.New("/path/to/export")
srv := &server.Server{FS: fs}
l, _ := net.Listen("tcp", ":5640")
srv.Serve(l)

or from the command line:

go run ./cmd/9ps -addr :5640 -root /path/to/export
go run ./cmd/9pc -addr localhost:5640 ls /
go run ./cmd/9pc -addr localhost:5640 cat /some/file
go run ./cmd/9pc -addr localhost:5640 get /some/file ./local-copy
go run ./cmd/9pc -addr localhost:5640 put ./local-file /remote-name

Talk to a server as a client:

c, err := client.Dial("tcp", "localhost:5640")
defer c.Close()

if _, err := c.Attach("glenda", ""); err != nil {
    log.Fatal(err)
}

f, err := c.Open("/some/file", p9.OREAD)
defer f.Close()
io.Copy(os.Stdout, f)

Writing a server backend

A backend only implements two small interfaces — server.FileSystem (one method, Attach) and server.File (walk to a child, open, read/write at an offset, stat, remove). The server package handles everything wire-related: message framing, fid allocation and lifecycle, splitting long walks into ≤16-element Twalk batches, and running each request in its own goroutine so a Tflush can cancel one that's still in flight. See examples/memfs for the smallest complete implementation, or examples/dirfs for one backed by real files.

Namespaces

p9/ns lets a 9P service compose its file tree from other services, the way a Plan 9 process builds its namespace with bind and mount. There is no FUSE or OS mount table involved; the tree lives in the process and is exposed by serving it.

n := ns.New(ns.WithUser("app"))

// The app's own files.
n.BindFS(ownFS, "", "/", ns.Replace)

// Graft another 9P service in: dial it, attach, bind its root.
c, _ := client.Dial("tcp", "peer:5640")
root, _ := c.Attach("app", "")
n.BindFS(ns.FromFid(root), "", "/mnt/peer", ns.Replace)

// Layer one directory over another (a union), then serve the lot.
n.BindPath(ctx, []string{"/mnt/peer/etc"}, "/etc", ns.After)
(&server.Server{FS: n}).Serve(l)

Clients of the app now see its own files and the peer's under one tree. Binds, Log and Resolve report what is bound where and what serves a path. Clone gives a private copy for a sandboxed block of work. .. is not supported while still inside the bind tree itself (different bound trees can disagree about what "up" means); inside a bound filesystem it is that filesystem's own business.

Plain 9P2000 has no concept of a symlink. Support for one is an opt-in extension, negotiated at connection time:

c, err := client.NewClient(conn, client.WithUnixExtensions())

If the server doesn't understand 9P2000.u, NewClient falls back to plain 9P2000 automatically — every other client call behaves exactly as it does today. Once negotiated:

qid, err := c.Symlink("/some/link", "target/path")

Client.Symlink/Fid.Symlink create one; a symlink's target comes back in Stat.Extension (Qid.IsSymlink()/Mode.IsSymlink() report its type). A symlink is never followed automatically — Stat or Walk it to discover it, don't Open it directly.

A backend adds symlink support by implementing the optional server.SymlinkFile interface (server.File plus one more method, Symlink) — examples/dirfs and examples/memfs both do. A server.File that doesn't implement it just can't have symlinks created under it; nothing else about it needs to change.

Known limitations

  • No authentication: Tauth always fails with "authentication not required," and the client always attaches with NOFID. Servers that require an auth handshake before attach aren't supported.
  • 9P2000.u support is limited to symlinks and the numeric uid/gid/muid fields on Stat — no other .u features (special/device files, remapped errno numbers) or the separate .L (Linux-specific) extension.
  • Tflush cancellation reaches a backend's context.Context parameters, but memfs/dirfs don't check them mid-operation (their operations are fast enough that it wouldn't matter); backends fronting something slower can.
  • dirfs's Qid.Path is a hash of the absolute file path, not a real inode number, so it isn't stable across a rename performed outside the server.

Testing

go build ./...
go vet ./...
go test ./...
go test -race ./...
go test -run=none -fuzz=FuzzUnmarshal -fuzztime=30s .

FuzzUnmarshal (in the root package) is the one place this matters most: it's the function parsing untrusted bytes off the wire, and it must never panic regardless of how malformed the input is.

Documentation

Overview

Package p9 implements the wire encoding for the Plan 9 filesystem protocol, 9P2000, as specified in the Plan 9 manual pages intro(5) and fcall(3). It provides only the message types and their marshaling/unmarshaling to and from the wire format; connection handling and RPC dispatch live in the client and server packages.

The 9P2000.u extension (VersionU) is also supported, for its symlink support (DMSYMLINK/QTSYMLINK and Stat's Extension field) and numeric Uid/Gid/Muid (Stat's Nuid/Ngid/Nmuid) — not the rest of .u (special/device files, remapped errno numbers) or .L.

Index

Constants

View Source
const (
	// Version is the protocol version string sent in Tversion.
	Version = "9P2000"

	// VersionU is the 9P2000.u extension's version string: adds
	// symlinks (DMSYMLINK/QTSYMLINK), an Extension field on Stat, and
	// an Extension field on Tcreate, among other Unix-specific
	// extensions this package does not implement.
	VersionU = "9P2000.u"

	// VersionUnknown is the version string a server returns when it
	// does not support the version requested in Tversion.
	VersionUnknown = "unknown"

	// DefaultMsize is a reasonable default maximum message size for
	// a client or server that has no other preference.
	DefaultMsize = 8192

	// MaxWElem is the maximum number of path elements permitted in a
	// single Twalk message.
	MaxWElem = 16
)

Variables

View Source
var (
	ErrTruncated     = errors.New("p9: message truncated")
	ErrTrailingBytes = errors.New("p9: trailing bytes after message")
	ErrMsgTooLarge   = errors.New("p9: message exceeds msize")
	ErrUnknownType   = errors.New("p9: unknown message type")
	ErrTooManyWElem  = errors.New("p9: too many walk elements")
)

Errors returned while framing or decoding a 9P2000 message. These indicate a malformed message on the wire — protocol-level failures (a file not found, permission denied, and so on) are reported by the peer as an RerrorFcall instead.

Functions

func Marshal

func Marshal(tag Tag, m Message) []byte

Marshal encodes tag and m into a complete wire message, including the leading 4-byte size prefix, using plain 9P2000. Use MarshalVersion on a connection that negotiated VersionU.

func MarshalVersion added in v0.9.0

func MarshalVersion(tag Tag, m Message, unix bool) []byte

MarshalVersion is Marshal, but includes the 9P2000.u-only fields on Stat and Tcreate when unix is true.

func ReadMessage

func ReadMessage(r io.Reader, msize uint32) ([]byte, error)

ReadMessage reads one complete 9P2000 message from r: a 4-byte little-endian size prefix followed by size-4 bytes of body. The returned slice includes the size prefix, as Unmarshal expects. If msize is non-zero, messages larger than msize are rejected without being read into memory, which bounds how much a misbehaving peer can force a reader to allocate.

func Unmarshal

func Unmarshal(raw []byte) (Tag, Message, error)

Unmarshal parses a complete wire message, as produced by ReadMessage, into its tag and typed Message, assuming plain 9P2000. Use UnmarshalVersion on a connection that negotiated VersionU.

func UnmarshalVersion added in v0.9.0

func UnmarshalVersion(raw []byte, unix bool) (Tag, Message, error)

UnmarshalVersion is Unmarshal, but expects the 9P2000.u-only fields on Stat and Tcreate to be present when unix is true.

func WriteMessage

func WriteMessage(w io.Writer, raw []byte) error

WriteMessage writes raw, as produced by Marshal, to w.

Types

type FcallType

type FcallType uint8

FcallType is the one-byte message type that begins every 9P2000 message on the wire, immediately after the 4-byte size.

const (
	Tversion FcallType = 100
	Rversion FcallType = 101
	Tauth    FcallType = 102
	Rauth    FcallType = 103
	Tattach  FcallType = 104
	Rattach  FcallType = 105
	// 106 (Terror) is illegal and never sent on the wire.
	Rerror  FcallType = 107
	Tflush  FcallType = 108
	Rflush  FcallType = 109
	Twalk   FcallType = 110
	Rwalk   FcallType = 111
	Topen   FcallType = 112
	Ropen   FcallType = 113
	Tcreate FcallType = 114
	Rcreate FcallType = 115
	Tread   FcallType = 116
	Rread   FcallType = 117
	Twrite  FcallType = 118
	Rwrite  FcallType = 119
	Tclunk  FcallType = 120
	Rclunk  FcallType = 121
	Tremove FcallType = 122
	Rremove FcallType = 123
	Tstat   FcallType = 124
	Rstat   FcallType = 125
	Twstat  FcallType = 126
	Rwstat  FcallType = 127
)

func (FcallType) String

func (t FcallType) String() string

type Fid

type Fid uint32

Fid is a client-chosen handle identifying a file on the server, analogous to a file descriptor.

const NoFid Fid = 0xFFFFFFFF

NoFid marks the absence of an authentication fid in Tattach.

type Message

type Message interface {
	MsgType() FcallType
	// contains filtered or unexported methods
}

Message is implemented by every 9P2000 message body (everything after the tag). Marshal and Unmarshal combine a Message with a Tag to produce or parse a complete wire message.

type Mode

type Mode uint32

Mode holds either an open mode (Topen.Mode, Tcreate.Mode — one byte on the wire) or a permission mode (Stat.Mode, Tcreate.Perm — four bytes on the wire). The two share no bits in practice, so one type serves both roles; callers marshal it as one or four bytes depending on which field it appears in.

const (
	OREAD   Mode = 0
	OWRITE  Mode = 1
	ORDWR   Mode = 2
	OEXEC   Mode = 3
	OTRUNC  Mode = 0x10
	ORCLOSE Mode = 0x40
)

Open-mode bits: the low two bits select the access mode, the rest are flags.

const (
	DMDIR     Mode = 0x80000000
	DMAPPEND  Mode = 0x40000000
	DMEXCL    Mode = 0x20000000
	DMTMP     Mode = 0x04000000
	DMSYMLINK Mode = 0x02000000 // 9P2000.u only
	DMPerm    Mode = 0777
)

Permission-mode bits: the top bits mark directories and other special files, the low 9 bits are rwxrwxrwx permissions.

func (Mode) IsDir

func (m Mode) IsDir() bool

IsDir reports whether the permission Mode marks a directory.

func (m Mode) IsSymlink() bool

IsSymlink reports whether the permission Mode marks a symlink (9P2000.u only).

type Qid

type Qid struct {
	Type    QidType
	Version uint32
	Path    uint64
}

Qid is the server's identification for a file: its type, a version number that changes whenever the file's contents change, and a path that is unique among all files served on a connection.

func (Qid) IsDir

func (q Qid) IsDir() bool

IsDir reports whether the Qid identifies a directory.

func (q Qid) IsSymlink() bool

IsSymlink reports whether the Qid identifies a symlink (9P2000.u only).

func (Qid) String

func (q Qid) String() string

type QidType

type QidType uint8

QidType identifies the type of a file. It occupies the type byte of a Qid and mirrors the high bits of the file's permission Mode.

const (
	QTDIR     QidType = 0x80
	QTAPPEND  QidType = 0x40
	QTEXCL    QidType = 0x20
	QTMOUNT   QidType = 0x10
	QTAUTH    QidType = 0x08
	QTTMP     QidType = 0x04
	QTSYMLINK QidType = 0x02 // 9P2000.u only
	QTFILE    QidType = 0x00
)

func (QidType) String

func (t QidType) String() string

type RattachFcall

type RattachFcall struct {
	Qid Qid
}

func (*RattachFcall) MsgType

func (m *RattachFcall) MsgType() FcallType

type RauthFcall

type RauthFcall struct {
	Aqid Qid
}

func (*RauthFcall) MsgType

func (m *RauthFcall) MsgType() FcallType

type RclunkFcall

type RclunkFcall struct{}

func (*RclunkFcall) MsgType

func (m *RclunkFcall) MsgType() FcallType

type RcreateFcall

type RcreateFcall struct {
	Qid    Qid
	Iounit uint32
}

func (*RcreateFcall) MsgType

func (m *RcreateFcall) MsgType() FcallType

type RerrorFcall

type RerrorFcall struct {
	Ename string
}

RerrorFcall is the error reply to any T-message. It implements the error interface so it can be returned directly from client RPC calls.

func (*RerrorFcall) Error

func (m *RerrorFcall) Error() string

func (*RerrorFcall) MsgType

func (m *RerrorFcall) MsgType() FcallType

type RflushFcall

type RflushFcall struct{}

func (*RflushFcall) MsgType

func (m *RflushFcall) MsgType() FcallType

type RopenFcall

type RopenFcall struct {
	Qid    Qid
	Iounit uint32
}

func (*RopenFcall) MsgType

func (m *RopenFcall) MsgType() FcallType

type RreadFcall

type RreadFcall struct {
	Data []byte
}

func (*RreadFcall) MsgType

func (m *RreadFcall) MsgType() FcallType

type RremoveFcall

type RremoveFcall struct{}

func (*RremoveFcall) MsgType

func (m *RremoveFcall) MsgType() FcallType

type RstatFcall

type RstatFcall struct {
	Stat Stat
}

func (*RstatFcall) MsgType

func (m *RstatFcall) MsgType() FcallType

type RversionFcall

type RversionFcall struct {
	Msize   uint32
	Version string
}

func (*RversionFcall) MsgType

func (m *RversionFcall) MsgType() FcallType

type RwalkFcall

type RwalkFcall struct {
	Wqid []Qid
}

func (*RwalkFcall) MsgType

func (m *RwalkFcall) MsgType() FcallType

type RwriteFcall

type RwriteFcall struct {
	Count uint32
}

func (*RwriteFcall) MsgType

func (m *RwriteFcall) MsgType() FcallType

type RwstatFcall

type RwstatFcall struct{}

func (*RwstatFcall) MsgType

func (m *RwstatFcall) MsgType() FcallType

type Stat

type Stat struct {
	Type   uint16 // kernel use; servers should leave this zero
	Dev    uint32 // kernel use; servers should leave this zero
	Qid    Qid
	Mode   Mode
	Atime  uint32
	Mtime  uint32
	Length uint64
	Name   string
	Uid    string
	Gid    string
	Muid   string

	// The following are 9P2000.u only: present on the wire, in this
	// order, only when the connection negotiated VersionU. Extension
	// holds a symlink's target when Mode has DMSYMLINK set. Nuid,
	// Ngid, and Nmuid are the numeric counterparts of Uid/Gid/Muid.
	Extension string
	Nuid      uint32
	Ngid      uint32
	Nmuid     uint32
}

Stat describes a file's metadata, as used in Tstat/Rstat, Twstat, and directory contents (a directory Read returns a concatenation of Stat blobs, one per entry).

func UnmarshalStat

func UnmarshalStat(b []byte) (Stat, error)

UnmarshalStat decodes a single plain-9P2000 Stat blob, including its leading size prefix. It returns ErrTrailingBytes if b holds more than one Stat's worth of data. Use UnmarshalStatVersion(b, true) to decode a 9P2000.u-flavored blob.

func UnmarshalStatVersion added in v0.9.0

func UnmarshalStatVersion(b []byte, unix bool) (Stat, error)

UnmarshalStatVersion is UnmarshalStat, but expects the 9P2000.u fields to be present when unix is true.

func (Stat) Marshal

func (s Stat) Marshal() []byte

Marshal encodes s as it appears on plain 9P2000: a 2-byte size prefix (the length of everything that follows) followed by the fixed and string fields. The 9P2000.u fields are never included; use MarshalVersion(true) on a connection that negotiated VersionU.

func (Stat) MarshalVersion added in v0.9.0

func (s Stat) MarshalVersion(unix bool) []byte

MarshalVersion is Marshal, but includes the 9P2000.u fields when unix is true.

type Tag

type Tag uint16

Tag identifies an in-flight request so a reply can be matched to it. NoTag marks a message with no associated tag.

const NoTag Tag = 0xFFFF

NoTag marks a message with no associated tag. It is only ever used on the first Tversion of a connection.

type TattachFcall

type TattachFcall struct {
	Fid   Fid
	Afid  Fid
	Uname string
	Aname string
}

func (*TattachFcall) MsgType

func (m *TattachFcall) MsgType() FcallType

type TauthFcall

type TauthFcall struct {
	Afid  Fid
	Uname string
	Aname string
}

func (*TauthFcall) MsgType

func (m *TauthFcall) MsgType() FcallType

type TclunkFcall

type TclunkFcall struct {
	Fid Fid
}

func (*TclunkFcall) MsgType

func (m *TclunkFcall) MsgType() FcallType

type TcreateFcall

type TcreateFcall struct {
	Fid  Fid
	Name string
	Perm Mode
	Mode Mode

	// Extension is 9P2000.u only: present on the wire only when the
	// connection negotiated VersionU. It holds the symlink target
	// when Perm has DMSYMLINK set.
	Extension string
}

func (*TcreateFcall) MsgType

func (m *TcreateFcall) MsgType() FcallType

type TflushFcall

type TflushFcall struct {
	Oldtag Tag
}

func (*TflushFcall) MsgType

func (m *TflushFcall) MsgType() FcallType

type TopenFcall

type TopenFcall struct {
	Fid  Fid
	Mode Mode
}

func (*TopenFcall) MsgType

func (m *TopenFcall) MsgType() FcallType

type TreadFcall

type TreadFcall struct {
	Fid    Fid
	Offset uint64
	Count  uint32
}

func (*TreadFcall) MsgType

func (m *TreadFcall) MsgType() FcallType

type TremoveFcall

type TremoveFcall struct {
	Fid Fid
}

func (*TremoveFcall) MsgType

func (m *TremoveFcall) MsgType() FcallType

type TstatFcall

type TstatFcall struct {
	Fid Fid
}

func (*TstatFcall) MsgType

func (m *TstatFcall) MsgType() FcallType

type TversionFcall

type TversionFcall struct {
	Msize   uint32
	Version string
}

func (*TversionFcall) MsgType

func (m *TversionFcall) MsgType() FcallType

type TwalkFcall

type TwalkFcall struct {
	Fid    Fid
	Newfid Fid
	Wname  []string
}

func (*TwalkFcall) MsgType

func (m *TwalkFcall) MsgType() FcallType

type TwriteFcall

type TwriteFcall struct {
	Fid    Fid
	Offset uint64
	Data   []byte
}

func (*TwriteFcall) MsgType

func (m *TwriteFcall) MsgType() FcallType

type TwstatFcall

type TwstatFcall struct {
	Fid  Fid
	Stat Stat
}

func (*TwstatFcall) MsgType

func (m *TwstatFcall) MsgType() FcallType

Directories

Path Synopsis
Package client implements a 9P2000 client: dial or wrap a connection, attach to a server's exported tree, and walk, open, read, write, and stat files on it.
Package client implements a 9P2000 client: dial or wrap a connection, attach to a server's exported tree, and walk, open, read, write, and stat files on it.
cmd
9pc command
Command 9pc is a bare 9P2000 client: ls, cat, get, and put against a running 9P server, for manual testing and quick scripting.
Command 9pc is a bare 9P2000 client: ls, cat, get, and put against a running 9P server, for manual testing and quick scripting.
9ps command
Command 9ps serves a filesystem over 9P2000 on a TCP address: either a real directory (the default) or an in-memory scratch filesystem (-mem).
Command 9ps serves a filesystem over 9P2000 on a TCP address: either a real directory (the default) or an in-memory scratch filesystem (-mem).
examples
dirfs
Package dirfs is a server.FileSystem backend that exports a real directory tree from the local filesystem, using only the standard library.
Package dirfs is a server.FileSystem backend that exports a real directory tree from the local filesystem, using only the standard library.
memfs
Package memfs is an in-memory server.FileSystem backend: a tree of files and directories held entirely in memory, useful both as a demo server and as a test fixture for the server package.
Package memfs is an in-memory server.FileSystem backend: a tree of files and directories held entirely in memory, useful both as a demo server and as a test fixture for the server package.
Package ns implements a Plan 9 style per-process namespace: a tree built purely by bind (no FUSE, no OS mount table), where a path may graft one or more underlying server.File trees on top of each other (a union directory) with Plan 9's before/after/replace disposition.
Package ns implements a Plan 9 style per-process namespace: a tree built purely by bind (no FUSE, no OS mount table), where a path may graft one or more underlying server.File trees on top of each other (a union directory) with Plan 9's before/after/replace disposition.
Package server implements a 9P2000 server: given a FileSystem backend, it handles wire encoding, fid bookkeeping, multi-element walk batching, and Tflush cancellation, and exposes the backend to any 9P2000 client over a net.Listener or a single connection.
Package server implements a 9P2000 server: given a FileSystem backend, it handles wire encoding, fid bookkeeping, multi-element walk batching, and Tflush cancellation, and exposes the backend to any 9P2000 client over a net.Listener or a single connection.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL