How it works
Declare a share against a peer
A share is protocol, peer, remotePath, mountpoint and two timeouts. peer is a name, not an address — conventionally a nixnet peer, so the share inherits LAN/overlay failover with zero address logic of nixshare's own. Any NSS-resolvable name works.
A provider builds the units
The nfs and cifs provider modules turn that one declaration into real systemd.mounts and systemd.automounts, each with its own protocol-appropriate mount options and a bounded TimeoutSec=. Core stays protocol-blind.
The watchdog unsticks what hangs
Every pollIntervalSec, a stateless oneshot asks systemd whether any share's unit has been activating longer than its automountTimeoutSec. If so: umount -f -l, immediately, plus an optional alert. No human, no sudo, no wedged session.
What you get
Force-unmount, not a kill signal
A mount attempt stuck against a dead peer sits in an uninterruptible kernel wait — SIGKILL doesn't free it, which is why systemd's own TimeoutSec= isn't enough on its own. umount -f -l detaches the mountpoint at the VFS level regardless of what the blocked helper is still doing kernel-side.
Proactive, not post-mortem
The watchdog acts on an attempt that is still in flight, before the first stat() from a shell prompt or file manager joins it in D-state. That's the whole difference from the manual recovery it replaces.
Zero address logic, permanently
nixshare will never grow its own IP/hostname failover — that's an explicit non-goal. Pairing peer with nixnet is what that field is for; nixshare only needs the name to already resolve.
Both directions of the same schema
Client mounts and server-side exports: nfs-server-provider/cifs-server-provider export a ZFS sharenfs/sharesmb tree matrix — kernel NFSv4 with idmapd and firewall scoping, Samba with usershares and wsdd/avahi discovery.
Core + pluggable providers
Core never learns what NFS or CIFS is. It renders one JSON file of (name, mountpoint, timeout) triples — the entire interface between Nix and the watchdog — and providers own their own mount-option surface. Adding a protocol never touches core.
Alerts without a dependency
watchdog.alertCommand is a plain command prefix, filled from nixpush's mkSendCommand if you use it, or anything else. A failed alert never blocks or reverts a recovery — the unmount already happened.
Credentials stay out of the store
credentialsFile is a runtime path to a username=/password= file — sops-nix, agenix, anything — handed to mount.cifs directly and never copied into the Nix store.
Not NixOS-only
The core and client providers touch nothing NixOS-exclusive, so the same files are exported as systemManagerModules.* for a non-NixOS Linux box under system-manager. The systemd.mounts surface there is flagged as unverified in experiments/, not silently assumed.
Architecture
nixshare.shares.<name>
protocol = "nfs" | "cifs"
peer = "storage-host" ← a name, never an address
│
▼
┌──────────────────────────────┐
│ core (protocol-blind) │──► /etc/nixshare/watchdog.json
│ schema + provider registry │ (name, mountpoint, timeout)
└──────────────┬───────────────┘ │
│ ▼
┌───────────┴───────────┐ ┌───────────────────────────────┐
│ nfs-provider │ │ nixshare-watchdog.timer │
│ cifs-provider │ │ every pollIntervalSec: │
└───────────┬───────────┘ │ unit activating too long? │
│ │ └─► umount -f -l + alert │
▼ └───────────────────────────────┘
systemd .mount / .automount
│
▼
mount.nfs / mount.cifs ──► storage-host
resolved by NSS — nixnet keeps the
name pointed at a transport that works
One JSON file is the whole Nix→runtime interface
The watchdog reads /etc/nixshare/watchdog.json and nothing else. It carries no state between ticks, resolves each mountpoint to its unit with the real systemd-escape at runtime, and asks systemd fresh every time whether an attempt is in flight.
Root and unsandboxed, deliberately
nixshare-watchdog.service runs with no Protect*/Private* hardening on purpose: those directives work by giving a unit its own private mount namespace, which would silently turn every force-unmount into a no-op against the real, stuck session.
Install
1. Add the flake input
inputs.nixshare.url = "github:julian-corbet/nixshare-corbet-ch";
Import nixosModules.default (core: schema + watchdog, zero providers) plus whichever providers you need — nfs-provider, cifs-provider, and their *-server-provider counterparts.
2. Declare a share
nixshare = {
enable = true;
shares.example = {
protocol = "nfs";
peer = "storage-host";
remotePath = "/export/example";
mountpoint = "/mnt/example";
cacheSettings.actimeo = "60";
};
};
Build from source
git clone https://github.com/julian-corbet/nixshare-corbet-ch
cd nixshare-corbet-ch
nix build .#nixshare-watchdog
Needs only Nix with flakes enabled. nix flake check runs the same evaluation checks CI does.
Key options
shares.<name>.peer | The server, by name — conventionally a nixnet.peers.<name> entry, but any NSS-resolvable name works. |
shares.<name>.automountTimeoutSec | Per-share watchdog threshold, default 30. Must exceed establishTimeoutSec (asserted). |
establishTimeoutSec | Every provider's .mount unit TimeoutSec=, default 15. Bounds a normal failure fast; does not by itself free a genuinely stuck attempt. |
shares.<name>.cacheSettings | Freeform protocol-specific tuning, opaque to core. NFS: nfsvers, timeo, retrans, actimeo, lookupcache, nconnect, fsc. CIFS: vers, cache. |
shares.<name>.credentialsFile | CIFS only — runtime path to a username=/password= file. Unset falls back to a guest mount. |
watchdog.pollIntervalSec | How often the oneshot runs, default 10. |
watchdog.alertCommand | Command prefix run on every force-unmount, with a description of what happened appended. |