From 835b7f6df1f5488c6cb463ab4ac13160a415377d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ryan4yin=20=7C=20=E4=BA=8C=E8=8A=B1?= Date: Wed, 7 Oct 2026 21:27:34 +0800 Subject: [PATCH] feat(shoukei): pin home Wi-Fi to a static IPv4 & adjust networking.nix (#407) --- Justfile | 6 ++ hosts/12kingdoms-shoukei/README.md | 6 +- modules/nixos/base/monitoring.nix | 8 +- scripts/shoukei-home-wifi-static.nu | 149 ++++++++++++++++++++++++++++ vars/networking.nix | 138 ++++++++++++++------------ 5 files changed, 237 insertions(+), 70 deletions(-) create mode 100644 scripts/shoukei-home-wifi-static.nu diff --git a/Justfile b/Justfile index 344e3610..f9870463 100644 --- a/Justfile +++ b/Justfile @@ -157,6 +157,12 @@ niri mode="switch" verbosity="normal": use {{utils_nu}} *; nixos-switch $"(hostname)-niri" {{mode}} {{verbosity}} +# Pin shoukei's home Wi-Fi to a static IPv4 (run once; the profile persists). +[linux] +[group('desktop')] +shoukei-home-wifi: + sudo nu {{absolute_path("scripts/shoukei-home-wifi-static.nu")}} + ############################################################################ # # Darwin related commands diff --git a/hosts/12kingdoms-shoukei/README.md b/hosts/12kingdoms-shoukei/README.md index 02be51fe..1dd7ed0e 100644 --- a/hosts/12kingdoms-shoukei/README.md +++ b/hosts/12kingdoms-shoukei/README.md @@ -2,8 +2,10 @@ This is NixOS's configuration for my Macbook Pro 2022 M2, 16G RAM. -This laptop joins untrusted networks and uses DHCP instead of a static IP, so it has no entry in -`vars/networking.nix`. +This laptop joins untrusted networks and uses DHCP by default. On the home Wi-Fi +(`shadow_light_ryan`) run `just shoukei-home-wifi` to pin `192.168.5.108`; the recipe calls +[`scripts/shoukei-home-wifi-static.nu`](../../scripts/shoukei-home-wifi-static.nu), and +`vars/networking.nix` records the address with `homeOnly = true`. ## x86/x86_64 applications diff --git a/modules/nixos/base/monitoring.nix b/modules/nixos/base/monitoring.nix index 0b402668..e058c00b 100644 --- a/modules/nixos/base/monitoring.nix +++ b/modules/nixos/base/monitoring.nix @@ -7,9 +7,11 @@ let # Exporters bind to this host's static LAN IPv4 (the same address # VictoriaMetrics scrapes), so the socket exists only on the LAN interface — - # defence in depth behind the firewall allowlist. Hosts without a static LAN - # address (laptops like shoukei, DHCP VMs like akane) run no exporters. - lanAddr = myvars.networking.hostsAddr.${config.networking.hostName}.ipv4 or null; + # defence in depth behind the firewall allowlist. Hosts without an always-on + # static LAN address (the `homeOnly` laptop, DHCP VMs like akane) run no + # exporters. + hostAddr = myvars.networking.hostsAddr.${config.networking.hostName} or { }; + lanAddr = if hostAddr.homeOnly or false then null else hostAddr.ipv4 or null; in { # enable the node exporter on all nixos hosts diff --git a/scripts/shoukei-home-wifi-static.nu b/scripts/shoukei-home-wifi-static.nu new file mode 100644 index 00000000..6d4d8694 --- /dev/null +++ b/scripts/shoukei-home-wifi-static.nu @@ -0,0 +1,149 @@ +#!/usr/bin/env nu +# +# Pin shoukei's home Wi-Fi profile to a static IPv4. +# +# NetworkManager matches connections by SSID. Run this ONCE, while connected to +# HOME_SSID: the script refuses to run unless the current Wi-Fi network is that +# exact SSID, so it can never pin the wrong network by accident. Every other +# network keeps using DHCP. The change is stored in the profile under +# /etc/NetworkManager/system-connections (preserved by impermanence), so it +# survives reboots and rebuilds -- no need to run it again. +# +# just shoukei-home-wifi +# +# See hosts/12kingdoms-shoukei/README.md and WORKAROUNDS.md WA-024/025. + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +# The home network: 2.4 and 5 GHz are merged into this single SSID, so it is +# the only network this script may touch. +const HOME_SSID = "shadow_light_ryan" + +# The address to pin on that SSID. +const STATIC = { + address: "192.168.5.108/24" + gateway: "192.168.5.1" + dns: "192.168.5.1" +} + +# --------------------------------------------------------------------------- +# nmcli helpers +# --------------------------------------------------------------------------- + +# Run nmcli; abort the script if it exits non-zero. +def nmcli [args: list] { + let result = (^nmcli ...$args | complete) + if $result.exit_code != 0 { + error make { msg: $"nmcli ($args | str join ' ') failed: ($result.stderr | str trim)" } + } + $result.stdout | str trim +} + +# Names of the connections that are active right now. +def active-connections [] { + nmcli ["-t" "-f" "NAME,ACTIVE" "connection" "show"] + | lines + | where { |line| ($line | split row ":" | last) == "yes" } + | each { |line| $line | split row ":" | first } +} + +# Every Wi-Fi connection as a { name, ssid } record. +def wifi-profiles [] { + nmcli ["-t" "-f" "NAME" "connection" "show"] + | lines + | where { |name| ($name | str trim) != "" } + | each { |name| + if (nmcli ["-g" "connection.type" "connection" "show" $name]) == "802-11-wireless" { + { name: $name, ssid: (nmcli ["-g" "802-11-wireless.ssid" "connection" "show" $name]) } + } + } + | compact +} + +# --------------------------------------------------------------------------- +# Guards +# --------------------------------------------------------------------------- + +def assert-root [] { + if (^id -u | str trim | into int) != 0 { + error make { msg: "must run as root -- nmcli edits system connections; try: just shoukei-home-wifi" } + } +} + +# Abort unless Wi-Fi is connected to HOME_SSID right now. +def assert-on-home [active: list, wifi: list] { + let connected = ($wifi | where { |p| $p.name in $active }) + let current = (if ($connected | is-empty) { [] } else { $connected | get ssid }) + if ($current | is-empty) { + error make { msg: $"not connected to Wi-Fi -- connect to '($HOME_SSID)' first" } + } + if (not ($HOME_SSID in $current)) { + error make { msg: $"not connected to '($HOME_SSID)'; currently on ($current | str join ', ')" } + } +} + +# --------------------------------------------------------------------------- +# Profile operations +# --------------------------------------------------------------------------- + +# The profile names that carry HOME_SSID. +def home-profiles [wifi: list] { + $wifi | where { |p| $p.ssid == $HOME_SSID } | get name +} + +def set-static-ip [profile: string] { + nmcli [ + "connection" "modify" $profile + "ipv4.method" "manual" + "ipv4.addresses" $STATIC.address + "ipv4.gateway" $STATIC.gateway + "ipv4.dns" $STATIC.dns + ] | ignore +} + +# Apply the new settings without dropping the link (avoids the brcmfmac +# re-auth/password re-prompt, WA-024/025); reconnect only as a fallback. +def apply-live [profile: string] { + let device = (nmcli ["-g" "GENERAL.DEVICES" "connection" "show" $profile]) + if ($device | is-empty) { return } + + let result = (^nmcli device reapply $device | complete) + if $result.exit_code == 0 { + print $" applied on ($device) via reapply, no reconnect" + } else { + print $" reapply failed, reconnecting '($profile)'" + nmcli ["connection" "up" $profile] | ignore + } +} + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main [] { + assert-root + + let active = (active-connections) + let wifi = (wifi-profiles) + assert-on-home $active $wifi + + let profiles = (home-profiles $wifi) + if ($profiles | is-empty) { + error make { msg: $"no Wi-Fi profile for SSID '($HOME_SSID)'" } + } + + print $"pinning '($HOME_SSID)': ($STATIC.address) via ($STATIC.gateway), dns ($STATIC.dns)" + for p in $profiles { + print $" set '($p)'" + set-static-ip $p + } + + for p in $profiles { + if $p in $active { apply-live $p } + } + + print "" + print "done. check: nmcli -f NAME,ipv4.method,ipv4.addresses connection show; ip -4 addr show wld0" +} diff --git a/vars/networking.nix b/vars/networking.nix index 504b889b..70986e26 100644 --- a/vars/networking.nix +++ b/vars/networking.nix @@ -36,39 +36,19 @@ rec { }; }; + # Static LAN addresses, sorted by IPv4 ascending -- keep it sorted when adding + # a host so the next free address is obvious. `homeOnly = true` marks an + # address that only exists at home (the laptop's home Wi-Fi static); ssh and + # monitoring skip those entries. hostsAddr = { - # ============================================ - # Homelab's Physical Machines (VM hosts) - # ============================================ - shoryu = { - iface = "eno1"; - ipv4 = "192.168.5.181"; - }; - shushou = { - iface = "eno1"; - ipv4 = "192.168.5.182"; - }; - youko = { - iface = "eno1"; - ipv4 = "192.168.5.183"; - }; - - # ============================================ - # Other VMs and Physical Machines - # ============================================ ai = { # Desktop PC iface = "enp130s0"; ipv4 = "192.168.5.100"; ipv6 = "fe80::10"; # Link-local Address }; - # akane = { - # # VM (running in macOS's UTM App), using DHCP instead of static ip. - # iface = "enp0s1"; - # ipv4 = "192.168.64.2"; - # }; aquamarine = { - # VM + # VM (retired) iface = "enp2s0"; ipv4 = "192.168.5.101"; }; @@ -102,40 +82,12 @@ rec { iface = "end1"; ipv4 = "192.168.5.107"; }; - rakushun = { - # Orange Pi 5 - ARM - # RJ45 port 1 - enP4p65s0 - # RJ45 port 2 - enP3p49s0 - iface = "enP4p65s0"; - ipv4 = "192.168.5.179"; - }; - suzi = { - iface = "enp2s0"; # fake iface, it's not used by the host - ipv4 = "192.168.5.178"; - ipv6 = "fe80::8"; # Link-local Address, can be used as default gateway - }; - mitsuha = { - iface = "enp2s0"; # fake iface, it's not used by the host - ipv4 = "192.168.5.177"; - }; - - # ============================================ - # Kubernetes Clusters - # ============================================ - k3s-test-1-master-1 = { - # VM - iface = "enp2s0"; - ipv4 = "192.168.5.114"; - }; - k3s-test-1-master-2 = { - # VM - iface = "enp2s0"; - ipv4 = "192.168.5.115"; - }; - k3s-test-1-master-3 = { - # VM - iface = "enp2s0"; - ipv4 = "192.168.5.116"; + shoukei = { + # Laptop: pinned on the home Wi-Fi only, by `just shoukei-home-wifi`; off + # the home network it uses DHCP. + iface = "wld0"; + ipv4 = "192.168.5.108"; + homeOnly = true; }; k3s-test-1-worker-1 = { # VM @@ -152,6 +104,57 @@ rec { iface = "enp2s0"; ipv4 = "192.168.5.113"; }; + k3s-test-1-master-1 = { + # VM + iface = "enp2s0"; + ipv4 = "192.168.5.114"; + }; + k3s-test-1-master-2 = { + # VM + iface = "enp2s0"; + ipv4 = "192.168.5.115"; + }; + k3s-test-1-master-3 = { + # VM + iface = "enp2s0"; + ipv4 = "192.168.5.116"; + }; + mitsuha = { + iface = "enp2s0"; # fake iface, it's not used by the host + ipv4 = "192.168.5.177"; + }; + suzi = { + iface = "enp2s0"; # fake iface, it's not used by the host + ipv4 = "192.168.5.178"; + ipv6 = "fe80::8"; # Link-local Address, can be used as default gateway + }; + rakushun = { + # Orange Pi 5 - ARM, now running Armbian (not NixOS) + # RJ45 port 1 - enP4p65s0 + # RJ45 port 2 - enP3p49s0 + iface = "enP4p65s0"; + ipv4 = "192.168.5.179"; + }; + shoryu = { + # Homelab's physical machine (VM host) + iface = "eno1"; + ipv4 = "192.168.5.181"; + }; + shushou = { + # Homelab's physical machine (VM host) + iface = "eno1"; + ipv4 = "192.168.5.182"; + }; + youko = { + # Homelab's physical machine (VM host) + iface = "eno1"; + ipv4 = "192.168.5.183"; + }; + # akane = { + # # VM (running in macOS's UTM App), using DHCP instead of static ip. + # iface = "enp0s1"; + # ipv4 = "192.168.64.2"; + # }; }; ssh = { @@ -164,15 +167,20 @@ rec { # IdentityFile — the location of your SSH key authentication file for the account. # Format in details: # https://www.ssh.com/academy/ssh/config + # `homeOnly` entries get no alias: their address is not reachable off the + # home LAN, so an alias would break e.g. `ssh shoukei` over Tailscale. extraConfig = ( lib.attrsets.foldlAttrs ( acc: host: val: - acc - + '' - Host ${host} - HostName ${val.ipv4} - Port 22 - '' + if val.homeOnly or false then + acc + else + acc + + '' + Host ${host} + HostName ${val.ipv4} + Port 22 + '' ) "" hostsAddr );