nixshare

A share names a peer, not an address — and gets out of the way when that peer dies.

Declarative NFS and CIFS shares for NixOS. The client side points each share at a named peer instead of a hardcoded address, so it inherits that name's own address failover. On top of that sits the piece nothing else covers: a watchdog that catches a mount attempt taking too long and force-lazy-unmounts it before it wedges your session, instead of leaving you to notice and reach for sudo umount -f -l yourself. The server side exports a ZFS sharenfs/sharesmb tree — kernel NFSv4 and Samba, both declarative.

nixshare — a peer goes dark mid-mount
$ systemctl status nixshare-watchdog.timer
● nixshare-watchdog.timer - Poll nixshare automounts for stuck establish attempts
     Loaded: loaded
     Active: active (waiting)

$ ls /mnt/example        # first access triggers the automount, transparently

$ journalctl -u nixshare-watchdog.service -n3
nixshare-watchdog: share 'example' (/mnt/example, unit mnt-example.mount) has been activating for 31s (>= 30s) -- force-lazy-unmounting
nixshare-watchdog: umount -f -l /mnt/example succeeded
nixshare-watchdog: ALERT: nixshare: force-unmounted stuck share 'example' at /mnt/example on example-host after 31s (threshold 30s)

How it works

1

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.

2

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.

3

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>.peerThe server, by name — conventionally a nixnet.peers.<name> entry, but any NSS-resolvable name works.
shares.<name>.automountTimeoutSecPer-share watchdog threshold, default 30. Must exceed establishTimeoutSec (asserted).
establishTimeoutSecEvery provider's .mount unit TimeoutSec=, default 15. Bounds a normal failure fast; does not by itself free a genuinely stuck attempt.
shares.<name>.cacheSettingsFreeform protocol-specific tuning, opaque to core. NFS: nfsvers, timeo, retrans, actimeo, lookupcache, nconnect, fsc. CIFS: vers, cache.
shares.<name>.credentialsFileCIFS only — runtime path to a username=/password= file. Unset falls back to a guest mount.
watchdog.pollIntervalSecHow often the oneshot runs, default 10.
watchdog.alertCommandCommand prefix run on every force-unmount, with a description of what happened appended.