From 34ddf5d54e4dfe25d6d1d0a712f34501731024f1 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Sat, 20 Jun 2026 23:16:41 -0400 Subject: [PATCH] Fix NetworkManager transition during upgrade --- bin/omarchy-dns | 266 ++++++++++++++---- bin/omarchy-upgrade-to-4 | 18 +- install/hardware/network.sh | 43 ++- migrations/1782002156.sh | 88 ++++++ .../network-manager-transition-test.sh | 35 +++ test/shell.d/upgrade-to-4-test.sh | 6 + 6 files changed, 402 insertions(+), 54 deletions(-) create mode 100644 migrations/1782002156.sh create mode 100644 test/shell.d/network-manager-transition-test.sh diff --git a/bin/omarchy-dns b/bin/omarchy-dns index 928743bf..61b97ca5 100755 --- a/bin/omarchy-dns +++ b/bin/omarchy-dns @@ -6,11 +6,69 @@ set -euo pipefail -current_dns_provider() { - local dns="" - local compact="" +NM_DNS_CONF=/etc/NetworkManager/conf.d/20-omarchy-dns.conf - dns=$(awk -F= ' +provider_from_arg() { + case "${1:-}" in + Cloudflare | cloudflare) + echo "Cloudflare" + ;; + Google | google) + echo "Google" + ;; + DHCP | dhcp) + echo "DHCP" + ;; + Custom | custom) + echo "Custom" + ;; + *) + return 1 + ;; + esac +} + +self_path() { + local self="${BASH_SOURCE[0]}" + + if [[ $self != /* ]]; then + self=$(command -v -- "$self") + fi + + readlink -f "$self" +} + +require_root() { + if (( EUID == 0 )); then + return + fi + + if ! command -v pkexec >/dev/null; then + echo "Error: pkexec is required to change DNS settings." >&2 + exit 1 + fi + + exec pkexec "$(self_path)" "$@" +} + +networkmanager_global_dns() { + [[ -f $NM_DNS_CONF ]] || return 0 + + awk -F= ' + /^[[:space:]]*#/ { next } + /^[[:space:]]*\[global-dns-domain-\*\][[:space:]]*$/ { in_default = 1; next } + /^[[:space:]]*\[/ { in_default = 0 } + in_default && /^[[:space:]]*servers[[:space:]]*=/ { + value = $0 + sub(/^[^=]*=/, "", value) + print value + exit + } + ' "$NM_DNS_CONF" +} + +resolved_dns() { + awk -F= ' /^[[:space:]]*#/ { next } /^[[:space:]]*DNS[[:space:]]*=/ { value=$0 @@ -18,9 +76,19 @@ current_dns_provider() { print value exit } - ' /etc/systemd/resolved.conf 2>/dev/null || true) + ' /etc/systemd/resolved.conf 2>/dev/null || true +} - compact=$(printf '%s' "$dns" | tr -d '[:space:]') +current_dns_provider() { + local dns="" + local compact="" + + dns=$(networkmanager_global_dns) + if [[ -z $(printf '%s' "$dns" | tr -d '[:space:],') ]]; then + dns=$(resolved_dns) + fi + + compact=$(printf '%s' "$dns" | tr -d '[:space:],') if [[ -z $compact ]]; then echo "DHCP" @@ -33,37 +101,129 @@ current_dns_provider() { fi } -require_root() { - if (( EUID != 0 )); then - exec pkexec omarchy-dns "$@" +normalize_servers() { + printf '%s\n' "$*" | tr ',\t\n' ' ' | xargs | tr ' ' ',' +} + +split_dns_servers() { + local servers="$1" + local server clean + ipv4_dns="" + ipv6_dns="" + + for server in ${servers//,/ }; do + clean=${server#dns+tls://} + clean=${clean#dns+udp://} + clean=${clean%%#*} + clean=${clean#[} + clean=${clean%]} + + [[ -n $clean ]] || continue + if [[ $clean == *:* ]]; then + ipv6_dns+="${ipv6_dns:+ }$clean" + else + ipv4_dns+="${ipv4_dns:+ }$clean" + fi + done +} + +write_networkmanager_dns() { + local servers="$1" + + install -d -m 0755 "$(dirname "$NM_DNS_CONF")" + cat >"$NM_DNS_CONF" </dev/null 2>&1 || return 0 + + while IFS=: read -r uuid type; do + [[ -n $uuid ]] || continue + networkmanager_dns_connection "$type" || continue + + nmcli connection modify "$uuid" \ + ipv4.ignore-auto-dns yes \ + ipv4.dns "$ipv4_dns" \ + ipv6.ignore-auto-dns yes \ + ipv6.dns "$ipv6_dns" \ + >/dev/null + done < <(nmcli -t -f UUID,TYPE connection show) +} + +clear_connection_dns() { + local uuid type + + command -v nmcli >/dev/null 2>&1 || return 0 + + while IFS=: read -r uuid type; do + [[ -n $uuid ]] || continue + networkmanager_dns_connection "$type" || continue + + nmcli connection modify "$uuid" \ + ipv4.ignore-auto-dns no \ + ipv4.dns "" \ + ipv6.ignore-auto-dns no \ + ipv6.dns "" \ + >/dev/null + done < <(nmcli -t -f UUID,TYPE connection show) +} + +reapply_active_dns_connections() { + local device type state + + command -v nmcli >/dev/null 2>&1 || return 0 + + while IFS=: read -r device type state; do + [[ -n $device && $state == connected ]] || continue + case "$type" in + wifi|ethernet) + nmcli device reapply "$device" >/dev/null 2>&1 || true + ;; + esac + done < <(nmcli -t -f DEVICE,TYPE,STATE device status) +} + +reload_dns_stack() { + if systemctl is-active --quiet NetworkManager.service 2>/dev/null; then + # Load the updated NetworkManager config first, then reapply the active + # profiles. A single conf,dns-full reload here pushes the old active DNS + # settings, making the shell toggle appear one selection behind. + nmcli general reload conf >/dev/null 2>&1 || systemctl reload NetworkManager.service 2>/dev/null || true + reapply_active_dns_connections + fi + + systemctl reload systemd-resolved.service 2>/dev/null || systemctl restart systemd-resolved.service + + if systemctl is-active --quiet NetworkManager.service 2>/dev/null; then + # A resolved reload/restart can leave per-link DNS stale or empty; ask + # NetworkManager to publish DNS after resolved has reread its config. + nmcli general reload dns-full >/dev/null 2>&1 || true fi } -lock_dns_to_resolved() { - local file="" - - for file in /etc/systemd/network/*.network; do - [[ -f $file ]] || continue - if ! grep -q "^\[DHCPv4\]" "$file"; then continue; fi - - if ! sed -n '/^\[DHCPv4\]/,/^\[/p' "$file" | grep -q "^UseDNS="; then - sed -i '/^\[DHCPv4\]/a UseDNS=no' "$file" - fi - - if grep -q "^\[IPv6AcceptRA\]" "$file" && ! sed -n '/^\[IPv6AcceptRA\]/,/^\[/p' "$file" | grep -q "^UseDNS="; then - sed -i '/^\[IPv6AcceptRA\]/a UseDNS=no' "$file" - fi - done -} - -unlock_dns_to_dhcp() { - local file="" - - for file in /etc/systemd/network/*.network; do - [[ -f $file ]] || continue - sed -i '/^\[DHCPv4\]/{n;/^UseDNS=no$/d}' "$file" - sed -i '/^\[IPv6AcceptRA\]/{n;/^UseDNS=no$/d}' "$file" - done +usage() { + echo "Usage: omarchy-dns [Cloudflare|Google|DHCP|Custom]" >&2 } if (( $# == 0 )); then @@ -71,63 +231,71 @@ if (( $# == 0 )); then exit 0 fi -provider="$1" +if (( $# > 1 )); then + usage + exit 1 +fi + +if ! provider=$(provider_from_arg "$1"); then + usage + exit 1 +fi + +require_root "$provider" case "$provider" in Cloudflare) - require_root "$provider" + write_networkmanager_dns "1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001" + set_connection_dns "1.1.1.1 1.0.0.1" "2606:4700:4700::1111 2606:4700:4700::1001" tee /etc/systemd/resolved.conf >/dev/null <<'EOF' [Resolve] DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com FallbackDNS=9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net DNSOverTLS=opportunistic EOF - lock_dns_to_resolved ;; Google) - require_root "$provider" + write_networkmanager_dns "8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844" + set_connection_dns "8.8.8.8 8.8.4.4" "2001:4860:4860::8888 2001:4860:4860::8844" tee /etc/systemd/resolved.conf >/dev/null <<'EOF' [Resolve] DNS=8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.google FallbackDNS=9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net DNSOverTLS=opportunistic EOF - lock_dns_to_resolved ;; DHCP) - require_root "$provider" + clear_networkmanager_dns + clear_connection_dns tee /etc/systemd/resolved.conf >/dev/null <<'EOF' [Resolve] DNSOverTLS=no EOF - unlock_dns_to_dhcp ;; Custom) - require_root "$provider" echo "Enter your DNS servers (space-separated, e.g. '192.168.1.1 1.1.1.1'):" if ! read -r dns_servers; then dns_servers="" fi + dns_servers=$(normalize_servers "$dns_servers") if [[ -z $dns_servers ]]; then echo "Error: No DNS servers provided." >&2 exit 1 fi + split_dns_servers "$dns_servers" + write_networkmanager_dns "$dns_servers" + set_connection_dns "$ipv4_dns" "$ipv6_dns" tee /etc/systemd/resolved.conf >/dev/null <&2 - exit 1 ;; esac -systemctl restart systemd-networkd systemd-resolved +reload_dns_stack diff --git a/bin/omarchy-upgrade-to-4 b/bin/omarchy-upgrade-to-4 index a74c493e..8e439654 100755 --- a/bin/omarchy-upgrade-to-4 +++ b/bin/omarchy-upgrade-to-4 @@ -848,10 +848,23 @@ USER_UNITS cleanup_retired_services() { log "Disabling services retired by Omarchy 4" - # Do not stop iwd mid-upgrade; disabling is enough and avoids dropping a - # running Wi-Fi connection before the user can reboot into NetworkManager. + # Do not stop iwd or systemd-networkd mid-upgrade; disabling is enough and + # avoids dropping a running Wi-Fi connection before the user can reboot into + # NetworkManager. as_root systemctl disable iwd.service >/dev/null 2>&1 || true + local networkd_unit + for networkd_unit in \ + systemd-networkd.service \ + systemd-networkd.socket \ + systemd-networkd-varlink.socket \ + systemd-networkd-varlink-metrics.socket \ + systemd-networkd-resolve-hook.socket; do + as_root systemctl disable "$networkd_unit" >/dev/null 2>&1 || true + done + as_root systemctl disable systemd-networkd-wait-online.service >/dev/null 2>&1 || true + as_root systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true + cleanup_retired_user_unit_files if [[ -S $target_runtime_dir/bus ]]; then run_as_user_session systemctl --user disable \ @@ -1050,6 +1063,7 @@ run_post_upgrade_migrations() { USER="$target_user" \ LOGNAME="$target_user" \ OMARCHY_PATH=/usr/share/omarchy \ + OMARCHY_UPGRADE_TO_4_LIVE=1 \ PATH="$package_path" \ omarchy-migrate; then warn "Could not run Omarchy migrations; the user may be prompted to run them after login." diff --git a/install/hardware/network.sh b/install/hardware/network.sh index e8adbc43..b56a1ef5 100644 --- a/install/hardware/network.sh +++ b/install/hardware/network.sh @@ -1,6 +1,43 @@ # NetworkManager enablement is centralized in enable-services.sh. systemctl disable iwd.service 2>/dev/null || true -# Prevent systemd-networkd-wait-online timeout on boot -systemctl disable systemd-networkd-wait-online.service -systemctl mask systemd-networkd-wait-online.service +# Fresh Omarchy uses NetworkManager. Archinstall's legacy "copy ISO network" +# mode enabled systemd-networkd and dropped DHCP .network files that compete +# with NetworkManager, so retire that state whenever hardware setup runs. +for unit in \ + systemd-networkd.service \ + systemd-networkd.socket \ + systemd-networkd-varlink.socket \ + systemd-networkd-varlink-metrics.socket \ + systemd-networkd-resolve-hook.socket; do + systemctl disable "$unit" 2>/dev/null || true +done + +# Prevent systemd-networkd-wait-online timeout on boot. +systemctl disable systemd-networkd-wait-online.service 2>/dev/null || true +systemctl mask systemd-networkd-wait-online.service 2>/dev/null || true + +stock_networkd_file() { + local file="$1" + + [[ -f $file ]] || return 1 + case "$(basename "$file")" in + 20-ethernet.network|20-wlan.network|20-wwan.network) ;; + *) return 1 ;; + esac + + grep -Eq '^[[:space:]]*DHCP=yes[[:space:]]*$' "$file" || return 1 + grep -Eq '^[[:space:]]*Name=(en\*|eth\*|wl\*|ww\*)[[:space:]]*$' "$file" || return 1 +} + +backup_dir="/etc/systemd/network/omarchy-networkd-retired-$(date +%Y%m%d%H%M%S)" +for file in /etc/systemd/network/20-ethernet.network /etc/systemd/network/20-wlan.network /etc/systemd/network/20-wwan.network; do + if stock_networkd_file "$file"; then + install -d -m 0755 "$backup_dir" + mv "$file" "$backup_dir/" + fi +done + +if systemctl is-active --quiet NetworkManager.service 2>/dev/null; then + systemctl stop systemd-networkd.service 2>/dev/null || true +fi diff --git a/migrations/1782002156.sh b/migrations/1782002156.sh new file mode 100644 index 00000000..2fc1010d --- /dev/null +++ b/migrations/1782002156.sh @@ -0,0 +1,88 @@ +echo "Retire systemd-networkd in favor of NetworkManager" + +as_root() { + if (( EUID == 0 )); then + "$@" + else + sudo "$@" + fi +} + +stock_networkd_file() { + local file="$1" + + [[ -f $file ]] || return 1 + case "$(basename "$file")" in + 20-ethernet.network|20-wlan.network|20-wwan.network) ;; + *) return 1 ;; + esac + + grep -Eq '^[[:space:]]*DHCP=yes[[:space:]]*$' "$file" || return 1 + grep -Eq '^[[:space:]]*Name=(en\*|eth\*|wl\*|ww\*)[[:space:]]*$' "$file" || return 1 +} + +backup_stock_networkd_files() { + local backup_dir="/etc/systemd/network/omarchy-networkd-retired-$(date +%Y%m%d%H%M%S)" + local file + + for file in /etc/systemd/network/20-ethernet.network /etc/systemd/network/20-wlan.network /etc/systemd/network/20-wwan.network; do + if stock_networkd_file "$file"; then + as_root install -d -m 0755 "$backup_dir" + as_root mv "$file" "$backup_dir/" + fi + done +} + +networkd_units=( + systemd-networkd.service + systemd-networkd.socket + systemd-networkd-varlink.socket + systemd-networkd-varlink-metrics.socket + systemd-networkd-resolve-hook.socket +) + +if [[ ${OMARCHY_UPGRADE_TO_4_LIVE:-0} == "1" ]]; then + as_root systemctl enable NetworkManager.service >/dev/null 2>&1 || true + + if systemctl is-active --quiet NetworkManager.service 2>/dev/null; then + # NetworkManager is already carrying the live network; it is safe to stop + # networkd now so the rest of the upgrade no longer has competing DHCP. + for unit in "${networkd_units[@]}"; do + as_root systemctl disable --now "$unit" >/dev/null 2>&1 || true + done + as_root systemctl disable systemd-networkd-wait-online.service >/dev/null 2>&1 || true + as_root systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true + backup_stock_networkd_files + as_root systemctl stop systemd-networkd.service >/dev/null 2>&1 || true + as_root systemctl reload NetworkManager.service >/dev/null 2>&1 || true + as_root systemctl restart systemd-resolved.service >/dev/null 2>&1 || true + else + # Older live upgrades may still be relying on networkd/iwd. Disable for the + # next boot, but do not stop or reconfigure the running link. + for unit in "${networkd_units[@]}"; do + as_root systemctl disable "$unit" >/dev/null 2>&1 || true + done + as_root systemctl disable systemd-networkd-wait-online.service >/dev/null 2>&1 || true + as_root systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true + fi + + exit 0 +fi + +if ! command -v NetworkManager >/dev/null 2>&1; then + omarchy-pkg-add networkmanager +fi + +as_root systemctl enable --now NetworkManager.service >/dev/null 2>&1 || true + +for unit in "${networkd_units[@]}"; do + as_root systemctl disable --now "$unit" >/dev/null 2>&1 || true +done +as_root systemctl disable systemd-networkd-wait-online.service >/dev/null 2>&1 || true +as_root systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true + +backup_stock_networkd_files +as_root systemctl stop systemd-networkd.service >/dev/null 2>&1 || true + +as_root systemctl reload NetworkManager.service >/dev/null 2>&1 || true +as_root systemctl restart systemd-resolved.service >/dev/null 2>&1 || true diff --git a/test/shell.d/network-manager-transition-test.sh b/test/shell.d/network-manager-transition-test.sh new file mode 100644 index 00000000..c1ec496a --- /dev/null +++ b/test/shell.d/network-manager-transition-test.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +dns="$ROOT/bin/omarchy-dns" +hardware_network="$ROOT/install/hardware/network.sh" +migration="$ROOT/migrations/1782002156.sh" + +! grep -F 'systemd-networkd' "$dns" >/dev/null || fail "omarchy-dns no longer restarts systemd-networkd" +grep -F 'NetworkManager/conf.d/20-omarchy-dns.conf' "$dns" >/dev/null +grep -F '[global-dns-domain-*]' "$dns" >/dev/null +grep -F 'ipv4.ignore-auto-dns yes' "$dns" >/dev/null +grep -F 'ipv4.ignore-auto-dns no' "$dns" >/dev/null +grep -F 'nmcli device reapply' "$dns" >/dev/null +grep -F 'nmcli general reload conf' "$dns" >/dev/null +grep -F 'nmcli general reload dns-full' "$dns" >/dev/null +if grep -F 'nmcli general reload conf,dns-full' "$dns" >/dev/null; then + fail "omarchy-dns must not push DNS before reapplying active profiles" +fi +pass "omarchy-dns configures DNS through NetworkManager" + +grep -F 'systemd-networkd.service' "$hardware_network" >/dev/null +grep -F 'systemd-networkd.socket' "$hardware_network" >/dev/null +grep -F '20-wlan.network' "$hardware_network" >/dev/null +grep -F 'omarchy-networkd-retired' "$hardware_network" >/dev/null +pass "hardware setup retires archinstall networkd state" + +grep -F 'OMARCHY_UPGRADE_TO_4_LIVE' "$migration" >/dev/null +grep -F 'systemctl disable --now "$unit"' "$migration" >/dev/null +grep -F 'systemctl stop systemd-networkd.service' "$migration" >/dev/null +grep -F 'NetworkManager.service' "$migration" >/dev/null +grep -F '20-wlan.network' "$migration" >/dev/null +pass "migration repairs upgraded systems with networkd still active" diff --git a/test/shell.d/upgrade-to-4-test.sh b/test/shell.d/upgrade-to-4-test.sh index da3af25c..754773f0 100644 --- a/test/shell.d/upgrade-to-4-test.sh +++ b/test/shell.d/upgrade-to-4-test.sh @@ -24,3 +24,9 @@ grep -F 'skip-first-run-update-notification' "$upgrade_to_4" >/dev/null grep -F 'skip-first-run-update-notification' "$first_run_wifi" >/dev/null grep -F '(( skip_update_notification )) && return 0' "$first_run_wifi" >/dev/null pass "Omarchy 4 upgrade suppresses the fresh-install update toast" + +grep -F 'OMARCHY_UPGRADE_TO_4_LIVE=1' "$upgrade_to_4" >/dev/null +grep -F 'systemd-networkd.service' "$upgrade_to_4" >/dev/null +grep -F 'systemd-networkd.socket' "$upgrade_to_4" >/dev/null +grep -F 'systemd-networkd-resolve-hook.socket' "$upgrade_to_4" >/dev/null +pass "Omarchy 4 upgrade retires systemd-networkd for NetworkManager"