From 73910674dc16b5bdc7ffed99c9b3e33cdfe6cda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 3 Sep 2024 08:57:21 +0200 Subject: [PATCH 1/7] restore-routes: introduce more type-safety and split up networkd unit generation --- nix/kexec-installer/restore_routes.py | 110 +++++++++++++++----------- 1 file changed, 65 insertions(+), 45 deletions(-) diff --git a/nix/kexec-installer/restore_routes.py b/nix/kexec-installer/restore_routes.py index 1bf1801..d0a0aeb 100644 --- a/nix/kexec-installer/restore_routes.py +++ b/nix/kexec-installer/restore_routes.py @@ -2,33 +2,49 @@ import json import sys from pathlib import Path from typing import Any +from dataclasses import dataclass -def filter_interfaces(network: list[dict[str, Any]]) -> list[dict[str, Any]]: - output = [] +@dataclass +class Interface: + name: str + ifname: str | None + mac_address: str + dynamic_addresses: list[str] + static_addresses: list[dict[str, Any]] + static_routes: list[dict[str, Any]] + + +def filter_interfaces(network: list[dict[str, Any]]) -> list[Interface]: + interfaces = [] for net in network: if net.get("link_type") == "loopback": continue - if not net.get("address"): + if not (mac_address := net.get("address")): # We need a mac address to match devices reliable continue - addr_info = [] - has_dynamic_address = False + static_addresses = [] + dynamic_addresses = [] for addr in net.get("addr_info", []): # no link-local ipv4/ipv6 if addr.get("scope") == "link": continue - # do not explicitly configure addresses from dhcp or router advertisement if addr.get("dynamic", False): - has_dynamic_address = True - continue + dynamic_addresses.append(addr["local"]) else: - addr_info.append(addr) - if addr_info != [] or has_dynamic_address: - net["addr_info"] = addr_info - output.append(net) + static_addresses.append(addr) + interfaces.append( + Interface( + name=net.get("ifname", mac_address.replace(":", "-")), + ifname=net.get("ifname"), + mac_address=mac_address, + dynamic_addresses=dynamic_addresses, + static_addresses=static_addresses, + static_routes=[], + ) + ) - return output + return interfaces def filter_routes(routes: list[dict[str, Any]]) -> list[dict[str, Any]]: @@ -42,44 +58,43 @@ def filter_routes(routes: list[dict[str, Any]]) -> list[dict[str, Any]]: return filtered +def generate_routes(interface: Interface, routes: list[dict[str, Any]]) -> list[str]: + route_sections = [] + for route in routes: + if interface.ifname is None or route.get("dev") != interface.ifname: + continue + + route_section = "[Route]\n" + if route.get("dst") != "default": + # can be skipped for default routes + route_section += f"Destination = {route['dst']}\n" + gateway = route.get("gateway") + # route v4 via v6 + route_via = route.get("via") + if route_via and route_via.get("family") == "inet6": + gateway = route_via.get("host") + if route.get("dst") == "default": + route_section += "Destination = 0.0.0.0/0\n" + if gateway: + route_section += f"Gateway = {gateway}\n" + + # we may ignore on-link default routes here, but I don't see how + # they would be useful for internet connectivity anyway + route_sections.append(route_section) + return route_sections + + def generate_networkd_units( - interfaces: list[dict[str, Any]], routes: list[dict[str, Any]], directory: Path + interfaces: list[Interface], routes: list[dict[str, Any]], directory: Path ) -> None: directory.mkdir(exist_ok=True) for interface in interfaces: - name = f"00-{interface['ifname']}.network" - addresses = [ - f"Address = {addr['local']}/{addr['prefixlen']}" - for addr in interface.get("addr_info", []) - ] - - route_sections = [] - for route in routes: - if route.get("dev", "nodev") != interface.get("ifname", "noif"): - continue - - route_section = "[Route]\n" - if route.get("dst") != "default": - # can be skipped for default routes - route_section += f"Destination = {route['dst']}\n" - gateway = route.get("gateway") - # route v4 via v6 - route_via = route.get("via") - if route_via and route_via.get("family") == "inet6": - gateway = route_via.get("host") - if route.get("dst") == "default": - route_section += "Destination = 0.0.0.0/0\n" - if gateway: - route_section += f"Gateway = {gateway}\n" - - # we may ignore on-link default routes here, but I don't see how - # they would be useful for internet connectivity anyway - route_sections.append(route_section) + name = f"00-{interface.name}.network" # FIXME in some networks we might not want to trust dhcp or router advertisements unit = f""" [Match] -MACAddress = {interface["address"]} +MACAddress = {interface.mac_address} [Network] # both ipv4 and ipv6 @@ -92,8 +107,13 @@ IPv6AcceptRA = yes MulticastDNS = yes """ - unit += "\n".join(addresses) - unit += "\n" + "\n".join(route_sections) + unit += "\n".join( + [ + f"Address = {addr['local']}/{addr['prefixlen']}" + for addr in interface.static_addresses + ] + ) + unit += "\n" + "\n".join(generate_routes(interface, routes)) (directory / name).write_text(unit) From c89ec7f957bf50ee8f5ed6e1bee2fe8703ef3389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 3 Sep 2024 09:04:47 +0200 Subject: [PATCH 2/7] restore-routes: just join list once with newlines --- nix/kexec-installer/restore_routes.py | 42 +++++++++++++-------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/nix/kexec-installer/restore_routes.py b/nix/kexec-installer/restore_routes.py index d0a0aeb..55df2f9 100644 --- a/nix/kexec-installer/restore_routes.py +++ b/nix/kexec-installer/restore_routes.py @@ -1,7 +1,7 @@ import json import sys from pathlib import Path -from typing import Any +from typing import Any, Iterator from dataclasses import dataclass @@ -58,30 +58,30 @@ def filter_routes(routes: list[dict[str, Any]]) -> list[dict[str, Any]]: return filtered -def generate_routes(interface: Interface, routes: list[dict[str, Any]]) -> list[str]: - route_sections = [] +def generate_routes( + interface: Interface, routes: list[dict[str, Any]] +) -> Iterator[str]: for route in routes: if interface.ifname is None or route.get("dev") != interface.ifname: continue - route_section = "[Route]\n" + # we may ignore on-link default routes here, but I don't see how + # they would be useful for internet connectivity anyway + + yield "[Route]" if route.get("dst") != "default": # can be skipped for default routes - route_section += f"Destination = {route['dst']}\n" + yield f"Destination = {route['dst']}" gateway = route.get("gateway") # route v4 via v6 route_via = route.get("via") if route_via and route_via.get("family") == "inet6": gateway = route_via.get("host") if route.get("dst") == "default": - route_section += "Destination = 0.0.0.0/0\n" + yield "Destination = 0.0.0.0/0" if gateway: - route_section += f"Gateway = {gateway}\n" + yield f"Gateway = {gateway}" - # we may ignore on-link default routes here, but I don't see how - # they would be useful for internet connectivity anyway - route_sections.append(route_section) - return route_sections def generate_networkd_units( @@ -89,10 +89,9 @@ def generate_networkd_units( ) -> None: directory.mkdir(exist_ok=True) for interface in interfaces: - name = f"00-{interface.name}.network" - # FIXME in some networks we might not want to trust dhcp or router advertisements - unit = f""" + unit_sections = [ + f""" [Match] MACAddress = {interface.mac_address} @@ -105,16 +104,15 @@ LLDP = yes IPv6AcceptRA = yes # allows us to ping "nixos.local" MulticastDNS = yes - """ - unit += "\n".join( - [ - f"Address = {addr['local']}/{addr['prefixlen']}" - for addr in interface.static_addresses - ] + ] + unit_sections.extend( + f"Address = {addr['local']}/{addr['prefixlen']}" + for addr in interface.static_addresses ) - unit += "\n" + "\n".join(generate_routes(interface, routes)) - (directory / name).write_text(unit) + unit_sections.extend(generate_routes(interface, routes)) + + (directory / f"00-{interface.name}.network").write_text("\n".join(unit_sections)) def main() -> None: From db9119b88afc907b5c82985662ad4e6b0d773117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 3 Sep 2024 09:25:50 +0200 Subject: [PATCH 3/7] try to restore previous ip lease --- nix/kexec-installer/restore_routes.py | 74 ++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/nix/kexec-installer/restore_routes.py b/nix/kexec-installer/restore_routes.py index 55df2f9..8bb997d 100644 --- a/nix/kexec-installer/restore_routes.py +++ b/nix/kexec-installer/restore_routes.py @@ -5,13 +5,22 @@ from typing import Any, Iterator from dataclasses import dataclass +@dataclass +class Address: + address: str + family: str + prefixlen: int + preferred_life_time: int = 0 + valid_life_time: int = 0 + + @dataclass class Interface: name: str ifname: str | None mac_address: str - dynamic_addresses: list[str] - static_addresses: list[dict[str, Any]] + dynamic_addresses: list[Address] + static_addresses: list[Address] static_routes: list[dict[str, Any]] @@ -25,14 +34,35 @@ def filter_interfaces(network: list[dict[str, Any]]) -> list[Interface]: continue static_addresses = [] dynamic_addresses = [] - for addr in net.get("addr_info", []): + for info in net.get("addr_info", []): # no link-local ipv4/ipv6 - if addr.get("scope") == "link": + if info.get("scope") == "link": continue - if addr.get("dynamic", False): - dynamic_addresses.append(addr["local"]) + if (preferred_life_time := info.get("preferred_life_time")) is None: + continue + if (valid_life_time := info.get("valid_life_time")) is None: + continue + if (prefixlen := info.get("prefixlen")) is None: + continue + if (family := info.get("family")) not in ["inet", "inet6"]: + continue + if (local := info.get("local")) is None: + continue + if (dynamic := info.get("dynamic", False)) is None: + continue + + address = Address( + address=local, + family=family, + prefixlen=prefixlen, + preferred_life_time=preferred_life_time, + valid_life_time=valid_life_time, + ) + + if dynamic: + dynamic_addresses.append(address) else: - static_addresses.append(addr) + static_addresses.append(address) interfaces.append( Interface( name=net.get("ifname", mac_address.replace(":", "-")), @@ -58,6 +88,19 @@ def filter_routes(routes: list[dict[str, Any]]) -> list[dict[str, Any]]: return filtered +def find_most_recent_v4_lease(addresses: list[Address]) -> Address | None: + most_recent_address = None + most_recent_lifetime = -1 + for addr in addresses: + if addr.family == "inet6": + continue + lifetime = max(addr.preferred_life_time, addr.valid_life_time) + if lifetime > most_recent_lifetime: + most_recent_lifetime = lifetime + most_recent_address = addr + return most_recent_address + + def generate_routes( interface: Interface, routes: list[dict[str, Any]] ) -> Iterator[str]: @@ -83,7 +126,6 @@ def generate_routes( yield f"Gateway = {gateway}" - def generate_networkd_units( interfaces: list[Interface], routes: list[dict[str, Any]], directory: Path ) -> None: @@ -103,16 +145,24 @@ LLDP = yes # ipv6 router advertisements IPv6AcceptRA = yes # allows us to ping "nixos.local" -MulticastDNS = yes -""" +MulticastDNS = yes""" ] unit_sections.extend( - f"Address = {addr['local']}/{addr['prefixlen']}" + f"Address = {addr.address}/{addr.prefixlen}" for addr in interface.static_addresses ) unit_sections.extend(generate_routes(interface, routes)) + most_recent_v4_lease = find_most_recent_v4_lease(interface.dynamic_addresses) + if most_recent_v4_lease: + unit_sections.append("[DHCPv4]") + unit_sections.append(f"RequestAddress = {most_recent_v4_lease.address}") - (directory / f"00-{interface.name}.network").write_text("\n".join(unit_sections)) + # trailing newline at the end + unit_sections.append("") + + (directory / f"00-{interface.name}.network").write_text( + "\n".join(unit_sections) + ) def main() -> None: From 46cd291c605f0f541c992b9744184904be7803b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 3 Sep 2024 09:28:12 +0200 Subject: [PATCH 4/7] avoid toplevel python script in nix-store this will trigger a os.listdir() on the nix-store --- nix/kexec-installer/module.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/kexec-installer/module.nix b/nix/kexec-installer/module.nix index a10e6a5..4bd1511 100644 --- a/nix/kexec-installer/module.nix +++ b/nix/kexec-installer/module.nix @@ -1,6 +1,6 @@ { config, lib, modulesPath, pkgs, ... }: let - restore-network = pkgs.writers.writePython3 "restore-network" { flakeIgnore = [ "E501" ]; } + restore-network = pkgs.writers.writePython3Bin "restore-network" { flakeIgnore = [ "E501" ]; } ./restore_routes.py; # does not link with iptables enabled @@ -64,7 +64,7 @@ in Type = "oneshot"; RemainAfterExit = true; ExecStart = [ - "${restore-network} /root/network/addrs.json /root/network/routes-v4.json /root/network/routes-v6.json /etc/systemd/network" + "${restore-network}/bin/restore-network /root/network/addrs.json /root/network/routes-v4.json /root/network/routes-v6.json /etc/systemd/network" ]; }; From b1e43cae8bfbad546fbe560c71c698d6526f8785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 3 Sep 2024 09:52:20 +0200 Subject: [PATCH 5/7] add script to gets network restore locally --- .../local-network-restore-test.sh | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100755 nix/kexec-installer/local-network-restore-test.sh diff --git a/nix/kexec-installer/local-network-restore-test.sh b/nix/kexec-installer/local-network-restore-test.sh new file mode 100755 index 0000000..447ace9 --- /dev/null +++ b/nix/kexec-installer/local-network-restore-test.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env -S nix shell --inputs-from .# nixos-unstable#bash nixos-unstable#iproute2 nixos-unstable#findutils nixos-unstable#coreutils nixos-unstable#python3 nixos-unstable#jq --command bash + +set -eu +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + +# This script can be used to see what network configuration would be restored by the restore_routes.py script for the current system. + +tmp=$(mktemp -d) +trap "rm -rf $tmp" EXIT +ip --json address >"$tmp/addrs.json" +ip -6 --json route >"$tmp/routes-v6.json" +ip -4 --json route >"$tmp/routes-v4.json" +python3 "$SCRIPT_DIR/restore_routes.py" "$tmp/addrs.json" "$tmp/routes-v4.json" "$tmp/routes-v6.json" "$tmp" +ls -la "$tmp" + +find "$tmp" -type f -name "*.json" -print0 | while IFS= read -r -d '' file; do + echo -e "\033[0;31m$(basename "$file")\033[0m" + jq . "$file" + echo "" +done + +find "$tmp" -type f -name "*.network" -print0 | while IFS= read -r -d '' file; do + echo -e "\033[0;31m$(basename "$file")\033[0m" + cat "$file" + echo "" +done From 23f987d2aff982f98266dbdfab6d35ce6ed9e8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 3 Sep 2024 11:03:51 +0200 Subject: [PATCH 6/7] add workaround for nvidia --- nix/image-installer/module.nix | 3 ++- nix/noninteractive.nix | 1 + nix/noveau-workaround.nix | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 nix/noveau-workaround.nix diff --git a/nix/image-installer/module.nix b/nix/image-installer/module.nix index 07a1c5b..44a2100 100644 --- a/nix/image-installer/module.nix +++ b/nix/image-installer/module.nix @@ -40,8 +40,9 @@ in imports = [ (modulesPath + "/installer/cd-dvd/installation-cd-base.nix") ../installer.nix - ./wifi.nix + ../noveau-workaround.nix ./hidden-ssh-announcement.nix + ./wifi.nix ]; systemd.tmpfiles.rules = [ "d /var/shared 0777 root root - -" ]; services.openssh.settings.PermitRootLogin = "yes"; diff --git a/nix/noninteractive.nix b/nix/noninteractive.nix index 54690dc..2e2d888 100644 --- a/nix/noninteractive.nix +++ b/nix/noninteractive.nix @@ -12,6 +12,7 @@ imports = [ ./zfs-minimal.nix ./no-bootloaders.nix + ./noveau-workaround.nix # reduce closure size by removing perl "${modulesPath}/profiles/perlless.nix" # FIXME: we still are left with nixos-generate-config due to nixos-install-tools diff --git a/nix/noveau-workaround.nix b/nix/noveau-workaround.nix new file mode 100644 index 0000000..5865852 --- /dev/null +++ b/nix/noveau-workaround.nix @@ -0,0 +1,4 @@ +{ + # fixes blank screen on boot for some cards + boot.kernelParams = [ "nouveau.modeset=0" ]; +} From b81c6e0ace8c3109352be28bb9ed9a856a215ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 3 Sep 2024 11:30:32 +0200 Subject: [PATCH 7/7] default to root for autologin in nixos kexec image --- nix/noninteractive.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/noninteractive.nix b/nix/noninteractive.nix index 2e2d888..ee87048 100644 --- a/nix/noninteractive.nix +++ b/nix/noninteractive.nix @@ -36,10 +36,14 @@ users.users.nixos = { isSystemUser = true; isNormalUser = lib.mkForce false; + shell = "/run/current-system/sw/bin/bash"; group = "nixos"; }; users.groups.nixos = {}; + # we prefer root as this is also what we use in nixos-anywhere + services.getty.autologinUser = lib.mkForce "root"; + # we are missing this from base.nix boot.supportedFilesystems = [ "btrfs"