server

package
v1.6.0 Latest Latest
Warning

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

Go to latest
Published: Apr 21, 2026 License: BSD-3-Clause Imports: 29 Imported by: 0

Documentation

Overview

Package server implements a 9P2000.L file server with a capability-based API inspired by go-fuse/v2/fs. Filesystem authors embed *Inode in their node types and implement only the capability interfaces they need; all unimplemented operations automatically return ENOSYS.

Capability Pattern

Define a struct, embed *Inode, and implement capability interfaces such as NodeReader, NodeWriter, NodeOpener, NodeGetattrer, NodeReaddirer, and others. The server's dispatch layer detects implemented interfaces at runtime and routes 9P messages accordingly.

type MyFile struct {
    server.Inode
}

func (f *MyFile) Read(_ context.Context, buf []byte, _ uint64) (int, error) {
    return copy(buf, "hello"), nil
}

Approximately 22 capability interfaces are defined (see node.go), covering file I/O, directory operations, symlinks, device nodes, xattrs, locking, and filesystem statistics.

Composable Helpers

ReadOnlyFile and ReadOnlyDir are pre-built composable types that embed Inode and signal intent: a read-only file that cannot be written, a read-only directory that cannot be mutated.

FileHandle

NodeOpener returns a FileHandle to carry per-open state. If the FileHandle implements FileReader, FileWriter, or FileReaddirer, those take priority over the corresponding Node-level methods for that open instance. This allows stateful I/O (e.g., seekable directory enumeration) without polluting the node itself.

Server Lifecycle

Create a server with New, passing the root Node and any Option values. Call Server.Serve with a context and net.Listener to accept connections:

srv := server.New(root,
    server.WithMaxMsize(1 << 20),
    server.WithLogger(slog.Default()),
)
ln, _ := net.Listen("tcp", ":5640")
srv.Serve(ctx, ln)

Each accepted connection runs in its own goroutine. Within a connection, a single writer goroutine serializes responses while requests are dispatched concurrently (bounded by WithMaxInflight).

Functional Options

Configure the server with:

Middleware

Middleware wraps the dispatch Handler, enabling cross-cutting concerns such as logging, metrics, or access control. When OpenTelemetry providers are configured, tracing and metrics middleware is prepended automatically.

Observability

The server supports OpenTelemetry traces and metrics via the OTel API (no SDK dependency). Configure with WithTracer and WithMeter. Structured logging uses log/slog with automatic trace ID correlation via NewTraceHandler.

Sub-packages

Example

A minimal read-only file server:

package main

import (
    "context"
    "log"
    "net"

    "github.com/dotwaffle/ninep/proto"
    "github.com/dotwaffle/ninep/server"
)

type HelloFile struct {
    server.Inode
}

func (f *HelloFile) Getattr(_ context.Context, _ proto.AttrMask) (proto.Attr, error) {
    return proto.Attr{
        Valid: proto.AttrMode | proto.AttrSize,
        Mode:  0o444,
        Size:  11,
    }, nil
}

func (f *HelloFile) Read(_ context.Context, buf []byte, offset uint64) (int, error) {
    data := []byte("hello world")
    if offset >= uint64(len(data)) {
        return 0, nil
    }
    end := min(offset+uint64(len(buf)), uint64(len(data)))
    return copy(buf, data[offset:end]), nil
}

func main() {
    root := &HelloFile{}
    root.Init(proto.QID{Type: proto.QTFILE, Path: 1}, root)

    srv := server.New(root)
    ln, err := net.Listen("tcp", ":5640")
    if err != nil {
        log.Fatal(err)
    }
    log.Fatal(srv.Serve(context.Background(), ln))
}

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrFidInUse is returned when attempting to allocate a fid that is
	// already present in the fid table.
	ErrFidInUse = errors.New("fid already in use")

	// ErrFidNotFound is returned when a fid lookup fails.
	ErrFidNotFound = errors.New("fid not found")

	// ErrNotNegotiated is returned when a message arrives before version
	// negotiation has completed.
	ErrNotNegotiated = errors.New("version not negotiated")

	// ErrMsizeTooSmall is returned when the client proposes an msize that
	// is too small to carry any useful payload.
	ErrMsizeTooSmall = errors.New("msize too small")

	// ErrNotDirectory is returned when a walk targets a node that does not
	// implement NodeLookuper.
	ErrNotDirectory = errors.New("not a directory")

	// ErrFidLimitExceeded is returned by fidTable.add when the configured
	// per-connection fid cap (see WithMaxFids) has been reached. Handlers
	// map this to proto.EMFILE in the response.
	ErrFidLimitExceeded = errors.New("fid limit exceeded")
)

Sentinel errors for the server package.

Functions

func EncodeDirents

func EncodeDirents(dirents []proto.Dirent, maxBytes uint32) ([]byte, int)

EncodeDirents packs dirents into bytes fitting within maxBytes. Returns the packed bytes and the number of entries that fit.

Each entry is encoded as:

qid[13] + offset[8] + type[1] + name[s]

where name[s] = len[2] + name_bytes.

The returned []byte is a freshly-allocated copy-out — safe to retain past the call boundary.

func EncodeDirentsInto added in v1.6.0

func EncodeDirentsInto(dst []byte, dirents []proto.Dirent) (int, int)

EncodeDirentsInto packs dirents into dst. It returns the number of bytes written and the number of entries that fit.

It is the zero-allocation backend for EncodeDirents and readdirSimple.

func NewTraceHandler

func NewTraceHandler(inner slog.Handler) slog.Handler

NewTraceHandler wraps a slog.Handler with OTel trace ID correlation. Log records emitted within an active span context will include trace_id and span_id attributes. Use this to wrap custom handlers when providing a logger via WithLogger.

func PathQID

func PathQID(t proto.QIDType, path string) proto.QID

PathQID returns a deterministic QID derived from the given path string using FNV-1a 64-bit hashing. Useful for nodes with stable, known paths.

Collision behavior: FNV-1a is not cryptographic; two distinct path strings can hash to the same 64-bit value with birthday-paradox probability around 2^32 paths. Suitable for small, stable namespaces where the path set is controlled by the server; unsuitable for hashing untrusted user-supplied path components. Use QIDGenerator for collision-free allocation when the QIDs do not need to be path-deterministic.

Types

type Attacher

type Attacher interface {
	// Attach resolves the root node for a connection given the uname and
	// aname from Tattach.
	Attach(ctx context.Context, uname, aname string) (Node, error)
}

Attacher provides full-control attach handling. When set via WithAttacher, it overrides the default root-node and aname-dispatch behavior.

type ConnInfo

type ConnInfo struct {
	Protocol   string // "9P2000.L" or "9P2000.u"
	Msize      uint32 // Negotiated message size
	RemoteAddr string // Remote address of the client
}

ConnInfo exposes per-connection metadata to node handlers. A pointer to ConnInfo is injected into the request context by the server before each handler invocation; retrieve it with ConnFromContext.

Fields are read-only. Callers MUST NOT mutate a ConnInfo returned from ConnFromContext -- the same pointer is shared across every request on the same connection, and mutation would race with concurrent request goroutines and corrupt observability labels.

func ConnFromContext

func ConnFromContext(ctx context.Context) *ConnInfo

ConnFromContext returns the connection info for the current request. Returns nil if not called within a connection handler.

There is intentionally no NewContext(ctx, *ConnInfo) helper: ConnInfo is injected by the server from negotiated connection state, and user code cannot construct a valid ConnInfo externally. This asymmetric accessor pattern mirrors stdlib context keys that represent server-owned state (e.g., the net/http pattern for server-injected values).

type Device

type Device struct {
	Inode
	Major uint32
	Minor uint32
}

Device is a node that represents a device node (block or character). Create with DeviceNode.

func DeviceNode

func DeviceNode(gen *QIDGenerator, major, minor uint32) *Device

DeviceNode creates a device node with the given major and minor numbers. The node's QID is assigned from gen with type QTFILE.

type FileHandle

type FileHandle any

FileHandle is a marker type for per-open state returned by NodeOpener.Open. Implement FileReader, FileWriter, FileReleaser, FileSyncer, FileReaddirer, or FileRawReaddirer on the returned value to handle the corresponding wire operations.

A nil FileHandle is permitted for nodes that don't need per-open state; the server will not invoke File-level interfaces against a nil handle and instead falls back to Node-level capability dispatch on the underlying node.

type FileRawReaddirer

type FileRawReaddirer interface {
	// RawReaddir reads raw dirent bytes for the given offset into buf
	// and returns the number of bytes read. The caller provides a buffer
	// sized to the 9P Treaddir count (clamped to msize).
	RawReaddir(ctx context.Context, buf []byte, offset uint64) (int, error)
}

FileRawReaddirer is implemented by file handles that manage their own readdir offset tracking.

type FileReaddirer

type FileReaddirer interface {
	// Readdir returns all directory entries for the open handle.
	Readdir(ctx context.Context) ([]proto.Dirent, error)
}

FileReaddirer is implemented by file handles that support reading directory entries.

type FileReader

type FileReader interface {
	// Read reads up to len(buf) bytes starting at offset into buf and
	// returns the number of bytes read. The caller provides a buffer
	// sized to the 9P Tread count (clamped to msize); implementations
	// fill it and return n.
	Read(ctx context.Context, buf []byte, offset uint64) (int, error)
}

FileReader is implemented by file handles that support reading.

type FileReleaser

type FileReleaser interface {
	// Release releases resources associated with this file handle.
	Release(ctx context.Context) error
}

FileReleaser is implemented by file handles that need cleanup on clunk.

type FileSyncer added in v1.1.0

type FileSyncer interface {
	// Fsync flushes pending writes on the open file to durable storage.
	Fsync(ctx context.Context) error
}

FileSyncer is implemented by file handles that support flushing buffered writes on the open handle to durable storage. Checked before NodeFsyncer by the bridge: Tfsync on an opened fid with a handle that implements FileSyncer takes the handle path; only if the handle does not implement FileSyncer does the bridge fall back to NodeFsyncer on the underlying node.

type FileWriter

type FileWriter interface {
	// Write writes data at the given offset and returns the count of bytes written.
	Write(ctx context.Context, data []byte, offset uint64) (uint32, error)
}

FileWriter is implemented by file handles that support writing.

type Handler

type Handler func(ctx context.Context, tag proto.Tag, msg proto.Message) proto.Message

Handler processes a decoded 9P message and returns the response. Middleware wraps Handler values to add cross-cutting behavior (tracing, metrics, logging) without modifying dispatch logic.

type Inode

type Inode struct {
	// contains filtered or unexported fields
}

Inode provides default implementations for all capability interfaces, returning ENOSYS for unimplemented operations. Embed *Inode in your node struct and call Init to set up the QID and back-reference.

Inode also manages the filesystem tree: parent/child relationships, child lookup, and child enumeration.

func (*Inode) AddChild

func (i *Inode) AddChild(name string, child *Inode)

AddChild adds a child inode under the given name. The child's parent is set to this inode. The children map is lazily initialized. Lock ordering: parent lock acquired before child lock to avoid deadlock.

func (*Inode) Children

func (i *Inode) Children() map[string]*Inode

Children returns a snapshot copy of the children map.

func (*Inode) Close

func (i *Inode) Close(_ context.Context) error

Close is a no-op that returns nil. Override by implementing NodeCloser.

func (*Inode) Create

func (i *Inode) Create(_ context.Context, _ string, _ uint32, _ proto.FileMode, _ uint32) (Node, FileHandle, uint32, error)

Create returns (nil, nil, 0, proto.ENOSYS). Override by implementing NodeCreater.

func (*Inode) EmbeddedInode

func (i *Inode) EmbeddedInode() *Inode

EmbeddedInode returns a pointer to the embedded Inode. Satisfies InodeEmbedder.

func (*Inode) Fsync added in v1.1.0

func (i *Inode) Fsync(_ context.Context) error

Fsync returns proto.ENOSYS. Override by implementing NodeFsyncer.

func (*Inode) GetLock

GetLock returns zero values and proto.ENOSYS. Override by implementing NodeLocker.

func (*Inode) GetXattr

func (i *Inode) GetXattr(_ context.Context, _ string) ([]byte, error)

GetXattr returns (nil, proto.ENOSYS). Override by implementing NodeXattrGetter.

func (*Inode) Getattr

func (i *Inode) Getattr(_ context.Context, _ proto.AttrMask) (proto.Attr, error)

Getattr returns (proto.Attr{}, proto.ENOSYS). Override by implementing NodeGetattrer.

func (*Inode) Init

func (i *Inode) Init(qid proto.QID, node InodeEmbedder)

Init initializes the Inode with a QID and a back-reference to the embedding node. If node is nil, the Inode references itself.

func (i *Inode) Link(_ context.Context, _ Node, _ string) error

Link returns proto.ENOSYS. Override by implementing NodeLinker.

func (*Inode) ListXattrs

func (i *Inode) ListXattrs(_ context.Context) ([]string, error)

ListXattrs returns (nil, proto.ENOSYS). Override by implementing NodeXattrLister.

func (*Inode) Lock

Lock returns (0, proto.ENOSYS). Override by implementing NodeLocker.

func (*Inode) Lookup

func (i *Inode) Lookup(_ context.Context, name string) (Node, error)

Lookup resolves a child by name. If the child exists, it returns the user's node (via the back-reference). If not found, returns proto.ENOENT. Users override this by implementing NodeLookuper on their struct.

func (*Inode) Mkdir

func (i *Inode) Mkdir(_ context.Context, _ string, _ proto.FileMode, _ uint32) (Node, error)

Mkdir returns (nil, proto.ENOSYS). Override by implementing NodeMkdirer.

func (*Inode) Mknod

func (i *Inode) Mknod(_ context.Context, _ string, _ proto.FileMode, _, _, _ uint32) (Node, error)

Mknod returns (nil, proto.ENOSYS). Override by implementing NodeMknoder.

func (*Inode) Open

func (i *Inode) Open(_ context.Context, _ uint32) (FileHandle, uint32, error)

Open returns (nil, 0, proto.ENOSYS). Override by implementing NodeOpener.

func (*Inode) Parent

func (i *Inode) Parent() *Inode

Parent returns the parent Inode, or nil if this is the root.

func (*Inode) QID

func (i *Inode) QID() proto.QID

QID returns the Inode's QID.

func (*Inode) Read

func (i *Inode) Read(_ context.Context, _ []byte, _ uint64) (int, error)

Read returns (0, proto.ENOSYS). Override by implementing NodeReader.

func (*Inode) Readdir

func (i *Inode) Readdir(_ context.Context) ([]proto.Dirent, error)

Readdir returns (nil, proto.ENOSYS). Override by implementing NodeReaddirer.

func (i *Inode) Readlink(_ context.Context) (string, error)

Readlink returns ("", proto.ENOSYS). Override by implementing NodeReadlinker.

func (*Inode) RemoveChild

func (i *Inode) RemoveChild(name string)

RemoveChild removes a child by name.

func (*Inode) RemoveXattr

func (i *Inode) RemoveXattr(_ context.Context, _ string) error

RemoveXattr returns proto.ENOSYS. Override by implementing NodeXattrRemover.

func (*Inode) Rename

func (i *Inode) Rename(_ context.Context, _ string, _ Node, _ string) error

Rename returns proto.ENOSYS. Override by implementing NodeRenamer.

func (*Inode) SetXattr

func (i *Inode) SetXattr(_ context.Context, _ string, _ []byte, _ uint32) error

SetXattr returns proto.ENOSYS. Override by implementing NodeXattrSetter.

func (*Inode) Setattr

func (i *Inode) Setattr(_ context.Context, _ proto.SetAttr) error

Setattr returns proto.ENOSYS. Override by implementing NodeSetattrer.

func (*Inode) StatFS

func (i *Inode) StatFS(_ context.Context) (proto.FSStat, error)

StatFS returns (proto.FSStat{}, proto.ENOSYS). Override by implementing NodeStatFSer.

func (i *Inode) Symlink(_ context.Context, _, _ string, _ uint32) (Node, error)

Symlink returns (nil, proto.ENOSYS). Override by implementing NodeSymlinker.

func (i *Inode) Unlink(_ context.Context, _ string, _ uint32) error

Unlink returns proto.ENOSYS. Override by implementing NodeUnlinker.

func (*Inode) Write

func (i *Inode) Write(_ context.Context, _ []byte, _ uint64) (uint32, error)

Write returns (0, proto.ENOSYS). Override by implementing NodeWriter.

type InodeEmbedder

type InodeEmbedder interface {
	// EmbeddedInode returns the embedded Inode pointer.
	EmbeddedInode() *Inode
}

InodeEmbedder is the base interface for all filesystem nodes that use the Inode tree management. Implement by embedding *Inode in your struct and calling Inode.Init during construction.

type Middleware

type Middleware func(next Handler) Handler

Middleware wraps a Handler, adding behavior before and/or after dispatch. Compose by stacking: the first middleware added is outermost (first to execute, last to see the response).

func NewLoggingMiddleware

func NewLoggingMiddleware(logger *slog.Logger) Middleware

NewLoggingMiddleware returns a Middleware that logs each 9P request at Debug level with structured attributes: op (operation type), duration (elapsed time), and error (whether the response was an error). The logger should be wrapped with NewTraceHandler for trace correlation.

type Node

type Node interface {
	// QID returns the server's unique identifier for this node.
	QID() proto.QID
}

Node is the minimal interface every filesystem node must implement. For most use cases, embed *Inode instead and implement capability interfaces selectively.

type NodeCloser

type NodeCloser interface {
	// Close releases resources associated with this node.
	Close(ctx context.Context) error
}

NodeCloser is implemented by nodes that need cleanup on clunk.

type NodeCreater

type NodeCreater interface {
	// Create creates a new file in this directory.
	Create(ctx context.Context, name string, flags uint32, mode proto.FileMode, gid uint32) (Node, FileHandle, uint32, error)
}

NodeCreater is implemented by directory nodes that can create files.

type NodeFsyncer added in v1.1.0

type NodeFsyncer interface {
	// Fsync flushes pending writes to durable storage.
	Fsync(ctx context.Context) error
}

NodeFsyncer is implemented by nodes that support flushing node-level state to durable storage. When both FileSyncer (on the open file handle) and NodeFsyncer are available for a given fid, the bridge prefers FileSyncer.

The Tfsync wire message carries a datasync flag which is decoded but not surfaced here -- implementations always perform a full fsync.

type NodeGetattrer

type NodeGetattrer interface {
	// Getattr retrieves attributes specified by mask.
	Getattr(ctx context.Context, mask proto.AttrMask) (proto.Attr, error)
}

NodeGetattrer is implemented by nodes that return file attributes.

type NodeLinker

type NodeLinker interface {
	// Link creates a hard link named name in this directory pointing to target.
	Link(ctx context.Context, target Node, name string) error
}

NodeLinker is implemented by directory nodes that can create hard links. The directory receives the request; target is the existing node being linked (resolved from Tlink.Fid). Wire format: dfid[4] fid[4] name[s] -- dfid is this directory, fid is the target.

type NodeLocker

type NodeLocker interface {
	// Lock acquires, tests, or releases a POSIX byte-range lock.
	Lock(ctx context.Context, lockType proto.LockType, flags proto.LockFlags, start, length uint64, procID uint32, clientID string) (proto.LockStatus, error)
	// GetLock tests whether a lock could be placed, returning the conflicting
	// lock parameters if one exists.
	GetLock(ctx context.Context, lockType proto.LockType, start, length uint64, procID uint32, clientID string) (proto.LockType, uint64, uint64, uint32, string, error)
}

NodeLocker is implemented by nodes that support POSIX byte-range locking. Implementations control blocking behavior; the library does not impose any blocking policy. Implementations should respect context deadlines if blocking.

NodeLocker is the only lock interface -- Lock and GetLock are two halves of the same 9P Tlock/Tgetlock pair, always co-implemented in practice, so splitting them into separate single-method interfaces would force every lock implementer to satisfy two interfaces with no separation benefit.

type NodeLookuper

type NodeLookuper interface {
	// Lookup resolves a child by name, returning the child Node or an error.
	// Return proto.ENOENT (wrapped) if the name does not exist.
	Lookup(ctx context.Context, name string) (Node, error)
}

NodeLookuper is implemented by directory nodes that can resolve child names. Walk calls Lookup for each path element.

type NodeMkdirer

type NodeMkdirer interface {
	// Mkdir creates a new subdirectory in this directory.
	Mkdir(ctx context.Context, name string, mode proto.FileMode, gid uint32) (Node, error)
}

NodeMkdirer is implemented by directory nodes that can create subdirectories.

type NodeMknoder

type NodeMknoder interface {
	// Mknod creates a device node named name with the given mode, major/minor
	// numbers, and owning group.
	Mknod(ctx context.Context, name string, mode proto.FileMode, major, minor, gid uint32) (Node, error)
}

NodeMknoder is implemented by directory nodes that can create device nodes.

type NodeOpener

type NodeOpener interface {
	// Open opens the node with the given flags and returns a FileHandle,
	// response flags, and any error.
	Open(ctx context.Context, flags uint32) (FileHandle, uint32, error)
}

NodeOpener is implemented by nodes that can be opened.

type NodeRawReaddirer

type NodeRawReaddirer interface {
	// RawReaddir reads raw dirent bytes for the given offset into buf
	// and returns the number of bytes read. The caller provides a buffer
	// sized to the 9P Treaddir count (clamped to msize).
	RawReaddir(ctx context.Context, buf []byte, offset uint64) (int, error)
}

NodeRawReaddirer is implemented by directory nodes that manage their own offset tracking and dirent packing.

type NodeReaddirer

type NodeReaddirer interface {
	// Readdir returns all directory entries. The server caches and packs
	// them into Rreaddir responses using EncodeDirents.
	Readdir(ctx context.Context) ([]proto.Dirent, error)
}

NodeReaddirer is implemented by directory nodes that return all entries. The server handles offset tracking and dirent packing per-fid.

type NodeReader

type NodeReader interface {
	// Read reads up to len(buf) bytes starting at offset into buf and
	// returns the number of bytes read. The caller provides a buffer
	// sized to the 9P Tread count (clamped to msize); implementations
	// fill it and return n.
	Read(ctx context.Context, buf []byte, offset uint64) (int, error)
}

NodeReader is implemented by nodes that support reading.

type NodeReadlinker

type NodeReadlinker interface {
	// Readlink returns the target path of this symbolic link.
	Readlink(ctx context.Context) (string, error)
}

NodeReadlinker is implemented by symlink nodes that can report their target.

type NodeRenamer

type NodeRenamer interface {
	// Rename moves the entry oldName from this directory to newDir with newName.
	Rename(ctx context.Context, oldName string, newDir Node, newName string) error
}

NodeRenamer is implemented by directory nodes that support renaming entries. newDir is the target directory Node resolved from the new directory fid.

type NodeSetattrer

type NodeSetattrer interface {
	// Setattr modifies attributes specified in attr.
	Setattr(ctx context.Context, attr proto.SetAttr) error
}

NodeSetattrer is implemented by nodes that support setting attributes.

type NodeStatFSer

type NodeStatFSer interface {
	// StatFS returns filesystem statistics for the filesystem containing this node.
	StatFS(ctx context.Context) (proto.FSStat, error)
}

NodeStatFSer is implemented by nodes that can report filesystem statistics.

type NodeSymlinker

type NodeSymlinker interface {
	// Symlink creates a symbolic link named name pointing to target in this
	// directory. Returns the new symlink Node.
	Symlink(ctx context.Context, name, target string, gid uint32) (Node, error)
}

NodeSymlinker is implemented by directory nodes that can create symbolic links.

type NodeUnlinker

type NodeUnlinker interface {
	// Unlink removes the entry named name from this directory.
	Unlink(ctx context.Context, name string, flags uint32) error
}

NodeUnlinker is implemented by directory nodes that can remove entries. Flags may include AT_REMOVEDIR (0x200) to indicate directory removal.

type NodeWriter

type NodeWriter interface {
	// Write writes data at the given offset and returns the count of bytes written.
	Write(ctx context.Context, data []byte, offset uint64) (uint32, error)
}

NodeWriter is implemented by nodes that support writing.

type NodeXattrGetter

type NodeXattrGetter interface {
	GetXattr(ctx context.Context, name string) ([]byte, error)
}

NodeXattrGetter reads a single extended attribute by name. Called in response to Txattrwalk with a non-empty name on a node that does NOT implement RawXattrer.

type NodeXattrLister

type NodeXattrLister interface {
	ListXattrs(ctx context.Context) ([]string, error)
}

NodeXattrLister returns all extended attribute names set on the node. Called in response to Txattrwalk with an empty name on a node that does NOT implement RawXattrer.

type NodeXattrRemover

type NodeXattrRemover interface {
	RemoveXattr(ctx context.Context, name string) error
}

NodeXattrRemover removes an extended attribute by name. Called in response to Txattrcreate(size=0) + Tclunk on a node that does NOT implement RawXattrer.

type NodeXattrSetter

type NodeXattrSetter interface {
	SetXattr(ctx context.Context, name string, data []byte, flags uint32) error
}

NodeXattrSetter sets (creates or replaces) an extended attribute. Called in response to Txattrcreate followed by Twrite and Tclunk on a node that does NOT implement RawXattrer. The flags argument is the raw Txattrcreate flags field.

type Option

type Option func(*Server)

Option configures a Server. Pass to New.

func WithAnames

func WithAnames(m map[string]Node) Option

WithAnames sets a map of aname strings to root nodes for vhost-style attach dispatch. When set, Tattach uses the aname field to select the root node. An empty aname falls back to the default root.

func WithAttacher

func WithAttacher(a Attacher) Option

WithAttacher sets a custom Attacher that handles all Tattach requests. When set, it takes precedence over both the default root node and any aname map configured via WithAnames.

func WithIdleTimeout

func WithIdleTimeout(d time.Duration) Option

WithIdleTimeout sets the per-connection idle timeout. When d > 0, the server resets read and write deadlines on the underlying net.Conn before each I/O operation. A connection that sees no activity for the duration is closed. Default: 0 (no timeout -- caller manages via net.Conn wrapping if needed).

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the structured logger for the server. The handler is automatically wrapped with trace ID correlation (see NewTraceHandler). Default: slog.Default() with trace correlation.

func WithMaxConnections added in v1.1.0

func WithMaxConnections(n int) Option

WithMaxConnections sets the maximum number of concurrent connections the server will serve. When the limit is reached, ServeConn closes the new connection immediately, logs a warning, and increments the ninep.server.connections_rejected OTel counter. Values less than 1 disable the limit. Default: 0 (no limit).

func WithMaxFids added in v1.1.0

func WithMaxFids(n int) Option

WithMaxFids sets the maximum number of concurrent fids the server will allow per connection. When the cap is reached, fid-creating operations (Tattach, Twalk, Txattrwalk) return EMFILE. The cap check runs inside fidTable.add under the write lock, making enforcement race-free. Values less than 1 disable the limit. Default: 0 (no limit).

func WithMaxInflight

func WithMaxInflight(n int) Option

WithMaxInflight sets the maximum number of concurrent in-flight requests per connection. Values less than 1 are clamped to 1. Default: 64.

func WithMaxMsize

func WithMaxMsize(msize uint32) Option

WithMaxMsize sets the maximum message size the server will accept during version negotiation. Default: 1048576 (1 MiB, matches the Linux kernel's silent msize cap).

func WithMeter

func WithMeter(mp metric.MeterProvider) Option

WithMeter sets the OpenTelemetry MeterProvider for the server. When set, an OTel middleware is automatically prepended to the middleware chain, recording duration, request/response sizes, and active request counts. If not set, no metrics overhead is incurred.

func WithMiddleware

func WithMiddleware(mw ...Middleware) Option

WithMiddleware adds middleware to the server's dispatch chain. Middleware runs in order: the first added is outermost (first to execute). Multiple calls append to the existing chain.

func WithTracer

func WithTracer(tp trace.TracerProvider) Option

WithTracer sets the OpenTelemetry TracerProvider for the server. When set, an OTel middleware is automatically prepended to the middleware chain, producing a span for every 9P operation. If not set, no tracing overhead is incurred.

type QIDGenerator

type QIDGenerator struct {
	// contains filtered or unexported fields
}

QIDGenerator produces QIDs with monotonically increasing Path values. Safe for concurrent use.

func (*QIDGenerator) Next

func (g *QIDGenerator) Next(t proto.QIDType) proto.QID

Next returns a new QID with the given type and a unique path. Each call increments the internal counter atomically.

type QIDer

type QIDer interface {
	// QID returns the node's unique identifier.
	QID() proto.QID
}

QIDer is implemented by nodes that provide their own QID. When present, nodeQID uses this in preference to Inode.QID.

type RawXattrer

type RawXattrer interface {
	HandleXattrwalk(ctx context.Context, name string) ([]byte, error)
	HandleXattrcreate(ctx context.Context, name string, size uint64, flags uint32) (XattrWriter, error)
}

RawXattrer provides protocol-level control over the xattr two-phase flow. When a node implements RawXattrer, it takes precedence over the simple xattr interfaces (NodeXattrGetter, NodeXattrSetter, NodeXattrLister, NodeXattrRemover) -- a node implementing RawXattrer need not implement any of those.

HandleXattrwalk is called for both get (name != "") and list (name == ""). It returns the full xattr data that will be served to Tread calls.

HandleXattrcreate returns an XattrWriter that accumulates Twrite data and commits the xattr on Tclunk.

RawXattrer is intentionally two-method: HandleXattrwalk and HandleXattrcreate are the two halves of the 9P xattr two-phase protocol (read-side and write-side). A node implementing only one half would be ill-defined. For the high-level alternative (one method per operation), implement NodeXattrGetter/Setter/Lister/Remover instead.

type ReadOnlyDir

type ReadOnlyDir struct {
	Inode
}

ReadOnlyDir is a composable node for directories that support Lookup, Readdir, and Getattr but not mutation. Embed in your struct and override Lookup/Readdir/Getattr as needed.

ReadOnlyDir relies on the embedded Inode defaults to return ENOSYS for Create (NodeCreater), Mkdir (NodeMkdirer), Unlink (NodeUnlinker), Rename (NodeRenamer), Link (NodeLinker), Symlink (NodeSymlinker), and Mknod (NodeMknoder). The type is a signal-of-intent: the compile-time surface is identical to embedding Inode directly, but the named type documents the contract.

type ReadOnlyFile

type ReadOnlyFile struct {
	Inode
}

ReadOnlyFile is a composable node for files that support Open, Read, and Getattr but not mutation. Embed in your struct and override Open/Read/Getattr as needed.

ReadOnlyFile relies on the embedded Inode defaults to return ENOSYS for Write (FileWriter), Setattr (NodeSetattrer), Create (NodeCreater), Mkdir (NodeMkdirer), Unlink (NodeUnlinker), Rename (NodeRenamer), Link (NodeLinker), and Symlink (NodeSymlinker). The type is a signal-of-intent: the compile-time surface is identical to embedding Inode directly, but the named type documents the contract.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server serves the 9P protocol over network connections. Create with New.

func New

func New(root Node, opts ...Option) *Server

New creates a Server rooted at the given Node. Options configure behavior. The root must implement NodeLookuper for walk resolution.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context, ln net.Listener) error

Serve accepts connections from ln and serves each one in a new goroutine. It blocks until the context is cancelled or the listener returns an error.

func (*Server) ServeConn

func (s *Server) ServeConn(ctx context.Context, nc net.Conn)

ServeConn serves a single 9P connection. It blocks until the connection is closed or the context is cancelled.

When the server has a WithMaxConnections limit configured and the limit is reached, ServeConn closes nc immediately, logs a warning, increments the ninep.server.connections_rejected counter, and returns without serving.

type StaticFS

type StaticFS struct {
	Inode
	Stat proto.FSStat
}

StaticFS is a node that returns fixed filesystem statistics. It implements NodeStatFSer. Embed or compose with other node types to add StatFS support.

func StaticStatFS

func StaticStatFS(gen *QIDGenerator, stat proto.FSStat) *StaticFS

StaticStatFS creates a node that returns the given filesystem statistics. The node's QID is assigned from gen with type QTFILE.

func (*StaticFS) StatFS

func (f *StaticFS) StatFS(_ context.Context) (proto.FSStat, error)

StatFS implements NodeStatFSer.

type Symlink struct {
	Inode
	Target string
}

Symlink is a node that represents a symbolic link. It implements NodeReadlinker to return the link target. Create with SymlinkTo.

func SymlinkTo

func SymlinkTo(gen *QIDGenerator, target string) *Symlink

SymlinkTo creates a symbolic link node pointing to target. The node's QID is assigned from gen with type QTSYMLINK. The returned node implements NodeReadlinker.

func (s *Symlink) Readlink(_ context.Context) (string, error)

Readlink implements NodeReadlinker.

type XattrWriter

type XattrWriter interface {
	Write(ctx context.Context, data []byte) (int, error)
	Commit(ctx context.Context) error
}

XattrWriter accumulates xattr write data and commits on Close. Returned by RawXattrer.HandleXattrcreate. The library calls Write for each Twrite on the xattr fid, then Commit on Tclunk.

Write and Commit form an accumulator lifecycle (write-phase then commit-phase); they are always used together per the 9P xattr two-phase protocol, so XattrWriter is intentionally two-method.

Directories

Path Synopsis
Package fstest provides a protocol-level test harness for validating filesystem implementations against the 9P2000.L contract.
Package fstest provides a protocol-level test harness for validating filesystem implementations against the 9P2000.L contract.
Package memfs provides in-memory filesystem node types for use with the ninep server.
Package memfs provides in-memory filesystem node types for use with the ninep server.
Package passthrough implements a 9P filesystem that proxies all operations to the host OS filesystem.
Package passthrough implements a 9P filesystem that proxies all operations to the host OS filesystem.

Jump to

Keyboard shortcuts

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