Skip to content

Instantly share code, notes, and snippets.

@mmguero
Created September 3, 2026 03:02
Show Gist options
  • Select an option

  • Save mmguero/20d27350e3207ffb15d64d5eb5aefb8f to your computer and use it in GitHub Desktop.

Select an option

Save mmguero/20d27350e3207ffb15d64d5eb5aefb8f to your computer and use it in GitHub Desktop.
setup ethernet to wifi failover using networkmanger on a fresh debian installation

Debian NetworkManager wired-first Wi-Fi fallback

This runbook migrates a remotely administered Debian host from ifupdown with dhcpcd and wpa_supplicant to NetworkManager. It then configures this policy:

  • Use Ethernet on enp1s0 whenever it has carrier and a global IP address.
  • Disconnect Wi-Fi on wlo1 while Ethernet is usable.
  • Connect the saved Wi-Fi profile when Ethernet loses carrier or its address.

The migration is staged so the existing SSH connection remains available until a timed, rollback-protected handoff.

Names used in this example

Purpose Name
Ethernet interface enp1s0
Wi-Fi interface wlo1
NetworkManager Ethernet profile wired-enp1s0
NetworkManager Wi-Fi profile wifi-fallback
Hostname bee

Change these names throughout if the target host differs.

1. Inspect the existing network configuration

Run these commands before changing anything:

printf '%s\n' '=== NetworkManager package ==='
dpkg-query -W -f='${Status} ${Version}\n' network-manager 2>/dev/null || true
command -v nmcli
command -v nmtui

printf '%s\n' '=== Network services ==='
systemctl is-active NetworkManager.service 2>/dev/null || true
systemctl list-units --type=service --all --no-pager |
  grep -E 'NetworkManager|dhcpcd|wpa_supplicant|networking|systemd-networkd'

printf '%s\n' '=== Addresses and routes ==='
ip -br link
ip -br -4 address
ip route

printf '%s\n' '=== Current SSH path ==='
client_ip="${SSH_CONNECTION%% *}"
ip route get "$client_ip"

printf '%s\n' '=== Ethernet state ==='
cat /sys/class/net/enp1s0/carrier 2>/dev/null || true
ip -4 address show dev enp1s0

printf '%s\n' '=== ifupdown configuration ==='
sudo sed -n '1,200p' /etc/network/interfaces
sudo find /etc/network/interfaces.d -maxdepth 1 -type f \
  -exec sh -c 'echo "### $1"; sed -n "1,200p" "$1"' sh {} \; 2>/dev/null

To identify which systemd unit owns a known networking process, pass its PID to systemctl status. For example:

sudo systemctl status 831 813 --no-pager

On the original host, this established that:

  • NetworkManager was absent.
  • networking.service and ifup@wlo1.service managed networking.
  • ifup@wlo1.service launched a per-interface wpa_supplicant and dhcpcd.
  • SSH was using wlo1, so stopping the old stack interactively would terminate the only remote connection.
  • /etc/network/interfaces contained the Wi-Fi SSID and PSK.

Do not publish the contents of /etc/network/interfaces while it contains a plaintext Wi-Fi password.

2. Back up the existing configuration

sudo cp -a /etc/network/interfaces \
  /etc/network/interfaces.before-networkmanager

Keep this backup until Wi-Fi fallback and Ethernet preference have both been tested.

3. Prevent NetworkManager from touching the live interfaces

Create a temporary safety configuration before installing NetworkManager:

sudo mkdir -p /etc/NetworkManager/conf.d
sudoedit /etc/NetworkManager/conf.d/00-migration-unmanaged.conf

Contents:

[keyfile]
unmanaged-devices=interface-name:wlo1;interface-name:enp1s0

This allows NetworkManager to start without taking either interface from the working ifupdown session.

4. Install NetworkManager

sudo apt update
sudo apt install network-manager

Verify the daemon and confirm that both interfaces remain unmanaged:

systemctl is-active NetworkManager
nmcli general status
nmcli device status
ip -br -4 address

Expected state during staging:

enp1s0  ethernet  unmanaged
wlo1    wifi      unmanaged

The original Wi-Fi address and SSH session should still be present.

5. Stage the Ethernet profile

sudo nmcli connection add \
  type ethernet \
  ifname enp1s0 \
  con-name wired-enp1s0 \
  connection.autoconnect yes \
  connection.autoconnect-priority 100 \
  ipv4.method auto \
  ipv4.route-metric 100 \
  ipv6.method auto \
  ipv6.route-metric 100

The route metric makes Ethernet the preferred default route whenever it is active.

6. Stage the Wi-Fi profile

The following commands read the existing SSID and PSK without embedding the password in shell history:

wifi_ssid="$(
  sudo sed -n \
    's/^[[:space:]]*wpa-ssid[[:space:]]\+//p' \
    /etc/network/interfaces |
  head -n1
)"

wifi_psk="$(
  sudo sed -n \
    's/^[[:space:]]*wpa-psk[[:space:]]\+//p' \
    /etc/network/interfaces |
  head -n1
)"

Create the connection:

sudo nmcli connection add \
  type wifi \
  ifname wlo1 \
  con-name wifi-fallback \
  ssid "$wifi_ssid"

sudo nmcli connection modify wifi-fallback \
  wifi-sec.key-mgmt wpa-psk \
  wifi-sec.psk "$wifi_psk" \
  connection.autoconnect yes \
  connection.autoconnect-priority 10 \
  ipv4.method auto \
  ipv4.route-metric 600 \
  ipv6.method auto \
  ipv6.route-metric 600

unset wifi_psk

Check the stored SSID and password length without printing the secret:

nmcli -g 802-11-wireless.ssid connection show wifi-fallback

sudo nmcli --show-secrets \
  -g 802-11-wireless-security.psk \
  connection show wifi-fallback |
  awk '{ print "PSK length:", length }'

A WPA passphrase should contain 8 through 63 characters.

Inspect both staged profiles:

nmcli -f NAME,UUID,TYPE,DEVICE,AUTOCONNECT,AUTOCONNECT-PRIORITY \
  connection show

Neither staged profile will have a device assigned while the temporary unmanaged rule is active.

7. Prepare the NetworkManager version of interfaces

Create the replacement file without altering the live file yet:

sudoedit /etc/network/interfaces.networkmanager

Contents:

source /etc/network/interfaces.d/*

auto lo
iface lo inet loopback

The physical-interface configuration is removed because NetworkManager will own both interfaces.

8. Create the handoff script

sudoedit /usr/local/sbin/networkmanager-migration-switch

Contents:

#!/bin/sh

logger -t nm-migration "Starting NetworkManager handoff"

ifdown --force wlo1 || true

install -o root -g root -m 0644 \
    /etc/network/interfaces.networkmanager \
    /etc/network/interfaces

if [ -f /etc/NetworkManager/conf.d/00-migration-unmanaged.conf ]; then
    mv /etc/NetworkManager/conf.d/00-migration-unmanaged.conf \
       /etc/NetworkManager/conf.d/00-migration-unmanaged.conf.disabled
fi

systemctl restart NetworkManager.service
nmcli radio wifi on
nmcli --wait 60 connection up wifi-fallback

9. Create the rollback script

sudoedit /usr/local/sbin/networkmanager-migration-rollback

Contents:

#!/bin/sh

logger -t nm-migration "Rolling back to ifupdown"

nmcli device disconnect wlo1 2>/dev/null || true
systemctl stop NetworkManager.service || true

if [ -f /etc/NetworkManager/conf.d/00-migration-unmanaged.conf.disabled ]; then
    mv /etc/NetworkManager/conf.d/00-migration-unmanaged.conf.disabled \
       /etc/NetworkManager/conf.d/00-migration-unmanaged.conf
fi

cp -af /etc/network/interfaces.before-networkmanager \
       /etc/network/interfaces

ifdown --force wlo1 2>/dev/null || true
ifup --force wlo1

Secure both scripts:

sudo chown root:root \
  /usr/local/sbin/networkmanager-migration-switch \
  /usr/local/sbin/networkmanager-migration-rollback

sudo chmod 0700 \
  /usr/local/sbin/networkmanager-migration-switch \
  /usr/local/sbin/networkmanager-migration-rollback

10. Perform the rollback-protected handoff

Arm a five-minute rollback:

sudo systemd-run \
  --unit=nm-migration-rollback \
  --on-active=5m \
  /usr/local/sbin/networkmanager-migration-rollback

Confirm that it is waiting:

systemctl status nm-migration-rollback.timer --no-pager

Schedule the handoff in five seconds:

sudo systemd-run \
  --unit=nm-migration-switch \
  --on-active=5s \
  /usr/local/sbin/networkmanager-migration-switch

The SSH connection will close while wlo1 changes managers. Wait about 15 seconds and reconnect. The DHCP address may change. Check the router for the host named bee if the former address does not respond.

After reconnecting, cancel the rollback immediately:

sudo systemctl stop nm-migration-rollback.timer

Validate the migration:

nmcli device status
nmcli connection show --active
ip -br -4 address
ip route

At this stage, wlo1 should be connected using wifi-fallback, and enp1s0 should be unavailable when its cable is unplugged.

11. Install the wired-first dispatcher policy

Create the dispatcher script:

sudoedit /etc/NetworkManager/dispatcher.d/70-wifi-fallback

Contents:

#!/bin/sh

WIRED="enp1s0"
WIFI="wlo1"
WIFI_CONNECTION="wifi-fallback"

wired_has_ip() {
    [ "$(cat "/sys/class/net/$WIRED/carrier" 2>/dev/null)" = "1" ] &&
        ip -o address show dev "$WIRED" scope global |
        grep -qv ' tentative '
}

wifi_active() {
    LC_ALL=C nmcli -t -f DEVICE,STATE device status |
        grep -Eq "^${WIFI}:(connected|connecting)"
}

if wired_has_ip; then
    if wifi_active; then
        logger -t wifi-fallback \
            "$WIRED has an address; disconnecting $WIFI"
        nmcli --wait 15 device disconnect "$WIFI"
    fi
else
    if ! wifi_active; then
        logger -t wifi-fallback \
            "$WIRED unavailable; connecting $WIFI"
        nmcli --wait 30 connection up "$WIFI_CONNECTION" ifname "$WIFI"
    fi
fi

exit 0

The wifi_active expression deliberately accepts states beginning with connecting, including values such as connecting (prepare).

Set the permissions required for a NetworkManager dispatcher:

sudo chown root:root /etc/NetworkManager/dispatcher.d/70-wifi-fallback
sudo chmod 0755 /etc/NetworkManager/dispatcher.d/70-wifi-fallback

stat -c '%U:%G %a %n' \
  /etc/NetworkManager/dispatcher.d/70-wifi-fallback

Expected result:

root:root 755 /etc/NetworkManager/dispatcher.d/70-wifi-fallback

No NetworkManager restart is required. Dispatcher scripts are read when network events occur.

12. Test the Wi-Fi branch

Keep Ethernet unplugged and invoke the policy manually:

sudo /etc/NetworkManager/dispatcher.d/70-wifi-fallback enp1s0 down
nmcli device status

wlo1 should remain connected through wifi-fallback.

13. Test the Ethernet branch with a safety timer

Before connecting Ethernet, create a five-minute test rollback. It disables the dispatcher and brings Wi-Fi back if remote access is lost:

sudo systemd-run \
  --unit=wifi-policy-test-rollback \
  --on-active=5m \
  /bin/sh -c 'chmod 0644 /etc/NetworkManager/dispatcher.d/70-wifi-fallback; nmcli connection up wifi-fallback'

Connect the Ethernet cable. Once enp1s0 receives an address, the dispatcher should disconnect wlo1, ending the Wi-Fi SSH session.

Find the Ethernet address in the router and reconnect. Then cancel the timer:

sudo systemctl stop wifi-policy-test-rollback.timer

If the safety timer fired, restore the dispatcher permission after resolving the problem:

sudo chmod 0755 /etc/NetworkManager/dispatcher.d/70-wifi-fallback

14. Validate Ethernet operation

nmcli device status
nmcli connection show --active
ip -br -4 address
ip route

Expected state:

  • enp1s0 is connected through wired-enp1s0.
  • wlo1 is disconnected.
  • The default route uses enp1s0 with metric 100.
  • Wi-Fi radio remains enabled so fallback can reconnect it.

Confirm that the current SSH session is using Ethernet:

client_ip="${SSH_CONNECTION%% *}"
ip route get "$client_ip"

The result should include dev enp1s0.

Check local and internet connectivity:

ping -c 3 172.16.0.1
ping -c 3 deb.debian.org
nmcli radio wifi

Inspect policy events:

sudo journalctl -b -t wifi-fallback --no-pager

A successful Ethernet transition produces a message resembling:

enp1s0 has an address; disconnecting wlo1

15. Validate automatic Wi-Fi fallback

Unplug Ethernet. The wired SSH session will end. Within several seconds, NetworkManager should activate wifi-fallback. Reconnect using the Wi-Fi address shown by the router.

Validate the state:

nmcli device status
ip -br -4 address
ip route
sudo journalctl -b -t wifi-fallback --since "5 minutes ago"

The journal should contain:

enp1s0 unavailable; connecting wlo1

Reconnect Ethernet and confirm the reverse transition. The resulting sequence should resemble:

enp1s0 unavailable; connecting wlo1
enp1s0 has an address; disconnecting wlo1

16. Final checks

Cancel any remaining test timer and confirm the dispatcher is executable:

sudo systemctl stop wifi-policy-test-rollback.timer 2>/dev/null || true

stat -c '%U:%G %a %n' \
  /etc/NetworkManager/dispatcher.d/70-wifi-fallback

Useful ongoing checks:

nmcli device status
nmcli connection show --active
ip -br -4 address
ip route
sudo journalctl -b -t wifi-fallback --no-pager

17. Optional cleanup after several successful reboots

The disabled safety configuration, migration scripts, and staged interfaces file are inert. Keep them until the machine has rebooted successfully and both network paths have been tested again.

Afterward, they can be removed:

sudo rm -f \
  /etc/NetworkManager/conf.d/00-migration-unmanaged.conf.disabled \
  /etc/network/interfaces.networkmanager \
  /usr/local/sbin/networkmanager-migration-switch \
  /usr/local/sbin/networkmanager-migration-rollback

Retaining /etc/network/interfaces.before-networkmanager provides a compact record of the former setup. Protect it because it may contain the old Wi-Fi PSK:

sudo chown root:root /etc/network/interfaces.before-networkmanager
sudo chmod 0600 /etc/network/interfaces.before-networkmanager

18. Stable addresses for remote recovery

Create DHCP reservations in the router for both interface MAC addresses. Each interface needs its own address because Ethernet and Wi-Fi have separate MAC addresses.

On the tested host, the final addresses were:

Interface Role Address during testing
enp1s0 Primary Ethernet 172.16.0.32
wlo1 Wi-Fi fallback 172.16.0.166

Reservations make it straightforward to reconnect after either interface takes over.

Troubleshooting

NetworkManager reports an interface as unmanaged

Check for both causes used during migration:

cat /etc/NetworkManager/conf.d/00-migration-unmanaged.conf 2>/dev/null
sudo sed -n '1,200p' /etc/network/interfaces

Physical interfaces should no longer appear in /etc/network/interfaces, and the temporary unmanaged file should have been renamed with a .disabled suffix.

Then restart NetworkManager:

sudo systemctl restart NetworkManager
nmcli device status

Wi-Fi profile will not activate

nmcli -g 802-11-wireless.ssid connection show wifi-fallback

sudo nmcli --show-secrets \
  -g 802-11-wireless-security.psk \
  connection show wifi-fallback |
  awk '{ print "PSK length:", length }'

sudo journalctl -b -u NetworkManager --no-pager

The SSID must match exactly. The passphrase length must be valid.

Ethernet is shown as unavailable

This normally means it lacks carrier:

cat /sys/class/net/enp1s0/carrier 2>/dev/null
ip link show enp1s0

With a working cable and switch port, carrier should be 1.

Wi-Fi remains connected after Ethernet receives an address

ip -o address show dev enp1s0 scope global
stat -c '%U:%G %a %n' \
  /etc/NetworkManager/dispatcher.d/70-wifi-fallback
sudo /etc/NetworkManager/dispatcher.d/70-wifi-fallback enp1s0 up
sudo journalctl -b -t wifi-fallback --no-pager

The dispatcher must be owned by root and executable. Ethernet must have carrier plus a global IPv4 or IPv6 address.

Recover the old setup manually

If NetworkManager cannot provide connectivity and the automatic rollback is unavailable:

sudo systemctl stop NetworkManager.service

sudo mv \
  /etc/NetworkManager/conf.d/00-migration-unmanaged.conf.disabled \
  /etc/NetworkManager/conf.d/00-migration-unmanaged.conf 2>/dev/null || true

sudo cp -af \
  /etc/network/interfaces.before-networkmanager \
  /etc/network/interfaces

sudo ifdown --force wlo1 2>/dev/null || true
sudo ifup --force wlo1

The backup may contain the Wi-Fi credential, so keep its permissions at 0600.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment