feat: full NFSv4.2 server and client in pure Go
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# Deployment
|
||||
|
||||
How nfsd runs in production.
|
||||
|
||||
## Topology
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
C1[NFS client] --> S[nfsd]
|
||||
C2[NFS client] --> S
|
||||
S --> Disk[(export tree)]
|
||||
S --> State[(state dir)]
|
||||
```
|
||||
|
||||
One nfsd process serves one exported directory tree to any number of NFSv4.2
|
||||
clients over the single TCP port 2049. There is no portmapper, no mountd and
|
||||
no separate locking protocol: a client mounts `nfs://host:2049/` directly and
|
||||
everything rides the one connection or its successors.
|
||||
|
||||
## Requirements
|
||||
|
||||
- a Linux host, on amd64, arm64, loong64 or riscv64; the binary is static, no
|
||||
runtime libraries
|
||||
- port 2049 free; it is not a privileged port, so the service does not need
|
||||
root
|
||||
- the exported directory must exist before start; the service identity needs
|
||||
read access to it, and write access where clients may write
|
||||
- a writable state directory, when persistence is on, writable by the service
|
||||
identity alone (mode 0700)
|
||||
- write access to the export tree requires one of: the service runs as root,
|
||||
or the service holds `CAP_CHOWN` and `CAP_MKNOD`, or the operator accepts
|
||||
the identity behaviour described under Privileges
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
just build
|
||||
```
|
||||
|
||||
The binaries land in `bin/nfsd` and `bin/nfs`.
|
||||
|
||||
## Run
|
||||
|
||||
```sh
|
||||
bin/nfsd -config /etc/nfsd/nfsd.toml
|
||||
```
|
||||
|
||||
The configuration file is described in [CONFIGURATION.md](CONFIGURATION.md);
|
||||
it carries the listen address, the export, the state directory, the
|
||||
connection cap, the TLS key pair and the root squash switch. The flags
|
||||
override the file.
|
||||
|
||||
## Service unit
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=NFSv4.2 server for one export
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
User=nfsd
|
||||
Group=nfsd
|
||||
ExecStart=/usr/local/bin/nfsd -config /etc/nfsd/nfsd.toml
|
||||
StateDirectory=nfsd
|
||||
AmbientCapabilities=CAP_CHOWN CAP_MKNOD
|
||||
CapabilityBoundingSet=CAP_CHOWN CAP_MKNOD
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/srv/export
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
`Type=notify` is real readiness: the server writes `READY=1` to
|
||||
`$NOTIFY_SOCKET` once the listener is up, and systemd considers the unit
|
||||
started at that moment, not at the fork. `StateDirectory=nfsd` creates
|
||||
`/var/lib/nfsd` owned by the service identity; point the configuration's
|
||||
`state-dir` at it and handles and opens survive a restart inside the grace
|
||||
window.
|
||||
|
||||
### Privileges
|
||||
|
||||
The port needs no privilege, so the unit runs under a dedicated identity and
|
||||
names exactly two capabilities:
|
||||
|
||||
- `CAP_CHOWN` lets the server hand a freshly created object to the identity
|
||||
the client presented. Without it the object keeps the service identity;
|
||||
the server still answers the client's claim as the owner attribute, but
|
||||
the on disk owner is the service one. This is a deliberate operator
|
||||
decision, documented here and not hidden: serving untrusted clients
|
||||
without `CAP_CHOWN` changes whose identity new files carry on disk.
|
||||
- `CAP_MKNOD` serves the special objects a CREATE may carry, character and
|
||||
block devices among them. Without it those creations fail.
|
||||
|
||||
Neither capability lets the service read a file it could not already reach.
|
||||
|
||||
### Root squash
|
||||
|
||||
Serve untrusted clients with `root-squash = true` in the export: a client
|
||||
claiming uid 0 acts as nobody (65534), loses the superuser grant, and its
|
||||
objects carry nobody. The default is `false`, which keeps the trust AUTH_SYS
|
||||
hands to the claim; an operator who controls every client may keep it.
|
||||
|
||||
## Production configuration
|
||||
|
||||
```toml
|
||||
listen = ":2049"
|
||||
state-dir = "/var/lib/nfsd"
|
||||
max-connections = 256
|
||||
log-ops = false
|
||||
|
||||
[tls]
|
||||
cert = "/etc/nfsd/cert.pem"
|
||||
key = "/etc/nfsd/key.pem"
|
||||
|
||||
[[export]]
|
||||
path = "/srv/export"
|
||||
root-squash = true
|
||||
```
|
||||
|
||||
The TLS key pair enables RPC-with-TLS of RFC 9289; a client that skips the
|
||||
upgrade is refused. The certificate comes from the operator's PKI; no
|
||||
credential belongs in this repository or its configuration examples.
|
||||
|
||||
## Firewall
|
||||
|
||||
One port in, no outbound requirement beyond what the clients reach the back
|
||||
channel on: the server calls the client back on the client's connection, so
|
||||
no inbound port per client is needed.
|
||||
|
||||
```sh
|
||||
firewall-cmd --permanent --add-port=2049/tcp && firewall-cmd --reload
|
||||
```
|
||||
|
||||
## Upgrade
|
||||
|
||||
```sh
|
||||
just build
|
||||
install -m 755 bin/nfsd /usr/local/bin/nfsd
|
||||
systemctl restart nfsd
|
||||
```
|
||||
|
||||
With `state-dir` set the restart is a recovery, not a loss: the new process
|
||||
loads the handle map and the opens, and clients inside the grace window
|
||||
reclaim with CLAIM_PREVIOUS. Without it, clients re establish their sessions
|
||||
and re open; their mounted trees keep working through the file handles the
|
||||
backend re registers.
|
||||
|
||||
## Rollback
|
||||
|
||||
Reinstall the previous binary and restart; the state directory format has
|
||||
one version so far, so a downgrade reloads the same state. Not rehearsed
|
||||
against a released predecessor yet: rehearse before relying on it.
|
||||
|
||||
## Monitoring
|
||||
|
||||
- the unit's ready state: `systemctl is-active nfsd`
|
||||
- the operation log, when `log-ops` is on: every operation, its status and
|
||||
its duration on standard error, journald's `journalctl -u nfsd` picks it up
|
||||
- the connection cap answering refusals shows up as clients reconnecting;
|
||||
a steady refusal rate means the cap or the client count is wrong
|
||||
- a healthy idle server logs nothing and holds no CPU: check
|
||||
`systemctl status nfsd` for a flat memory figure and `ss -tnp sport = 2049`
|
||||
for the connected clients
|
||||
Reference in New Issue
Block a user