# 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=`, 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.