NixDaemon

NixOS pentest workstation as one flake — IceBreaker's successor
git clone https://git.daemon-sec.xyz/NixDaemon.git
Log | Files | Refs | README

fan-cli.nix (6780B)


      1 # modules/hosts/laptop/fan-cli.nix — root side of the `fan` command (modules/home/fan.nix).
      2 #
      3 # `fan-ec` talks to the embedded controller the way fanfix does (same
      4 # acpi_call recipe, same registers; see ~/.local/bin/fanfix and the TUXEDO
      5 # driver), but it is a fixed script in the Nix store, so the wheel group may
      6 # run it through sudo without a password. That is what lets the watchdog in
      7 # fan.nix put the fans back to EC-automatic from a background unit, where
      8 # sudo could not ask for one. fanfix itself lives in the user-writable
      9 # ~/.local/bin and must never get such a rule.
     10 #
     11 #   fan-ec status        mode, duty %, EC flags            fan-ec max      100 % (manual)
     12 #   fan-ec auto          hand control back to the EC       fan-ec <30-100> fixed % (manual)
     13 #   fan-ec mode          one word: auto | manual | curve-daemon
     14 #
     15 # Manual mode on this laptop makes the EC assert PROCHOT (all cores 399 MHz)
     16 # under load (fan-throttle-guard.nix, line ~97). fan-ec does not try to
     17 # prevent that; the user-side watchdog detects the clamp and calls `fan-ec auto`.
     18 { ... }:
     19 {
     20   flake.nixosModules.laptop-fan-cli =
     21   { pkgs, lib, ... }:
     22   let
     23     fan-ec = pkgs.writeShellScriptBin "fan-ec" ''
     24       set -uo pipefail
     25       [ "$(id -u)" = 0 ] || { echo "fan-ec: run as root (sudo fan-ec …)" >&2; exit 1; }
     26       PATH=${lib.makeBinPath [ pkgs.coreutils pkgs.kmod pkgs.systemd ]}:$PATH
     27 
     28       ACPI_CALL=/proc/acpi/call
     29       EC_DEV='\_SB.INOU'
     30       FAN_UNIT=fanfix-fan.service
     31       FAN_MIN_PCT=30
     32 
     33       ec_ready() { [ -w "$ACPI_CALL" ] || modprobe acpi_call 2>/dev/null; [ -w "$ACPI_CALL" ]; }
     34       ec_raw()  { printf '%s' "$1" > "$ACPI_CALL" || return 1; local o; o=$(tr -d '\0\n\r ' < "$ACPI_CALL"); printf '%s' "$o"; }
     35       ec_read() { local out; out=$(ec_raw "$(printf '%s.ECRR 0x%04x' "$EC_DEV" "$1")") || return 1
     36         [[ "$out" =~ ^0x[0-9a-fA-F]+$ ]] || { echo "EC read $(printf '0x%04x' "$1") failed: $out" >&2; return 1; }
     37         echo $(( out )); }
     38       ec_write() { local out; out=$(ec_raw "$(printf '%s.ECRW 0x%04x 0x%02x' "$EC_DEV" "$1" "$2")") || return 1
     39         case "$out" in Error*|"not called"|"") echo "EC write $(printf '0x%04x' "$1") failed: ''${out:-no result}" >&2; return 1;; esac
     40         sleep 0.005; }
     41       ec_set_bits()   { local v; v=$(ec_read "$1") || return 1; ec_write "$1" $(( v | $2 )); }
     42       ec_clear_bits() { local v; v=$(ec_read "$1") || return 1; ec_write "$1" $(( v & ~$2 & 0xff )); }
     43       ec_bit() { local v; v=$(ec_read "$1") || return 1; echo $(( (v >> $2) & 1 )); }
     44 
     45       R_AP_OEM=0x0741; R_FAN_MODE=0x0751; R_PWM1=0x075B; R_PWM2=0x075C
     46       R_FAN_CTRL=0x078E; R_TBL_SPLIT=0x07C5; R_TBL_ENABLE=0x07C6
     47       R_CPU_TBL_END=0x0F00; R_CPU_TBL_START=0x0F10; R_CPU_TBL_SPEED=0x0F20
     48       R_GPU_TBL_END=0x0F30; R_GPU_TBL_START=0x0F40; R_GPU_TBL_SPEED=0x0F50
     49       R_PWM1_W=0x1804; R_PWM2_W=0x1809
     50 
     51       universal_ctrl() { ec_bit $R_FAN_CTRL 6; }
     52       tables_enabled() { ec_bit $R_TBL_ENABLE 2; }
     53       pct_to_duty() { echo $(( $1 * 200 / 100 )); }
     54       duty_to_pct() { echo $(( $1 * 100 / 200 )); }
     55 
     56       fan_init_tables() {
     57         local i
     58         ec_clear_bits $R_FAN_MODE 0x40
     59         [ "$(ec_bit $R_TBL_SPLIT 7)" = 1 ] || ec_set_bits $R_TBL_SPLIT 0x80
     60         ec_write $R_CPU_TBL_END 115; ec_write $R_CPU_TBL_START 0; ec_write $R_CPU_TBL_SPEED 1
     61         ec_write $R_GPU_TBL_END 120; ec_write $R_GPU_TBL_START 0; ec_write $R_GPU_TBL_SPEED 1
     62         for i in $(seq 1 15); do
     63           ec_write $(( R_CPU_TBL_END + i )) $(( 115 + i + 1 )); ec_write $(( R_CPU_TBL_START + i )) $(( 115 + i )); ec_write $(( R_CPU_TBL_SPEED + i )) 200
     64           ec_write $(( R_GPU_TBL_END + i )) $(( 115 + i + 1 )); ec_write $(( R_GPU_TBL_START + i )) $(( 115 + i )); ec_write $(( R_GPU_TBL_SPEED + i )) 200
     65         done
     66         [ "$(tables_enabled)" = 1 ] || ec_set_bits $R_TBL_ENABLE 0x04
     67       }
     68       fan_apply_duty() {
     69         local d=$1
     70         if [ "$(universal_ctrl)" = 1 ]; then
     71           [ "$(tables_enabled)" = 1 ] && [ "$(ec_bit $R_FAN_MODE 6)" = 0 ] || fan_init_tables
     72           ec_write $R_CPU_TBL_SPEED "$d"; ec_write $R_GPU_TBL_SPEED "$d"
     73           ec_write $R_PWM1_W "$d"; ec_write $R_PWM2_W "$d"
     74         else
     75           local i; [ "$(ec_bit $R_FAN_MODE 6)" = 1 ] || ec_set_bits $R_FAN_MODE 0x40
     76           for i in 1 2 3 4 5 6 7 8 9 10; do ec_write $R_PWM1_W "$d"; ec_write $R_PWM2_W "$d"; sleep 0.01; done
     77         fi
     78       }
     79       fan_set_auto() {
     80         if [ "$(universal_ctrl)" = 1 ]; then
     81           [ "$(tables_enabled)" = 1 ] && ec_clear_bits $R_TBL_ENABLE 0x04
     82           [ "$(ec_bit $R_TBL_SPLIT 7)" = 1 ] && ec_clear_bits $R_TBL_SPLIT 0x80
     83         fi
     84         [ "$(ec_bit $R_FAN_MODE 6)" = 1 ] && ec_clear_bits $R_FAN_MODE 0x40
     85         return 0
     86       }
     87       mode_word() {
     88         if systemctl is-active --quiet "$FAN_UNIT" 2>/dev/null; then echo curve-daemon; return; fi
     89         if [ "$(ec_bit $R_FAN_MODE 6)" = 1 ] || [ "$(tables_enabled)" = 1 ]; then echo manual; else echo auto; fi
     90       }
     91       stop_daemon() { systemctl is-active --quiet "$FAN_UNIT" 2>/dev/null && systemctl stop "$FAN_UNIT"; return 0; }
     92       guard() { ec_ready || { echo "fan-ec: acpi_call not available" >&2; exit 1; }
     93         ec_read $R_AP_OEM >/dev/null || { echo "fan-ec: EC not reachable via $EC_DEV.ECRR" >&2; exit 1; }; }
     94 
     95       case "''${1:-status}" in
     96         mode)   guard; mode_word ;;
     97         status) guard
     98           printf 'mode=%s duty=%s%% target=%s%% fan-abnormal=%s full-fan-bit=%s tables=%s\n' \
     99             "$(mode_word)" "$(duty_to_pct "$(ec_read $R_PWM1)")" \
    100             "$([ "$(tables_enabled)" = 1 ] && duty_to_pct "$(ec_read $R_CPU_TBL_SPEED)" || echo -)" \
    101             "$(ec_bit $R_AP_OEM 5)" "$(ec_bit $R_FAN_MODE 6)" "$(tables_enabled)" ;;
    102         auto)   guard; stop_daemon; fan_set_auto && echo "fans: auto (EC curve)" ;;
    103         max)    guard; stop_daemon; fan_apply_duty 200 && echo "fans: 100 % (manual)" ;;
    104         [0-9]*) p=''${1%\%}; [[ "$p" =~ ^[0-9]+$ ]] && [ "$p" -le 100 ] && [ "$p" -ge "$FAN_MIN_PCT" ] \
    105                   || { echo "fan-ec: percent must be $FAN_MIN_PCT-100 (below that the EC trips at 79 °C)" >&2; exit 2; }
    106                 guard; stop_daemon; fan_apply_duty "$(pct_to_duty "$p")" && echo "fans: $p % (manual)" ;;
    107         *) echo "usage: fan-ec status|mode|auto|max|<30-100>" >&2; exit 2 ;;
    108       esac
    109     '';
    110   in
    111   {
    112     environment.systemPackages = [ fan-ec ];
    113 
    114     # wheel may run fan-ec without a password: it is immutable store content
    115     # (via the system profile symlink, which is root-owned), does one thing,
    116     # and the watchdog has no terminal to type into.
    117     security.sudo.extraRules = [
    118       {
    119         groups = [ "wheel" ];
    120         commands = [
    121           { command = "/run/current-system/sw/bin/fan-ec"; options = [ "NOPASSWD" ]; }
    122         ];
    123       }
    124     ];
    125   }
    126   ;
    127 }