My take on a network manager

📆
🏷
,

There’s one thing that I am really missing under OpenBSD, a network manager which seemlessly handles running around with my laptop. So my main itches to scratch are doing magic things at boot and resume so I don’t have to bother with fiddling with hostname.if(5) ever.

My first take on tackling that problem actually was working ok’ish but depended on sqlite3 and after sqlite3 left base the solution started to annoy me everytime I moved to a current snapshot and sqlite3 stoppped working or was unavailable from within bsd.rd.

So I started to look around what other people did to get rid of those problems which led me to netctl. I like that netctl is nothing but a shell script. I dislike that it didn’t work at boot time.

As my first take already worked at boot time, reducing my hostname.if(5) to being only 2 lines:

up
!/etc/netmanager \$if

I decided to rewrite the script, reusing chunks of netstart(8) in order to keep the fileformat in a well-known format. /etc/netmanager basically searches below /etc/hostname.d for a file matching the given – or autodetected – network ID to connect to. So without further ado here it is:

#!/bin/sh -

# parse_hn_line() and ifstart() are taken from /etc/netstart revision 1.195 with
# some small additions (basically addition of $_nwid and the removal of unneeded routines)

set +o sh

usage() {
	cat <<EOF >&2
usage: /etc/netmanager [<nwid>] <if>
	<nwid>	network to connect to
	<if>	interface to connect

netmanager searches for <nwid>.nwid in /etc/hostname.d, parses the file and
feeds ifconfig(8) accordingly. <nwid>.nwid has the same format as hostname.if(5).

If no <nwid> has been given netmanager issues a scan for access points and
searches for a matching <nwid>.nwid file.
EOF
	exit 2
}

# Parse and "unpack" a hostname.if(5) line given as positional parameters.
# Fill the _cmds array with the resulting interface configuration commands.
parse_hn_line() {
        local _af=0 _name=1 _mask=2 _bc=3 _prefix=2 _c _cmd _prev _daddr
        set -A _c -- "$@"
        set -o noglob

        case ${_c[_af]} in
        ''|*([[:blank:]])'#'*)
                return
                ;;
        inet)   ((${#_c[*]} > 1)) || return
                [[ ${_c[_name]} == alias ]] && _mask=3 _bc=4
                [[ -n ${_c[_mask]} ]] && _c[_mask]="netmask ${_c[_mask]}"
                if [[ -n ${_c[_bc]} ]]; then
                        _c[_bc]="broadcast ${_c[_bc]}"
                        [[ ${_c[_bc]} == *NONE ]] && _c[_bc]=
                fi
                _cmds[${#_cmds[*]}]="ifconfig $_if ${_c[@]}"
                ;;
        inet6)  ((${#_c[*]} > 1)) || return
                if [[ ${_c[_name]} == autoconf ]]; then
                        _cmds[${#_cmds[*]}]="ifconfig $_if ${_c[@]}"
                        V6_AUTOCONF=true
                        return
                fi
                [[ ${_c[_name]} == alias ]] && _prefix=3
                [[ -n ${_c[_prefix]} ]] && _c[_prefix]="prefixlen ${_c[_prefix]}"
                _cmds[${#_cmds[*]}]="ifconfig $_if ${_c[@]}"
                ;;
        dest)   ((${#_c[*]} == 2)) && _daddr=${_c[1]} || return
                _prev=$((${#_cmds[*]} - 1))
                ((_prev >= 0)) || return
                set -A _c -- ${_cmds[_prev]}
                _name=3
                [[ ${_c[_name]} == alias ]] && _name=4
                _c[_name]="${_c[_name]} $_daddr"
                _cmds[$_prev]="${_c[@]}"
                ;;
        dhcp)   _c[0]=
                _cmds[${#_cmds[*]}]="ifconfig $_if ${_c[@]} down;dhclient $_if"
                V4_DHCPCONF=true
                ;;
        '!'*)   _cmd=$(print -- "${_c[@]}" | sed 's/\$if/'$_if'/g')
                _cmds[${#_cmds[*]}]="${_cmd#!}"
                ;;
        *)      _cmds[${#_cmds[*]}]="ifconfig $_if ${_c[@]}"
                ;;
        esac
        unset _c
        set +o noglob
}

# Start a single interface.
# Usage: ifstart if1
ifstart() {
        local _nwid=$1 _hn=/etc/hostname.d/$_nwid.nwid _cmds _i=0 _line _stat
        set -A _cmds

        if [[ ! -f $_hn ]]; then
                print -u2 "${0##*/}: $_hn: No such file or directory."
                return
        fi

        # Not using stat(1), we can't rely on having /usr yet.
        set -A _stat -- $(ls -nL $_hn)
        if [[ "${_stat[0]}${_stat[2]}${_stat[3]}" != *---00 ]]; then
                print -u2 "WARNING: $_hn is insecure, fixing permissions."
                chmod -LR o-rwx $_hn
                chown -LR root:wheel $_hn
        fi

        # Parse the hostname.if(5) file and fill _cmds array with interface
        # configuration commands.
        set -o noglob
        while IFS= read -- _line; do
                parse_hn_line $_line
        done <$_hn

        # Apply the interface configuration commands stored in _cmds array.
        while ((_i < ${#_cmds[*]})); do
                if $PRINT_ONLY; then
                        print -r -- "${_cmds[_i]}"
                else
                        eval "${_cmds[_i]}"
                fi
                ((_i++))
        done
        unset _cmds
        set +o noglob
}

autoselect() {
	local _if=$1
	ifconfig $_if down
	ifconfig $_if scan | awk '
		/ieee80211:/{ next }
		/nwid/{
			start=index($0,"nwid")+length("nwid ")
			end=index($0,"chan ")
			nwid=substr($0,start,end-start)
			if (nwid)
				print nwid
		}' | while read nwid; do
			{ ifconfig $_if | grep 'status: active' >/dev/null ;} && return
			[[ -r /etc/hostname.d/"$nwid".nwid ]] && \
				connect "$nwid" $_if
		done
}

connect() {
	local _nwid="$1" _if=$2;
	
	if [[ -r /etc/hostname.d/"$_nwid".nwid ]]; then
		printf "%s: connecting to %s\n" "$_if" "$_nwid"
		ifstart $_nwid $_if
	fi
}

PRINT_ONLY=false
if [ $# -eq 1 ]; then
	autoselect $1
elif [ $# -eq 2 ]; then
	connect $1 $2
else
	usage
fi

exit 0

As I am currently running the latest snapshot I haven’t tested if it’s working under bsd.rd, too, but I am actually very optimistic. So this is my take on a network manager that does what I am expecting of it, is base only and hopefully even works in the installer environment, making the whole experience basically frictionless. At least in my workflow.

Keep in mind this is just a quick hack but maybe somebody finds it useful.

--EOF