# NAS wake-on-access (fw side) # # Detects traffic aimed at the NAS (10.42.97.11) and sends a WOL magic # packet so the machine comes back up on demand after it has powered itself # off (see hosts/nas/modules/auto-shutdown.nix). # # Traffic reaches the NAS via two paths, so we need two detectors that feed # the same wake script: # # 1. Cross-VLAN traffic is routed through fw and hits nftables' forward # chain. A logging rule tags these packets and a journal follower # translates the log line into a wake invocation. # # 2. Same-VLAN (server) traffic stays on the bridge and never reaches # nftables. A tcpdump follower watches ARP-who-has for 10.42.97.11 on # the server interface and triggers the wake from there. { config, lib, pkgs, ... }: let nasIp = "${config.networkPrefix}.97.11"; nasMac = "6c:1f:f7:8e:a9:86"; serverBroadcast = "${config.networkPrefix}.97.255"; serverIface = "server"; stateDir = "/run/nas-wake-on-access"; lastWakeFile = "${stateDir}/last-wake"; cooldownSeconds = 30; wakeScript = pkgs.writeShellScript "nas-wake" '' set -euo pipefail mkdir -p "${stateDir}" now=$(date +%s) # Cooldown gate: at most one WOL every ${toString cooldownSeconds}s. if [[ -f "${lastWakeFile}" ]]; then last=$(cat "${lastWakeFile}" 2>/dev/null || echo 0) if (( now - last < ${toString cooldownSeconds} )); then exit 0 fi fi # If the NAS answers ping it is already up; skip WOL but refresh # the cooldown so repeated probes don't spin the CPU. if ${pkgs.iputils}/bin/ping -c1 -W1 -n ${nasIp} >/dev/null 2>&1; then echo "nas-wake: NAS already up, not sending WOL" echo "$now" > "${lastWakeFile}" exit 0 fi echo "nas-wake: sending WOL to ${nasMac} via ${serverBroadcast}" ${pkgs.wol}/bin/wol -i ${serverBroadcast} ${nasMac} || true echo "$now" > "${lastWakeFile}" ''; # Journal follower for cross-VLAN (routed) traffic. nftables logs a line # prefixed with "nas-wake: " into the kernel ring buffer for every new # packet headed to the NAS (rate-limited kernel-side). journalFollowerScript = pkgs.writeShellScript "nas-wake-journal-follower" '' set -euo pipefail ${pkgs.systemd}/bin/journalctl -kf -o cat --since now \ | ${pkgs.gnugrep}/bin/grep --line-buffered -F "nas-wake:" \ | while IFS= read -r _line; do ${wakeScript} || true done ''; # ARP follower for same-VLAN traffic. Clients on the server VLAN talk to # the NAS directly via the bridge, so their packets never hit nftables. # An ARP "who-has 10.42.97.11" is the reliable early signal that someone # wants to reach the NAS. arpFollowerScript = pkgs.writeShellScript "nas-wake-arp-follower" '' set -euo pipefail ${pkgs.tcpdump}/bin/tcpdump -i ${serverIface} -l -n -p -Q in \ 'arp and host ${nasIp}' \ | while IFS= read -r _line; do ${wakeScript} || true done ''; in { systemd.services.nas-wake-journal = { description = "Wake NAS on cross-VLAN traffic (nftables log follower)"; after = [ "nftables.service" "systemd-journald.service" ]; requires = [ "systemd-journald.service" ]; wantedBy = [ "multi-user.target" ]; path = with pkgs; [ coreutils iputils wol systemd gnugrep ]; serviceConfig = { Type = "simple"; ExecStart = "${journalFollowerScript}"; Restart = "always"; RestartSec = "5s"; }; }; systemd.services.nas-wake-arp = { description = "Wake NAS on same-VLAN ARP (server bridge)"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; path = with pkgs; [ coreutils iputils wol tcpdump ]; serviceConfig = { Type = "simple"; ExecStart = "${arpFollowerScript}"; Restart = "always"; RestartSec = "5s"; AmbientCapabilities = [ "CAP_NET_RAW" "CAP_NET_ADMIN" ]; }; }; }