Merge pull request #272 from nix-community/dhcp

Better dhcp support
This commit is contained in:
Jörg Thalheim 2024-09-03 11:49:40 +02:00 committed by GitHub
commit e8b6d35f6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 161 additions and 57 deletions

View file

@ -40,8 +40,9 @@ in
imports = [ imports = [
(modulesPath + "/installer/cd-dvd/installation-cd-base.nix") (modulesPath + "/installer/cd-dvd/installation-cd-base.nix")
../installer.nix ../installer.nix
./wifi.nix ../noveau-workaround.nix
./hidden-ssh-announcement.nix ./hidden-ssh-announcement.nix
./wifi.nix
]; ];
systemd.tmpfiles.rules = [ "d /var/shared 0777 root root - -" ]; systemd.tmpfiles.rules = [ "d /var/shared 0777 root root - -" ];
services.openssh.settings.PermitRootLogin = "yes"; services.openssh.settings.PermitRootLogin = "yes";

View file

@ -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

View file

@ -1,6 +1,6 @@
{ config, lib, modulesPath, pkgs, ... }: { config, lib, modulesPath, pkgs, ... }:
let let
restore-network = pkgs.writers.writePython3 "restore-network" { flakeIgnore = [ "E501" ]; } restore-network = pkgs.writers.writePython3Bin "restore-network" { flakeIgnore = [ "E501" ]; }
./restore_routes.py; ./restore_routes.py;
# does not link with iptables enabled # does not link with iptables enabled
@ -64,7 +64,7 @@ in
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; RemainAfterExit = true;
ExecStart = [ 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"
]; ];
}; };

View file

@ -1,34 +1,80 @@
import json import json
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Iterator
from dataclasses import dataclass
def filter_interfaces(network: list[dict[str, Any]]) -> list[dict[str, Any]]: @dataclass
output = [] 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[Address]
static_addresses: list[Address]
static_routes: list[dict[str, Any]]
def filter_interfaces(network: list[dict[str, Any]]) -> list[Interface]:
interfaces = []
for net in network: for net in network:
if net.get("link_type") == "loopback": if net.get("link_type") == "loopback":
continue continue
if not net.get("address"): if not (mac_address := net.get("address")):
# We need a mac address to match devices reliable # We need a mac address to match devices reliable
continue continue
addr_info = [] static_addresses = []
has_dynamic_address = False dynamic_addresses = []
for addr in net.get("addr_info", []): for info in net.get("addr_info", []):
# no link-local ipv4/ipv6 # no link-local ipv4/ipv6
if addr.get("scope") == "link": if info.get("scope") == "link":
continue continue
# do not explicitly configure addresses from dhcp or router advertisement if (preferred_life_time := info.get("preferred_life_time")) is None:
if addr.get("dynamic", False): continue
has_dynamic_address = True 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 continue
else:
addr_info.append(addr)
if addr_info != [] or has_dynamic_address:
net["addr_info"] = addr_info
output.append(net)
return output 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(address)
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 interfaces
def filter_routes(routes: list[dict[str, Any]]) -> list[dict[str, Any]]: def filter_routes(routes: list[dict[str, Any]]) -> list[dict[str, Any]]:
@ -42,44 +88,54 @@ def filter_routes(routes: list[dict[str, Any]]) -> list[dict[str, Any]]:
return filtered return filtered
def generate_networkd_units( def find_most_recent_v4_lease(addresses: list[Address]) -> Address | None:
interfaces: list[dict[str, Any]], routes: list[dict[str, Any]], directory: Path most_recent_address = None
) -> None: most_recent_lifetime = -1
directory.mkdir(exist_ok=True) for addr in addresses:
for interface in interfaces: if addr.family == "inet6":
name = f"00-{interface['ifname']}.network" continue
addresses = [ lifetime = max(addr.preferred_life_time, addr.valid_life_time)
f"Address = {addr['local']}/{addr['prefixlen']}" if lifetime > most_recent_lifetime:
for addr in interface.get("addr_info", []) most_recent_lifetime = lifetime
] most_recent_address = addr
return most_recent_address
route_sections = []
def generate_routes(
interface: Interface, routes: list[dict[str, Any]]
) -> Iterator[str]:
for route in routes: for route in routes:
if route.get("dev", "nodev") != interface.get("ifname", "noif"): if interface.ifname is None or route.get("dev") != interface.ifname:
continue 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": if route.get("dst") != "default":
# can be skipped for default routes # can be skipped for default routes
route_section += f"Destination = {route['dst']}\n" yield f"Destination = {route['dst']}"
gateway = route.get("gateway") gateway = route.get("gateway")
# route v4 via v6 # route v4 via v6
route_via = route.get("via") route_via = route.get("via")
if route_via and route_via.get("family") == "inet6": if route_via and route_via.get("family") == "inet6":
gateway = route_via.get("host") gateway = route_via.get("host")
if route.get("dst") == "default": if route.get("dst") == "default":
route_section += "Destination = 0.0.0.0/0\n" yield "Destination = 0.0.0.0/0"
if gateway: 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)
def generate_networkd_units(
interfaces: list[Interface], routes: list[dict[str, Any]], directory: Path
) -> None:
directory.mkdir(exist_ok=True)
for interface in interfaces:
# FIXME in some networks we might not want to trust dhcp or router advertisements # FIXME in some networks we might not want to trust dhcp or router advertisements
unit = f""" unit_sections = [
f"""
[Match] [Match]
MACAddress = {interface["address"]} MACAddress = {interface.mac_address}
[Network] [Network]
# both ipv4 and ipv6 # both ipv4 and ipv6
@ -89,12 +145,24 @@ LLDP = yes
# ipv6 router advertisements # ipv6 router advertisements
IPv6AcceptRA = yes IPv6AcceptRA = yes
# allows us to ping "nixos.local" # allows us to ping "nixos.local"
MulticastDNS = yes MulticastDNS = yes"""
]
unit_sections.extend(
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}")
""" # trailing newline at the end
unit += "\n".join(addresses) unit_sections.append("")
unit += "\n" + "\n".join(route_sections)
(directory / name).write_text(unit) (directory / f"00-{interface.name}.network").write_text(
"\n".join(unit_sections)
)
def main() -> None: def main() -> None:

View file

@ -12,6 +12,7 @@
imports = [ imports = [
./zfs-minimal.nix ./zfs-minimal.nix
./no-bootloaders.nix ./no-bootloaders.nix
./noveau-workaround.nix
# reduce closure size by removing perl # reduce closure size by removing perl
"${modulesPath}/profiles/perlless.nix" "${modulesPath}/profiles/perlless.nix"
# FIXME: we still are left with nixos-generate-config due to nixos-install-tools # FIXME: we still are left with nixos-generate-config due to nixos-install-tools
@ -35,10 +36,14 @@
users.users.nixos = { users.users.nixos = {
isSystemUser = true; isSystemUser = true;
isNormalUser = lib.mkForce false; isNormalUser = lib.mkForce false;
shell = "/run/current-system/sw/bin/bash";
group = "nixos"; group = "nixos";
}; };
users.groups.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 # we are missing this from base.nix
boot.supportedFilesystems = [ boot.supportedFilesystems = [
"btrfs" "btrfs"

View file

@ -0,0 +1,4 @@
{
# fixes blank screen on boot for some cards
boot.kernelParams = [ "nouveau.modeset=0" ];
}