@k13engineering/nfs-server
v0.0.3
Published
Platform agnostic NFSv3 and NFSv4 server on top of fsapi
Readme
nfs-server
NFSv3 and NFSv4.0 servers for any fsapi filesystem, written in TypeScript without platform dependencies. You provide the network as a small interface, so the servers run wherever you can supply TCP (and optionally UDP) sockets.
Note: this project is mostly AI coded. Its code, tests and documentation were largely written by an AI assistant, and reviewed and directed by a human maintainer.
npm install @k13engineering/nfs-server @k13engineering/fsapiExample
import { createRamFileSystem } from "@k13engineering/fsapi";
import { createNfsV3Server, createNfsV4Server } from "@k13engineering/nfs-server";
import { createNodeNetwork } from "./node-network.ts";
const fileSystem = createRamFileSystem();
const network = createNodeNetwork();
const v3 = createNfsV3Server({ fileSystem, network });
// or
const v4 = createNfsV4Server({ fileSystem, network, port: 2050 });
// later
v3.close();
v4.close();Mount with mount -t nfs -o vers=3 server:/ /mnt or mount -t nfs -o vers=4.0 server:/ /mnt.
The network interface
The servers never open sockets themselves. They call listenTcp, and optionally listenUdp, of a TNetworkInterface:
type TNetworkInterface = {
listenTcp: (args: {
port: number,
onConnection: (args: { connection: TTcpConnection }) => TTcpConnectionHandler
}) => { close: () => void };
listenUdp?: (args: {
port: number,
onDatagram: (args: { datagram: TUdpDatagram }) => void
}) => { close: () => void };
};A connection gives the addresses of both ends plus write and close. The returned handler receives onData and onClose. A datagram carries its data, the sender, and a reply function.
Listening is synchronous and never fails, and so are creating and closing the servers. Connections and datagrams may arrive as soon as the network is ready. A network that cannot listen on a port, e.g. because it is in use, reports that its own way, as only the platform knows how. The servers call the close of each listener once. Their own close stops listening and closes open connections, and can be called more than once.
An adapter for Node.js:
import net from "node:net";
import dgram from "node:dgram";
import type { TNetworkInterface } from "@k13engineering/nfs-server";
// onError gets errors of the listening sockets, e.g. EADDRINUSE; by default they are thrown, which ends the process
const createNodeNetwork = ({
host = "0.0.0.0",
onError = (error: Error) => {
throw error;
}
} = {}): TNetworkInterface => {
return {
listenTcp: ({ port, onConnection }) => {
const server = net.createServer((socket) => {
const handler = onConnection({
connection: {
remoteAddress: socket.remoteAddress ?? "",
remotePort: socket.remotePort ?? 0,
localAddress: socket.localAddress ?? "",
localPort: socket.localPort ?? 0,
write: ({ data }) => {
socket.write(data);
},
close: () => {
socket.destroy();
},
}
});
socket.on("data", (data) => {
handler.onData({ data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength) });
});
socket.on("close", () => {
handler.onClose();
});
socket.on("error", () => {
// followed by close
});
});
server.on("error", onError);
server.listen(port, host);
return {
close: () => {
server.close();
}
};
},
listenUdp: ({ port, onDatagram }) => {
const socket = dgram.createSocket("udp4");
socket.on("message", (data, remote) => {
onDatagram({
datagram: {
data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
remoteAddress: remote.address,
remotePort: remote.port,
localAddress: undefined,
reply: ({ data: reply }) => {
socket.send(reply, remote.port, remote.address);
},
}
});
});
socket.on("error", onError);
socket.bind(port, host);
return {
close: () => {
socket.close();
}
};
},
};
};Options
Both servers take:
| Option | Default | Description |
| --- | --- | --- |
| fileSystem | required | The fsapi filesystem to export, from its root. Use fsapi's createChroot to export a subdirectory, or createReadOnlyFileSystem. |
| network | required | The network interface. |
| port | 2049 | The port the server listens on. |
| readOnly | false | Refuses all modifications with ROFS. |
| rootSquash | true | Maps requests of uid 0 to the anonymous user. |
| allSquash | false | Maps requests of all users to the anonymous user. |
| anonUid, anonGid | 65534 | The anonymous user. |
| maxReadSize, maxWriteSize | 1048576 | The largest READ and WRITE transfers in bytes. |
| isClientAllowed | all | ({ address }) => boolean. Requests of other clients get an authentication error. |
| clock | Date.now | The current time in nanoseconds. It drives time stamps set to "now" and NFSv4 leases, so tests can control it. |
| logger | none | ({ message, error }) => void, called when a procedure fails unexpectedly. |
createNfsV3Server also takes:
| Option | Default | Description |
| --- | --- | --- |
| exportPath | "/" | The path clients mount with the MOUNT protocol. |
| portmapperPort | 111 | The port of the built-in port mapper (rpcbind), or false if another port mapper runs on the host. |
| udp | true | Also serves UDP, if the network supports it. |
createNfsV4Server also takes:
| Option | Default | Description |
| --- | --- | --- |
| leaseTime | 90 | Seconds after which a client that did not renew its lease loses its opens and locks. |
What is served
- NFSv3 (RFC 1813) serves all 22 procedures. The MOUNT protocol version 3 runs on the same port, with no need for
mountportoptions. Version 1 is also offered, for tools that list exports and mounts. The network lock manager NLM v4 and the status monitor NSM supportfcntllocks. A port mapper (portmap v2, rpcbind v3 and v4) points clients to all of them. - NFSv4.0 (RFC 7530) serves all operations except delegations and named attributes. That covers client ids and leases, open and lock owners with sequence ids and replay protection, share reservations, and byte-range locks.
- Permissions follow POSIX, checked against the mode, owner and group of each node and the AUTH_SYS credentials of the request. As on Linux, owners may read and write their own files regardless of the mode.
Limitations
- Protocol versions: NFSv4.1 and 4.2 are not supported. Clients fall back to 4.0, Linux does this automatically. NFSv2 is not supported.
- Security: only AUTH_SYS and AUTH_NONE. No RPCSEC_GSS (Kerberos). NFSv4 owners are numeric, as with
nfs4_disable_idmapping. - Server state: nothing survives a restart. File handles become stale (clients have to remount), locks are lost, and there is no grace period for reclaiming them.
- Locking:
- The server cannot call clients back. There are no NFSv4 delegations, and blocked lock requests are not granted by callback, so clients poll for them. The Linux NLM client polls every 30 seconds.
- Locks are advisory and only known to the server instance. NFSv3 and NFSv4 servers do not see each other's locks, other users of the fsapi filesystem do not see them at all, and READ and WRITE do not check them.
- Exports: there is only one export per server. NFSv4 exports the filesystem root,
exportPathapplies to NFSv3 only. - Unsupported features: ACLs, named attributes, referrals and quotas.
- Concurrency: requests are processed one after another, which keeps the state consistent but gives no parallelism.
- Memory: the file handle table keeps an entry for every node a client has seen.
- File names: names that are not valid UTF-8 are refused.
Development
npm ci
npm run build
npm run type-check
npm run test
npm run lintLicense
LGPL-2.1-only
