ethwifi: throw away networking tools and keep simple things simple
TLDR
Not mentioning systemd, which I'm going to get rid of, I suddenly realized that all those network managers, netplan.io, and even old good ifupdown with ifplugd are simply obstacles to implement same old thing: automatic switching from wifi to ethernet when network cable is plugged in and vice versa. I purged all those packages. A simple python script looks much clearer and does the job without any quirks.
Of course, I have to run this script as a systemd service for now, but that's not for long. In the world of SBCs the choice of distros is limited, but I keep looking towards Buildroot and Yocto. Not sure when I get there, though. I prefer to paint, you know.

Not so long read
Although my daughter called me old believer when I rolled down windows in her car instead of turning on AC, I don't feel myself too old and have no desire to fap on all that oldschool crap. I dislike ifupdown and sysvinit no less than modish systemd.
Recently I dug out my quite old Armbian-based mediacenter and decided to completely upgrade it and setup automatic switching between ethernet and wifi. Given that systemd is not an option at all, I tried to make things working with old tools.
Actually, I don't remember how I did such an automatic switching before. It was more than a decade ago. My habits drifted towards manual network management using command line even on laptops, even for wifi. I use Debian so my choice was old (but I can't say good) ifupdown.
I started from scratch. Initially I gave ifplugd a try but I found its configuration confusing. Not to mention it did not work for me out of the box. Although it detected link status changes, its behavior was weird and soon I realized I'm wasting my time.
Well, ifupdown and ifplugd aren't good helpers but what's a good replacement then? I bet you have already guessed. Yes, this world is rolling to hell, it's worth to recall ISC which has dropped the development of their DHCP client and I guess in favor of what. But they could not foresee that the author would find a better place so there's a little hope all that crap will be eventually thrown away and the unix way gets restored. But as of now, I haven't found any good replacement.
And my second thought was "DBUS might provide some events". I was wrong. In my research I found this page https://unix.stackexchange.com/questions/503309/udev-network-cable-hotplug-event-not-catched but suggested evaluation script really did not catch anything when I plugged and unplugged ethernet cable. I can be wrong saying there are no such events on DBUS, but that's true. Yes, I'm aware that systemd-networkd can generate them, but... re-read the beginning, please.
Okay, I dug deeper into netlink. I had no experience with it before, so I started from search. Although the best hit was https://olegkutkov.me/2018/02/14/monitoring-linux-networking-state-using-netlink/, I prefer python so I searched pypi as well and even found a few packages. However I did not want any external dependencies. Also, those packages look incomplete so I had to extract all necessary constants from linux headers.
Finally, I found this code and used it as a boilerplate: https://gist.github.com/Lukasa/6209575d588f1584c374 Soon my implementation started detecting link state changes very well, but what's next? The code looked too complicated and I still needed to get initial link state, avoid race conditions, and switch interfaces somehow.
Instead, polling /sys/class/eth0/carrier and invoking bare ip
command looked much simple and reliable approach. If I used ifupdown, I could not poll carrier file because ifdown
deactivates network interface completely.
So, here's the script:
#!/usr/bin/env python3
'''
Manage networking for a host with ethernet and wifi interfaces.
'''
eth_ifname = 'eth0'
wifi_ifname = 'wlan0'
wpa_ssid = 'MY-WIFI'
wpa_passphrase = 'my-very-strong-password'
ipv4_address = '192.168.0.3/24'
ipv4_gateway = '192.168.0.1'
eth_carrier_poll_delay = 1
# when link gets down, that might be a temporary failure so try a few times:
linkdown_checks = 3
# delay after unanticipated exception
restart_delay = 3
def main():
init_interfaces()
start_wpa_supplicant()
prev_carrier = None
while True:
try:
eth_carrier = get_carrier_state(eth_ifname)
if eth_carrier != prev_carrier:
if eth_carrier == '1':
print('Ethernet link up, deinit wifi')
deinit_wifi()
init_eth()
else:
print('Ethernet link down, init wifi')
deinit_eth()
init_wifi()
prev_carrier = eth_carrier
time.sleep(eth_carrier_poll_delay)
except Exception:
traceback.print_exc()
time.sleep(restart_delay)
def init_interfaces():
# initialize interfaces so we can check link carrier or scan wifi
invoke(f'ip link set {eth_ifname} up')
invoke(f'ip link set {wifi_ifname} up')
# delete routes and assigned addresses
deinit_wifi()
deinit_eth()
def start_wpa_supplicant():
# start wpa supplicant for wifi interface
invoke('killall wpa_supplicant', check=False)
wpa_conf = invoke(f'wpa_passphrase {wpa_ssid} {wpa_passphrase}').stdout
wpa_conf_filename = f'/etc/wpa_supplicant/{wpa_ssid}.conf'
with open(wpa_conf_filename, 'w') as f:
f.write(wpa_conf)
invoke(f'wpa_supplicant -B -i {wifi_ifname} -c {wpa_conf_filename}')
def init_wifi():
# setup ip configuration for wifi
invoke(f'ip address add {ipv4_address} broadcast + dev {wifi_ifname}')
invoke(f'ip route add default via {ipv4_gateway} dev {wifi_ifname}')
def deinit_wifi():
# delete ip configuration for wifi
invoke(f'ip route del default via {ipv4_gateway} dev {wifi_ifname}', check=False)
invoke(f'ip address del {ipv4_address} broadcast + dev {wifi_ifname}', check=False)
def init_eth():
# setup ip configuration for ethernet
invoke(f'ip address add {ipv4_address} broadcast + dev {eth_ifname}')
invoke(f'ip route add default via {ipv4_gateway} dev {eth_ifname}')
def deinit_eth():
# delete ip configuration for ethernet
invoke(f'ip route del default via {ipv4_gateway} dev {eth_ifname}', check=False)
invoke(f'ip address del {ipv4_address} broadcast + dev {eth_ifname}', check=False)
def get_carrier_state(ifname):
for i in range(linkdown_checks):
if read_carrier(ifname) == '1':
# link is up, return immediately
return '1'
time.sleep(eth_carrier_poll_delay)
return read_carrier(ifname)
def read_carrier(ifname):
try:
with open(f'/sys/class/net/{ifname}/carrier') as f:
return f.read(1)
except OSError:
return '0'
def invoke(command, check=True, shell=False, **kwargs):
if not shell:
command = shlex.split(command)
result = subprocess.run(command, capture_output=True, text=True, shell=shell, **kwargs)
if check and result.returncode != 0:
raise Exception(f'Failed {command}: {result.stderr or result.stdout}')
return result
import shlex
import subprocess
import time
import traceback
if __name__ == '__main__':
main()
Someday I'll rewrite that in C, and maybe end up with systemd-2, but I doubt this will ever happen so the world is in safety for now.
The script does not update resolv.conf. I have no plans to use DHCP so I purged resolvconf package too.
A unit for systemd, based on networking.service:
[Unit] Description=Networking Wants=network.target After=local-fs.target network-pre.target apparmor.service systemd-sysctl.service systemd-modules-load.service Before=network.target shutdown.target network-online.target Conflicts=shutdown.target [Install] WantedBy=multi-user.target WantedBy=network-online.target [Service] Type=simple ExecStart=/root/ethwifi
Here is a summary of what was purged:
apt purge network-manager* networkd-dispatcher* avahi* netplan* ifupdown resolvconf
And, one more thing:
systemctl disable wpa_supplicant
That's all. Bye for now.