133 lines
5 KiB
Nix
133 lines
5 KiB
Nix
# 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).
|
|
#
|
|
# Shape:
|
|
#
|
|
# 1. Detector: nftables' forward chain logs packets headed to the NAS with
|
|
# a "nas-wake: " prefix. A journal follower translates each log line
|
|
# into a wake invocation. Only cross-VLAN traffic hits this path, which
|
|
# is exactly what we want — same-VLAN ARP bursts from stale neighbor
|
|
# entries are not user intent and must not wake the NAS.
|
|
#
|
|
# 2. Reachability probe: a systemd timer pings the NAS every 5s and
|
|
# writes the current epoch to last-seen-up on success. The wake script
|
|
# consults this timestamp and skips the WOL if the NAS was seen up
|
|
# recently — this both saves redundant WOLs and closes the UGREEN N100
|
|
# PSU-ramp-down race where a magic packet arriving right after
|
|
# poweroff makes the board boot into BIOS setup instead of the OS.
|
|
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
nasIp = "${config.networkPrefix}.97.11";
|
|
nasMac = "6c:1f:f7:8e:a9:86";
|
|
serverBroadcast = "${config.networkPrefix}.97.255";
|
|
|
|
stateDir = "/run/nas-wake-on-access";
|
|
lastWakeFile = "${stateDir}/last-wake";
|
|
lastSeenFile = "${stateDir}/last-seen-up";
|
|
cooldownSeconds = 30;
|
|
holdoffSeconds = 60;
|
|
|
|
wakeScript = pkgs.writeShellScript "nas-wake" ''
|
|
set -euo pipefail
|
|
|
|
mkdir -p "${stateDir}"
|
|
now=$(date +%s)
|
|
|
|
# Cooldown gate: at most one WOL every ${toString cooldownSeconds}s.
|
|
# Second line of defense against burst triggers.
|
|
if [[ -f "${lastWakeFile}" ]]; then
|
|
last_wake=$(cat "${lastWakeFile}" 2>/dev/null || echo 0)
|
|
if (( now - last_wake < ${toString cooldownSeconds} )); then
|
|
echo "nas-wake: cooldown active ($((now - last_wake))s < ${toString cooldownSeconds}s), skipping WOL"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# Hold-off gate: if the NAS was probed up within the last
|
|
# ${toString holdoffSeconds}s, skip WOL. Two cases covered:
|
|
# a) NAS is still up — WOL would be wasted (harmless but noisy).
|
|
# b) NAS just started powering off — PSU is ramping down and a magic
|
|
# packet arriving now is the UGREEN BIOS-cold-boot race window.
|
|
# Missing file => first boot of fw or probe has never run; fall through
|
|
# and send the WOL (state unknown, default to waking).
|
|
if [[ -f "${lastSeenFile}" ]]; then
|
|
last_seen=$(cat "${lastSeenFile}" 2>/dev/null || echo 0)
|
|
age=$(( now - last_seen ))
|
|
if (( age < ${toString holdoffSeconds} )); then
|
|
echo "nas-wake: NAS seen up ''${age}s ago (< ${toString holdoffSeconds}s), skipping WOL"
|
|
exit 0
|
|
fi
|
|
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
|
|
'';
|
|
|
|
# Periodic reachability probe. One-shot: ping the NAS, on success write
|
|
# the current epoch to lastSeenFile. On failure, leave the file alone so
|
|
# the timestamp ages out naturally past holdoffSeconds.
|
|
nasProbeScript = pkgs.writeShellScript "nas-probe" ''
|
|
set -euo pipefail
|
|
if ${pkgs.iputils}/bin/ping -c1 -W1 -n ${nasIp} >/dev/null 2>&1; then
|
|
date +%s > "${lastSeenFile}"
|
|
fi
|
|
'';
|
|
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 wol systemd gnugrep ];
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
ExecStart = "${journalFollowerScript}";
|
|
Restart = "always";
|
|
RestartSec = "5s";
|
|
RuntimeDirectory = "nas-wake-on-access";
|
|
RuntimeDirectoryPreserve = "yes";
|
|
};
|
|
};
|
|
|
|
systemd.services.nas-probe = {
|
|
description = "Probe NAS reachability for wake-on-access hold-off";
|
|
after = [ "network-online.target" ];
|
|
wants = [ "network-online.target" ];
|
|
path = with pkgs; [ coreutils iputils ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = "${nasProbeScript}";
|
|
TimeoutStartSec = "3s";
|
|
RuntimeDirectory = "nas-wake-on-access";
|
|
RuntimeDirectoryPreserve = "yes";
|
|
};
|
|
};
|
|
|
|
systemd.timers.nas-probe = {
|
|
description = "Run NAS reachability probe every 5s";
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig = {
|
|
OnBootSec = "10s";
|
|
OnUnitActiveSec = "5s";
|
|
AccuracySec = "1s";
|
|
};
|
|
};
|
|
}
|