Homemade internet state monitor

Posted in hygiene bash systemd network i3wm tmpfs openvpn

I use i3wm. I use it because it has a very low resource footprint, it has great documentation, and it's simple to configure.

Complementing i3wm is the i3status status provider to the i3bar. It provides a good suite of preset components for things like temperatur measurement, disk capacity. It also provides a couple of generic ones, namely "output the contents of a file" or "yes if file exists, no if not."

It's not you, it's me?

Warning

Always use a VPN! Always!!

Now, I always use VPN no matter what, and I configure network manually. That is, I do not use a network manager. For this reason, when network applications start misbehaving, it's sometimes hard to tell whether it is because the host is gone, the local network is flaky, the internet route is flaky, or simply that the connection is gone for good.

Evidently, it would be practical to have an item in the status bar telling me whether I have access to internet right now or not.

The "yes if file exists, no if not" option seems to be the right one to go for. Basically, we need to stick an empty file in a runtime file store whenever we detect that internet is available.

Ping, rinse, repeat

There's not much trickery involved here. The check is a ping to a remote location every now and then.

Some caveats to cover to grant it some minimum of intelligence:

  • We need a few alternative destinations for the ping, that can take over if others happen to be down or take too long to repond.
  • To make the script useful for general purpose use, we'd also want to be able to supply a custom list of hosts to ping.
  • We also want to make sure that when the script is interrupted, it deletes the internet indicator file. That way, when it stops running, the internet indicator will be frozen in the "no" state.
  • Lastly, it can be useful to log to syslog every time the state of the internet connection (or the script itself) changes.

That can translate to a shell script like this;

#!/bin/bash

# this is our runtime file, if - which exists - tells us internet is up
f="/run/user/$UID/probe_up"
rm -f $f

up() {
        if [ ! -f $f ]; then
                logger -p info "iup up"
        fi
        touch $f
}
down() {
        if [ -f $f ]; then
                logger -p info "iup down"
        fi
        rm -f $f
}

argh() {
        logger -p info "iup die"
        rm -f $f
        exit 0
}

IUP_DELAY=${IUP_DELAY:-$1}
if [ -z $IUP_DELAY ]; then
        IUP_DELAY=5
fi
logger -p debug iup started with delay $IUP_DELAY

hosts=()
while read h; do
        hosts+=($h)
done < $HOME/.config/iup 2> /dev/null
if [ ${#hosts[@]} -eq "0" ]; then
        hosts=(8.8.8.8 151.101.193.67) # google nameserver and cnn.com
        logger -p warn missing hosts input file, using evil default: ${hosts[@]}
fi

trap argh SIGINT
trap argh SIGTERM
trap argh SIGQUIT

while true; do
        isup=""
        for h in ${hosts[@]}; do
                ping -c1 $h -w1 -q &> /dev/null
                if [ $? -eq '0' ]; then
                        up
                        isup=1
                        #logger -p debug "ping success $h"
                        break
                fi
        done
        if [ -z $isup ]; then
                down
        fi
        sleep $IUP_DELAY
done

Since we'll want this to run when network interfaces allegedly are up, it makes sense to link it to systemd and the network target:

[Unit]
Description=Internet connection poller
After=multi-user.target

[Service]
ExecStart=/usr/local/bin/iup.sh
Restart=always

[Install]
WantedBy=multi-user.target

Stick this in ~/.config/systemd/user/iup.service and enable and start the service:

$ systemctl --user enable iup.service
$ systemctl --user start iup.service

Checking what status our status is in

Now, in the i3status configuration, assuming that your system uid is 1000 [1], add:

order += "run_watch INET"

kj

run_watch INET {
        pidfile = "/run/user/1000/probe_up"
}

To reload the config on the fly, press mod+r (that's ctrl+r on mine). The result should be a new item in your bar showing INET: yes (most likely yes, anyway, since you're currently reading this).

Safety first, eh ... second

Warning

Did I mention that you should always use a VPN?

The same run_watch trick can be used for VPN.

I use openvpn, and it defines a flag --writepid. If you pass a file path to this parameter, either through the writepid config directiry or on the --writepid flag on the command line, it will write the openvpn pid to that file, and similarly remove it when the openvpn service ends.

However, this raises another issue. Namely the fact that a location is needed for openvpn to write the file to, for which it also has access.

If we want to use the /run directory again, now we also has to make sure that this directory exists.

Got the runs?

This is exactly what systemd-tmpfiles is for. You can add files to /etc/tmpfiles.d which describe what kind of temporary files or directories to add [2].

Create a file /etc/tmpfiles.d/openvpn.conf and add a single line to it:

d       /run/openvpn 0755    openvpn openvpn

This will ensure that the folder is created at startup.

Now, to create one right away for the current session, run systemd-tmpfiles --create.

Now we have everything we need to add the VPN status to i3status aswell. Provided that we chose /run/openvpn/openvpn.pid as our argument to openvpn --writepid, the setup is more or less the same as before:

order += "run_watch VPN"

kj

run_watch VPN {
        pidfile = "/run/openvpn/openvpn.pid"
}
[1]The /run/user/$UID folder will be created on system startup, and will be writable by that user. It provides a tmpfs storage location for processes that run in early stages of boot, before the entire filesystem tree (which may include /var/run because /var may be on a separate partition) has been mounted. More in that here: https://lwn.net/Articles/436012/
[2]This service has an impressive amount of flexibility, which you can inspect yourself by visiting the tmpfiles.d (and systemd-tmpfiles) manpage.