From 5fbe4e306f753e07a2de025c3a6d484cfe7b6c26 Mon Sep 17 00:00:00 2001 From: Sergei Poljanski Date: Thu, 7 May 2026 17:55:07 +0300 Subject: [PATCH] ThinkPad T14 G2 AMD -> P14s G6 AMD --- .gitignore | 2 + flake.lock | 51 +++++++++++- flake.nix | 9 +++ hardware-configuration.nix | 10 ++- modules/networking.nix | 20 ++++- modules/packages.nix | 7 +- modules/power.nix | 136 ++++++++++++++++++++++++-------- modules/private/ssh-hosts.nix | 6 ++ modules/private/xray-routes.nix | 6 ++ modules/programs.nix | 4 + modules/security.nix | 5 +- modules/wireguard.nix | 4 + modules/xray.nix | 20 +++-- 13 files changed, 222 insertions(+), 58 deletions(-) create mode 100644 modules/private/ssh-hosts.nix create mode 100644 modules/private/xray-routes.nix diff --git a/.gitignore b/.gitignore index d715629..2dd0052 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ modules/usb-devices.nix +secrets/ +.sops.yaml \ No newline at end of file diff --git a/flake.lock b/flake.lock index 4e13c39..6fa2f65 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,26 @@ { "nodes": { + "anotherim": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1777518264, + "narHash": "sha256-60eHsvcyBcpIbpHNMaMnoc1Nz164Z1+JvMxb78IBSZc=", + "ref": "dev", + "rev": "2d25b219ca4f2b9252b22b5aaa2f73de422e9caa", + "revCount": 1547, + "type": "git", + "url": "https://dev.narayana.im/anotherim/anotherim-desktop.git" + }, + "original": { + "ref": "dev", + "type": "git", + "url": "https://dev.narayana.im/anotherim/anotherim-desktop.git" + } + }, "crane": { "locked": { "lastModified": 1765145449, @@ -79,11 +100,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771008912, - "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "lastModified": 1776877367, + "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", "type": "github" }, "original": { @@ -118,8 +139,10 @@ }, "root": { "inputs": { + "anotherim": "anotherim", "lanzaboote": "lanzaboote", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "sops-nix": "sops-nix" } }, "rust-overlay": { @@ -142,6 +165,26 @@ "repo": "rust-overlay", "type": "github" } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1777338324, + "narHash": "sha256-bc+ZZCmOTNq86/svGnw0tVpH7vJaLYvGLLKFYP08Q8E=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "8eaee5c45428b28b8c47a83e4c09dccec5f279b5", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index aca0c6e..fcf14e1 100644 --- a/flake.nix +++ b/flake.nix @@ -7,6 +7,14 @@ inputs.nixpkgs.follows = "nixpkgs"; }; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + anotherim = { + url = "git+https://dev.narayana.im/anotherim/anotherim-desktop.git?ref=dev"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = { self, nixpkgs, ... }@inputs: { @@ -15,6 +23,7 @@ specialArgs = { inherit inputs; }; modules = [ inputs.lanzaboote.nixosModules.lanzaboote + inputs.sops-nix.nixosModules.sops ./configuration.nix ]; }; diff --git a/hardware-configuration.nix b/hardware-configuration.nix index 8481282..63650d5 100644 --- a/hardware-configuration.nix +++ b/hardware-configuration.nix @@ -1,4 +1,4 @@ -# ThinkPad T14 G2(AMD) +# ThinkPad P14s Gen 6 AMD (Ryzen AI 7 PRO 350, Strix Point) # Do not modify this file! It was generated by ‘nixos-generate-config’ # and may be overwritten by future invocations. Please make changes @@ -10,7 +10,7 @@ [ (modulesPath + "/installer/scan/not-detected.nix") ]; - boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "usb_storage" "usbhid" "sd_mod" "rtsx_pci_sdmmc" ]; + boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "thunderbolt" ]; boot.initrd.kernelModules = [ ]; boot.kernelModules = [ "kvm-amd" ]; boot.extraModulePackages = [ ]; @@ -40,7 +40,11 @@ options = [ "nofail" ]; }; - swapDevices = [ ]; + # Encrypted swapfile on LUKS root, sized for hibernate (RAM + headroom). + # NixOS auto-creates the file and computes resume offset on activation. + swapDevices = [ + { device = "/var/swapfile"; size = 59392; } # 58 GiB, RAM is 54 GiB + ]; nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; diff --git a/modules/networking.nix b/modules/networking.nix index dc2af50..7731ff3 100644 --- a/modules/networking.nix +++ b/modules/networking.nix @@ -12,11 +12,12 @@ # Enable networking networking.networkmanager.enable = true; - # Quectel EM120R-GL WWAN Modem Support - # FCC unlock for Quectel EM120R-GL + # Quectel EM160R-GL WWAN Modem Support + # FCC unlock for Quectel EM160R-GL (USB ID 1eac:100d) + # Reuses the EM120R-GL (1eac:1001) script — same AT command across the family. networking.modemmanager.fccUnlockScripts = [ { - id = "1eac:1001"; + id = "1eac:100d"; path = "${pkgs.modemmanager}/share/ModemManager/fcc-unlock.available.d/1eac:1001"; } ]; @@ -24,6 +25,15 @@ # Kernel modules for WWAN/MBIM modems boot.kernelModules = [ "cdc_mbim" "qmi_wwan" "cdc_wdm" "mhi" ]; + # MediaTek MT7925 Wi-Fi: disable PCIe ASPM. + # Why: under high throughput (>~50 Mbit/s sustained) the PCIe link enters L1 + # power-saving mid-flow and the firmware queue stalls, causing TX to decay + # to zero and the driver to deauth (reason=3, locally_generated=1). + # Verified live by reloading mt7925e with disable_aspm=1. + boot.extraModprobeConfig = '' + options mt7925e disable_aspm=1 + ''; + # udev rules for WWAN devices services.udev.extraRules = '' KERNEL=="wwan*mbim*", MODE="0660", GROUP="networkmanager" @@ -97,8 +107,10 @@ }; }; - # IVPN service + # IVPN service — installed but not auto-started at boot. + # Start manually: `sudo systemctl start ivpn-service` services.ivpn.enable = true; + systemd.services.ivpn-service.wantedBy = lib.mkForce [ ]; # Open ports in the firewall. # networking.firewall.allowedTCPPorts = [ ... ]; diff --git a/modules/packages.nix b/modules/packages.nix index fed6642..0c3e51c 100644 --- a/modules/packages.nix +++ b/modules/packages.nix @@ -1,5 +1,5 @@ # System packages configuration -{ config, pkgs, lib, ... }: +{ config, pkgs, lib, inputs, ... }: { # Allow unfree packages @@ -64,6 +64,7 @@ # Desktop / GUI Apps gnome-tweaks obs-studio + inputs.anotherim.packages.${pkgs.system}.default # Development nodejs_20 @@ -106,6 +107,7 @@ libmbim libqmi modemmanager + lpac # eSIM Local Profile Assistant # Estonian ID / digital signature qdigidoc @@ -123,5 +125,8 @@ ivpn-service ivpn-ui xray + + # GNOME extensions + gnomeExtensions.tlp-profile-switcher ]; } diff --git a/modules/power.nix b/modules/power.nix index c0cc954..ecd00cf 100644 --- a/modules/power.nix +++ b/modules/power.nix @@ -1,9 +1,31 @@ -# Power management — ThinkPad T14 Gen 2 (AMD) +# Power management — ThinkPad P14s Gen 6 (AMD) { config, pkgs, lib, ... }: { # AMD P-State driver (active mode for full EPP control) - boot.kernelParams = [ "amd_pstate=active" ]; + # resume_offset = first physical block of /var/swapfile (for hibernate from + # an ext4 swapfile on LUKS). Recompute if the swapfile is recreated: + # sudo filefrag -v /var/swapfile | awk 'NR==4{gsub(/\.\./,""); print $4}' + boot.kernelParams = [ "amd_pstate=active" "resume_offset=1769472" ]; + + # Hibernate / resume from encrypted swapfile on LUKS root. + # P14s G6 AMD has no S3 (Modern Standby only); this enables + # suspend-then-hibernate so long sleeps drain to disk. + boot.resumeDevice = "/dev/mapper/luks-71c97ce7-dd29-4b07-8d2a-8cc3985a65bd"; + + # systemd-logind: when lid closes / suspend pressed, suspend first; + # if still asleep after the delay, hibernate. + services.logind.settings.Login = { + HandleLidSwitch = "suspend-then-hibernate"; + HandleSuspendKey = "suspend-then-hibernate"; + IdleAction = "suspend-then-hibernate"; + IdleActionSec = "30min"; + }; + + # Switch to hibernate after this long in s2idle. + systemd.sleep.settings.Sleep = { + HibernateDelaySec = "45min"; + }; # CPU frequency scaling powerManagement.cpuFreqGovernor = "powersave"; @@ -11,47 +33,93 @@ # Disable power-profiles-daemon (conflicts with TLP) services.power-profiles-daemon.enable = false; - # TLP — main power management daemon - services.tlp = { - enable = true; - settings = { - # CPU - CPU_SCALING_GOVERNOR_ON_AC = "performance"; - CPU_SCALING_GOVERNOR_ON_BAT = "powersave"; - CPU_ENERGY_PERF_POLICY_ON_AC = "balance_performance"; - CPU_ENERGY_PERF_POLICY_ON_BAT = "balance_power"; - CPU_BOOST_ON_AC = 1; - CPU_BOOST_ON_BAT = 1; - PLATFORM_PROFILE_ON_AC = "balanced"; - PLATFORM_PROFILE_ON_BAT = "balanced"; + # TLP — settings owned by the TLP Profile Switcher GNOME extension. + # The extension does `pkexec cp ~/.tlp/.conf /etc/tlp.conf`, but + # NixOS makes /etc/tlp.conf a read-only symlink into the Nix store. We + # replace the symlink with a writable copy on every activation, AND if + # one of the user's ~/.tlp/*.conf matches the previous /etc/tlp.conf + # (= the user picked it from the panel), restore that one instead of the + # NixOS stub. This way the chosen profile survives nixos-rebuild switch. + services.tlp.enable = true; + system.activationScripts.tlpWritable = lib.stringAfter [ "etc" ] '' + profilesDir=/home/asxpi/.tlp + target=/etc/tlp.conf + activeMarker=/var/lib/tlp-active-profile - # AMD P-State - CPU_DRIVER_OPMODE_ON_AC = "active"; - CPU_DRIVER_OPMODE_ON_BAT = "active"; + # If the current file matches one of the user's profiles, remember it + # before NixOS replaces the symlink target. + if [ -f "$target" ] && [ ! -L "$target" ] && [ -d "$profilesDir" ]; then + currentSum=$(${pkgs.coreutils}/bin/sha256sum "$target" | ${pkgs.coreutils}/bin/cut -d' ' -f1) + for p in "$profilesDir"/*.conf; do + [ -f "$p" ] || continue + s=$(${pkgs.coreutils}/bin/sha256sum "$p" | ${pkgs.coreutils}/bin/cut -d' ' -f1) + if [ "$s" = "$currentSum" ]; then + ${pkgs.coreutils}/bin/basename "$p" .conf > "$activeMarker" + break + fi + done + fi - # WiFi - WIFI_PWR_MGT_ON_AC = "off"; - WIFI_PWR_MGT_ON_BAT = "on"; + # Replace the read-only symlink with a writable real file. + if [ -L "$target" ]; then + cp --remove-destination "$(readlink -f "$target")" "$target" + chmod 0644 "$target" + fi - # PCIe ASPM - PCIE_ASPM_ON_AC = "default"; - PCIE_ASPM_ON_BAT = "default"; + # If we previously remembered an active profile, restore it. + if [ -f "$activeMarker" ]; then + active=$(cat "$activeMarker") + if [ -f "$profilesDir/$active.conf" ]; then + cp "$profilesDir/$active.conf" "$target" + chmod 0644 "$target" + fi + fi + ''; - # Runtime PM - RUNTIME_PM_ON_AC = "on"; - RUNTIME_PM_ON_BAT = "auto"; - - # NVMe - NVME_RUNTIME_PM_ON_AC = "on"; - NVME_RUNTIME_PM_ON_BAT = "auto"; - - # USB autosuspend - USB_AUTOSUSPEND = 1; + # Hook the extension's cp: whenever /etc/tlp.conf changes via the panel, + # also update the marker. systemd.path watches the file mtime. + systemd.paths.tlp-active-profile-tracker = { + description = "Track which TLP profile is currently active"; + wantedBy = [ "multi-user.target" ]; + pathConfig = { + PathChanged = "/etc/tlp.conf"; + Unit = "tlp-active-profile-tracker.service"; + }; + }; + systemd.services.tlp-active-profile-tracker = { + description = "Update /var/lib/tlp-active-profile to match /etc/tlp.conf"; + serviceConfig = { + Type = "oneshot"; + ExecStart = pkgs.writeShellScript "tlp-track" '' + profilesDir=/home/asxpi/.tlp + target=/etc/tlp.conf + marker=/var/lib/tlp-active-profile + [ -f "$target" ] || exit 0 + [ -d "$profilesDir" ] || exit 0 + currentSum=$(${pkgs.coreutils}/bin/sha256sum "$target" | ${pkgs.coreutils}/bin/cut -d' ' -f1) + for p in "$profilesDir"/*.conf; do + [ -f "$p" ] || continue + s=$(${pkgs.coreutils}/bin/sha256sum "$p" | ${pkgs.coreutils}/bin/cut -d' ' -f1) + if [ "$s" = "$currentSum" ]; then + ${pkgs.coreutils}/bin/basename "$p" .conf > "$marker" + exit 0 + fi + done + ''; }; }; # Bluetooth off at boot hardware.bluetooth.powerOnBoot = true; + hardware.bluetooth.settings = { + General = { + ControllerMode = "dual"; + FastConnectable = true; + JustWorksRepairing = "always"; + Privacy = "device"; + Experimental = true; + }; + }; # Battery monitoring services.upower.enable = true; diff --git a/modules/private/ssh-hosts.nix b/modules/private/ssh-hosts.nix new file mode 100644 index 0000000..30a6374 --- /dev/null +++ b/modules/private/ssh-hosts.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + # Stub. Real values kept locally via: + # git update-index --skip-worktree modules/private/ssh-hosts.nix + programs.ssh.extraConfig = ""; +} diff --git a/modules/private/xray-routes.nix b/modules/private/xray-routes.nix new file mode 100644 index 0000000..e5f5ace --- /dev/null +++ b/modules/private/xray-routes.nix @@ -0,0 +1,6 @@ +# Stub. Real values kept locally via: +# git update-index --skip-worktree modules/private/xray-routes.nix +{ + xrayServer = "0.0.0.0"; + routeExcludeAddress = [ ]; +} diff --git a/modules/programs.nix b/modules/programs.nix index 20d688e..93876e8 100644 --- a/modules/programs.nix +++ b/modules/programs.nix @@ -2,6 +2,8 @@ { config, pkgs, lib, ... }: { + imports = [ ./private/ssh-hosts.nix ]; + # GPG agent programs.gnupg.agent = { enable = true; @@ -49,6 +51,8 @@ # Enable the OpenSSH daemon. # services.openssh.enable = true; + # SSH client extraConfig is provided by ./private/ssh-hosts.nix + # Enable fwupd service services.fwupd.enable = true; diff --git a/modules/security.nix b/modules/security.nix index c3fa3ea..b753ac8 100644 --- a/modules/security.nix +++ b/modules/security.nix @@ -21,7 +21,10 @@ ]; # Kernel Hardening - security.protectKernelImage = true; + # Disabled to allow hibernate (suspend-then-hibernate on P14s G6 AMD which has no S3). + # Hibernate image lives on LUKS-encrypted root, so the tamper window is the same as + # the LUKS threat boundary. Revisit if TPM-bound LUKS unlock gets set up. + security.protectKernelImage = false; # Sysctl hardening boot.kernel.sysctl = { diff --git a/modules/wireguard.nix b/modules/wireguard.nix index 0786975..a676869 100644 --- a/modules/wireguard.nix +++ b/modules/wireguard.nix @@ -12,5 +12,9 @@ configFile = "/etc/wireguard/wg0.conf"; # TODO: Secret Manager. TODO: Add SSH, GPG as well to secret manager autostart = true; }; + wg2 = { + configFile = "/etc/wireguard/wg2.conf"; + autostart = true; + }; }; } diff --git a/modules/xray.nix b/modules/xray.nix index 88665c2..ffff68c 100644 --- a/modules/xray.nix +++ b/modules/xray.nix @@ -2,7 +2,8 @@ { config, pkgs, lib, ... }: let - xrayServer = "172.232.216.157"; + private = import ./private/xray-routes.nix; + xrayServer = private.xrayServer; in { # Xray — VLESS+Reality with post-quantum encryption @@ -32,12 +33,11 @@ in type = "tun"; tag = "tun-in"; interface_name = "tun0"; - address = [ "198.18.0.1/15" ]; + address = [ "198.18.0.1/15" "fdfe:dcba:9876::1/126" ]; auto_route = true; strict_route = true; - route_exclude_address = [ "${xrayServer}/32" ]; + route_exclude_address = [ "${xrayServer}/32" ] ++ private.routeExcludeAddress; stack = "gvisor"; - sniff = true; }]; outbounds = [ @@ -46,7 +46,7 @@ in tag = "xray"; server = "127.0.0.1"; server_port = 10808; - udp_over_tcp = true; + udp_over_tcp = false; } { type = "direct"; @@ -67,17 +67,15 @@ in type = "local"; } ]; - rules = [ - { - outbound = [ "any" ]; - server = "direct-dns"; - } - ]; }; route = { auto_detect_interface = true; + default_domain_resolver = "doh-proxy"; rules = [ + { + action = "sniff"; + } { protocol = "dns"; action = "hijack-dns";