Files
mesh-catalog/modules/nfs-server/README.md
T
jochen 29ac6ee16a
mesh/delivery-group group feat/mounts-module delivering: 0 of 3 delivered
mesh/merge-gate pass: builds new: modules/mounts, modules/nfs-server, sent nowhere; no bus step; every machine composes with the change as it did without (…
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestAnExportLineSquashesEveryClientToTheFoldersOwnerToTheMeshRangeOnly (0.00s)
mesh/delivery superseded: a newer head of the same pull request
Say that the modules' own checks are the only line before root, since sudo grants the account everything
2026-10-08 20:21:28 +02:00

7.9 KiB

nfs-server

A node shares a folder with the mesh's machines, as a module (novox/hq ADR 0263, to-be 52). It holds the node seat node-nfs-server and provides the provision nfs-share, which the mounts module on another node requires.

The rules it keeps

  • One share per folder. The shares are the assignment's shares setting: name=folder, or name=folder:ro, separated by spaces. The setting has no default: which folders a machine shares is the operator's value (ADR 0112, ADR 0262).
  • To the nodes that ask, as far as this machine allows. A share is exported to a node only when this machine's grants setting names that node for it (share=node:rw|ro[,node:rw|ro], separated by spaces; none, the default, grants nothing) and that node's mounts module wants it from this machine (its state shares on the bus). The access is the lower of the grant and the want, and of the share's own :ro. The address is the node's private address from the mesh's roster (/etc/nfs-server/machines, rendered by the mesh), never one a consumer gives, and it must lie inside the private network's range. The control node, or any node, gets a share only when both name it. A want that is not granted, and a grant nobody wants, are said by exports. Port 2049 is opened to the mesh only; the home network is never named.
  • Every client is the folder's owner. NFS with sec=sys believes the user id a client sends, and one id names different accounts on different machines. Every line is all_squash with anonuid and anongid read from the folder's owner on this machine, so a file written from any node lands owned by the folder's owner, and root is squashed with everyone else.
  • Judged where it really is. A share's path is resolved through every symlink, and every check runs on the real folder, which is what the export line names.
  • Never root's. A folder owned by root, or by root's group, is refused: squashing every client to root would make every client root there.
  • Never the backups. A folder that is the machine's root, is not there, is not a folder, is inside a backup (restic) repository (any parent up to /), or holds one within four levels below it is refused, said by the tools, and makes the module unhealthy. The look below is bounded (200 000 folders); when it is cut short, the share is refused rather than exported unchecked.
  • Only while its filesystem is mounted. A share that is a filesystem of its own is exported with mp, a folder inside one with mp=<its mount point>, so an unmounted dataset never exports the empty folder under it. A folder of the root filesystem carries neither.
  • The folders are the operator's (ADR 0051). The module reads a folder's owner and exports it. It never creates, chowns, chmods or removes anything under a shared path; its code writes two files of its own and nothing else, and a test holds it to that.
  • NFS version 4 only, so 2049 is the only port a client needs: rpcbind and mountd stay closed.

What it owns

  • the nfs-utils package and nfs-server.service, running and enabled;
  • /etc/nfs.conf.d/50-mesh.conf: versions 2, 3 and 4.0 off, 4.1 and 4.2 on;
  • /etc/nfs-server/shares.conf: the settings shares and grants and the range, rendered by the mesh;
  • /etc/nfs-server/machines: every machine of the mesh and its private address, rendered by the mesh;
  • /var/lib/nfs-server/from-bus/wants.json: what the nodes want of this machine, written by the tools from the bus (the directory belongs to the operator account the tools run as);
  • /etc/exports.d/mesh.exports: written by its process, with a marker header;
  • /run/nfs-server/status.json: the process's last look, for the tools;
  • the state exports on the bus, one key per machine: what exports answers, for a consumer's module and for a view (ADR 0201).

How it runs

  • Its process (nfs-server exports, as root) looks every 30 seconds. It reads the shares, the grants, the roster and the wants, judges each folder, writes the export file when it changed, and runs exportfs -ra when the file changed or a share is missing from the kernel's table. Then it writes its status. With no wants taken from the bus yet, it exports nothing.
  • Its tools also take every node's mounts state from the bus every 30 seconds and write what is asked of this machine to the wants file; when the bus does not answer, the last wants stay.
  • Its tools run as the operator account. They read the status and the kernel's table, and run what only root may do (exportfs -ra, zfs set) through sudo -n, like the packet filter's tools.
  • Health is the tool nfs_health, asked by the node-engine every minute: unhealthy when the NFS server is not active, the process has not looked for five minutes, a share is refused or missing from the kernel's table, or the kernel could not read the files again.

Tools

tool answers
node-nfs-server.exports r every share: name, folder, rw or ro, the owner every client is mapped to, the clients, whether it is live; refused shares and why; every export that is not the mesh's (/etc/exports, other files in /etc/exports.d, ZFS sharenfs)
node-nfs-server.clients r the NFS clients the kernel knows (address, name), or the connections to 2049
node-nfs-server.test r whether one share is exported now and the server is active; with an address, whether it is inside the range and the share is exported to it
node-nfs-server.reload a exportfs -ra, then the shares as the kernel holds them
node-nfs-server.adopt a take over a ZFS dataset's sharenfs export of a folder the mesh exports: sharenfs=off on that one dataset, then exportfs -ra; only when the mesh's export of the folder is live to at least one node; a dry run unless confirm is true (or "true"); a person's act, with why
nfs_health r the module's health, one sentence per finding

exports answers, for the mounts module:

{"shares": [{"name", "path", "access", "uid", "gid", "clients", "to": [{"node", "address", "access"}],
             "mp", "line", "live"}], "unmatched": [...],
 "refused": [{"name", "path", "why"}], "foreign": [{"path", "client", "options", "from"}],
 "service": "active", "range", "looked", "said", "notes"}

When it is unassigned

The process writes /etc/exports.d/mesh.exports, not the node-engine, because each line carries the folder's owner and the nodes' addresses as this machine reads them. The node-engine removes a module's resources in reverse order of declaration (mesh-host internal/apply), and the process is declared before its config file, so on unassignment the config file is gone when the process is stopped. The process then removes its export file — the one removal its code makes — and runs exportfs -ra, so the kernel lets the shares go. A plain restart, a push or a shutdown leaves the config file in place, and the exports stay.

What it leaves

  • A subfolder export shares its filesystem. A share that is a folder inside a larger filesystem (not a filesystem of its own) is exported with no_subtree_check, so a client that forges file handles could reach other folders of that filesystem. Every client is a node and is squashed to the share's owner, so a folder that owner cannot read stays closed. A share that must be strictly fenced is better given its own dataset.
  • Exports that are not the mesh's (/etc/exports, a ZFS dataset's sharenfs) are left as found and shown, until adopt takes one over on a person's word.

Root acts and sudo

The tools run as the operator account and act as root through sudo -n. The sudo module grants that account every command, so this module's code is the only line between a tool call and root: each act checks again what it may touch before it runs a command, and a test holds each check. Narrowing what sudo grants the account to the commands these modules need is a separate decision, not made here.