1## @defgroup network Netzwerk 
    2## @brief Umgang mit uci-Netzwerk-Interfaces und Firewall-Zonen 
    3# Beginn der Doku-Gruppe 
    9# shellcheck disable=SC2034 
   11# diese Domain wird testweise abgefragt, um die Verfügbarkeit des on-DNS zu prüfen 
   12DNS_SERVICE_REFERENCE=
"opennet-initiative.de" 
   13# ein Timeout von einer Sekunde scheint zu kurz zu sein (langsame Geräte brauchen mindestens 0,5s - abhängig vom Load) 
   17# Liefere alle IPs fuer diesen Namen zurueck 
   19    nslookup 
"$1" 2>/dev/
null | sed 
'1,3d' | grep 
"^Address:" | awk 
'{print $2}' | sort -n
 
   24    nslookup 
"$1" 2>/dev/
null | tail -n 1 | awk 
'{ printf "%s", $4 }' 
   28## @fn has_opennet_dns() 
   29## @brief Prüfe, ob *.on-Domains aufgelöst werden. 
   30## @returns Der Exitcode ist Null, falls on-DNS verfügbar ist. 
   31## @details Die maximale Laufzeit dieser Funktion ist auf eine Sekunde begrenzt. 
   33    trap 
'error_trap has_opennet_dns "'"$*"'"' EXIT
 
   34    # timeout ist kein shell-builtin - es benoetigt also ein global ausfuehrbares Kommando 
   35    [ -n 
"$(timeout "$DNS_TIMEOUT
" on-function query_dns "$DNS_SERVICE_REFERENCE
")" ] && 
return 0
 
   36    trap 
"" EXIT && 
return 1
 
   41## @brief Ermittle die Latenz eines Ping-Pakets auf dem Weg zu einem Ziel. 
   42## @param target IP oder DNS-Name des Zielhosts 
   43## @param duration die Dauer der Ping-Kommunikation in Sekunden (falls ungesetzt: 5) 
   44## @returns Ausgabe der mittleren Ping-Zeit in ganzen Sekunden; bei Nichterreichbarkit ist die Ausgabe leer 
   46    trap 
'error_trap get_ping_time "'"$*"'"' EXIT
 
   48    local duration=
"${2:-5}" 
   51    [ -z 
"$ip" ] && 
return 0
 
   52    ping -w 
"$duration" -q 
"$ip" 2>/dev/
null \
 
   53        | grep 
"min/avg/max" \
 
   56        | awk 
'{ print int($1 + 0.5); }' 
   60# Lege eine Weiterleitungsregel fuer die firewall an (firewall.@forwarding[?]=...) 
   61# WICHTIG: anschliessend muss "uci commit firewall" ausgefuehrt werden
 
   62# Parameter: Quell-Zone und Ziel-Zone 
   64    trap 
'error_trap add_zone_forward "'"$*"'"' EXIT
 
   71# Das Masquerading in die Opennet-Zone soll nur fuer bestimmte Quell-Netze erfolgen. 
   72# Diese Funktion wird bei hotplug-Netzwerkaenderungen ausgefuehrt. 
   73update_opennet_zone_masquerading() {
 
   74    trap 
'error_trap update_opennet_zone_masquerading "'"$*"'"' EXIT
 
   76    local network_with_prefix
 
   78    uci_prefix=
$(find_first_uci_section firewall zone 
"name=$ZONE_MESH")
 
   79    # Abbruch, falls die Zone fehlt 
   80    [ -z 
"$uci_prefix" ] && 
msg_info "failed to find opennet mesh zone ($ZONE_MESH)" && 
return 0
 
   81    # alle masquerade-Netzwerke entfernen 
   83    # aktuelle Netzwerke wieder hinzufuegen 
   84    for network in 
$(get_zone_log_interfaces 
"$ZONE_LOCAL"; get_zone_log_interfaces 
"$ZONE_WAN"); 
do 
   86            echo 
"$network_with_prefix" 
   89    if [ -n 
"$(uci_get "${uci_prefix}.masq_src
")" ]; then
 
   90        # masquerading aktiveren (nur fuer die obigen Quell-Adressen) 
   91        uci set 
"${uci_prefix}.masq=1" 
   93        # Es gibt keine lokalen Interfaces - also duerfen wir kein Masquerading aktivieren. 
   94        # Leider interpretiert openwrt ein leeres "masq_src" nicht als "masq fuer niemanden" :(
 
   95        uci set 
"${uci_prefix}.masq=0" 
   97    # Seit April 2017 (commit e751cde8) verwirft fw3 "INVALID"-Pakete (also beispielsweise
 
   98    # asymmetrische Antworten), sofern Masquerading aktiv ist. Dies schalten wir ab. 
   99    uci set 
"${uci_prefix}.masq_allow_invalid=1" 
  100    apply_changes firewall
 
  104## @fn get_current_addresses_of_network() 
  105## @brief Liefere die IP-Adressen eines logischen Interface inkl. Praefix-Laenge (z.B. 172.16.0.1/24). 
  106## @param network logisches Netzwerk-Interface 
  107## @details Es werden sowohl IPv4- als auch IPv6-Adressen zurückgeliefert. 
  109    trap 
'error_trap get_current_addresses_of_network "'"$*"'"' EXIT
 
  114    } | xargs -r -n 1 echo
 
  118# Liefere die logischen Netzwerk-Schnittstellen einer Zone zurueck. 
  119get_zone_log_interfaces() {
 
  120    trap 
'error_trap get_zone_log_interfaces "'"$*"'"' EXIT
 
  124    uci_prefix=
$(find_first_uci_section firewall zone 
"name=$zone")
 
  125    # keine Zone -> keine Interfaces 
  126    [ -z 
"$uci_prefix" ] && 
return 0
 
  128    # falls 'network' und 'device' leer sind, dann enthaelt 'name' den Interface-Namen 
  130    [ -z 
"$interfaces" ] && [ -z 
"$(uci_get "${uci_prefix}.device
")" ] && interfaces=
"$(uci_get "${uci_prefix}.name
")" 
  131    for interface in $interfaces; 
do 
  132        if [ 
"$interface" != 
"$(get_device_of_interface "$interface
")" ]; then
 
  133            printf 
'%s\n' "$interface" 
  139## @fn get_zone_raw_devices() 
  140## @brief Ermittle die physischen Netzwerkinterfaces, die direkt einer Firewall-Zone zugeordnet sind. 
  141## @details Hier werden _nicht_ die logischen Interfaces in die physischen aufgeloest, sondern 
  142##   es werden nur die physischen Interfaces zurückgegeben. 
  144    trap 
'error_trap get_zone_raw_devices "'"$*"'"' EXIT
 
  147    uci_prefix=
$(find_first_uci_section 
"firewall" "zone" "name=$zone")
 
  148    [ -z 
"$uci_prefix" ] && 
msg_debug "Failed to retrieve raw devices of non-existing zone '$zone'" && 
return 0
 
  149    # extrahiere die phys. Interfaces 
  152            printf '%s\n' "$interface
" 
  158# Ist das gegebene physische Netzwerk-Interface Teil einer Firewall-Zone? 
  160    trap 'error_trap is_device_in_zone "'"$*"'"' EXIT 
  165    for log_interface in $(get_zone_log_interfaces "$2
"); do 
  166        for item in $(get_subdevices_of_interface "$log_interface
"); do 
  167            [ "$device
" = "$item
" ] && return 0 
  171    trap "" EXIT && return 1 
  175# Ist das gegebene logische Netzwerk-Interface Teil einer Firewall-Zone? 
  176is_interface_in_zone() { 
  180    for item in $(get_zone_log_interfaces "$zone
"); do 
  181        [ "$item
" = "$interface
" ] && return 0 
  184    trap "" EXIT && return 1 
  188## @fn get_device_of_interface() 
  189## @brief Ermittle das physische Netzwerk-Gerät, das einem logischen Netzwerk entspricht. 
  190## @details Ein Bridge-Interface wird als Gerät betrachtet und zurückgeliefert (nicht seine Einzelteile). 
  191##   Entspricht der Parameter einem physischen Interface, dann wird es mit gleichem Namen unveraendert wieder zurueckgegeben. 
  192get_device_of_interface() { 
  196    # OpenWrt nutzt jetzt DSA anstatt swconfig. Daher sind fuer eine Uebergangszeit hier zwei Pruefungen notwendig. 
  198    # Bereite DSA Check vor. Ermittle aktuelle Anzahl an Devices laut network.@device[]. 
  199    # (Alternative zur Schleife unten ist die Nutzung von config_foreach(), siehe https://openwrt.org/docs/guide-developer/config-scripting.) 
  201    local max_dev_index=-1  # finde groessten Index 
  202    local MAX_BRIDGES=10  # oberes Limit gegen Endlosschleife 
  203    while [ "$i
" -lt "$MAX_BRIDGES
" ]; do 
  204        local dev_exists="-1
" 
  205        dev_exists="$(uci_get 
"network.@device[$i].type" -1)
" 
  206        if [ "$dev_exists
" != "-1
" ]; then 
  209        elif [ "$dev_exists
" = "-1
" ]; then 
  216    # DSA: Ist Interface eine Bridge? 
  218    phy_dev="$(uci_get 
"network.${interface}.device")
" 
  220    while [ "$i
" -le "$max_dev_index
" ]; do 
  221        if [ "$(uci_get 
"network.@device[$i].name")
" = "${phy_dev}
" ] && [ "$(uci_get 
"network.@device[$i].type")
" = "bridge
" ]; then 
  223            printf '%s\n' "${phy_dev}
" 
  228    # swconfig: Ist Interface eine Bridge? 
  229    if [ "$(uci_get 
"network.${interface}.type")
" = "bridge
" ]; then 
  234    if [ "$found_bridge
" -ne 1 ]; then 
  235        # Interface ist keine Bridge 
  236        get_subdevices_of_interface "$interface
" 
  241# Ist das gegebene physische Netzwerk-Interface Teil einer Firewall-Zone? 
  243    trap 'error_trap is_device_in_zone "'"$*"'"' EXIT 
  248    for log_interface in $(get_zone_log_interfaces "$2
"); do 
  249        for item in $(get_subdevices_of_interface "$log_interface
"); do 
  250            [ "$device
" = "$item
" ] && return 0 
  254    trap "" EXIT && return 1 
  258## @fn _run_system_network_function() 
  259## @brief Führe eine der in /lib/functions/network.sh definierten Funktionen aus. 
  260## @param func: der Name der Funktion 
  261## @param ...: alle anderen Parameter werden der Funktion nach der Zielvariable (also ab 
  262##              Parameter #2) übergeben 
  263## @returns: die Ausgabe der Funktion 
  264_run_system_network_function() { 
  270        # shellcheck source=openwrt/package/base-files/files/lib/functions/network.sh 
  271        . /lib/functions/network.sh 
  273        [ -n "$result
" ] && echo "$result
" 
  278## @fn get_subdevices_of_interface() 
  279## @brief Ermittle die physischen Netzwerk-Geräte (bis auf wifi), die zu einem logischen Netzwerk-Interface gehören. 
  280## @details Im Fall eines Bridge-Interface werden nur die beteiligten Komponenten zurückgeliefert. 
  281##   Wifi-Geräte werden nur dann zurückgeliefert, wenn sie Teil einer Bridge sind. Andernfalls sind ihre Namen nicht 
  283## @returns Der oder die Namen der physischen Netzwerk-Geräte oder nichts. 
  284get_subdevices_of_interface() { 
  285    trap 'error_trap get_subdevices_of_interface "'"$*"'"' EXIT 
  290        # kabelgebundene Geräte 
  291        for device in $(uci_get "network.${interface}.device
"); do 
  292            # entferne Alias-Nummerierungen 
  293            device=$(echo "$device
" | cut -f 1 -d :) 
  294            [ -z "$device
" ] || [ "$device
" = "none
" ] && continue 
  297        # wir fügen das Ergebnis der ubus-Abfrage hinzu (unten werden Duplikate entfernt) 
  298        _run_system_network_function "network_get_physdev
" "$interface
" 
  299    } | tr ' ' '\n' | sort | uniq | while read -r device; do 
  300        # Falls das Verzeichnis existiert, ist es wohl eine Bridge, deren Bestandteile wir ausgeben. 
  301        # Ansonsten wird das Device ausgegeben. 
  302        ls "/sys/devices/
virtual/net/$device/brif/
" 2>/dev/null || echo "$device
" 
  303    done | sort | uniq | grep -v "^none$
" | grep -v "^
$" || true 
  307## @fn add_interface_to_zone() 
  308## @brief Fuege ein logisches Netzwerk-Interface zu einer Firewall-Zone hinzu. 
  309## @details Typischerweise ist diese Funktion nur fuer temporaere Netzwerkschnittstellen geeignet. 
  310add_interface_to_zone() { 
  314    uci_prefix=$(find_first_uci_section "firewall
" "zone
" "name=$zone
") 
  315    [ -z "$uci_prefix
" ] && msg_debug "Failed to add 
interface '$interface' to non-existing zone '$zone'" && 
return 0
 
  320## @fn del_interface_from_zone() 
  321## @brief Entferne ein logisches Interface aus einer Firewall-Zone. 
  326    uci_prefix=
$(find_first_uci_section 
"firewall" "zone" "name=$zone")
 
  327    [ -z 
"$uci_prefix" ] && 
msg_debug "Failed to remove interface '$interface' from non-existing zone '$zone'" && trap 
"" EXIT && 
return 1
 
  328    uci -q del_list 
"${uci_prefix}.network=$interface" 
  332## @fn get_zone_of_device() 
  333## @brief Ermittle die Zone eines physischen Netzwerk-Interfaces. 
  334## @param interface Name eines physischen Netzwerk-Interface (z.B. eth0) 
  335## @details Das Ergebnis ist ein leerer String, falls zu diesem Interface keine Zone existiert 
  336##   oder falls es das Interface nicht gibt. 
  338    trap 
'error_trap get_zone_of_device "'"$*"'"' EXIT
 
  344    find_all_uci_sections firewall zone | 
while read -r uci_prefix; 
do 
  345        zone=
$(uci_get 
"${uci_prefix}.name")  
# e.g. on_mesh 
  346        for interface in 
$(get_zone_log_interfaces "$zone"); 
do 
  347            for current_device 
in \ 
  350                [ 
"$current_device" = 
"$device" ] && echo 
"$device" && 
return 0
 
  355    # keine Zone gefunden 
  359## @fn get_zone_of_interface() 
  360## @brief Ermittle die Zone eines logischen Netzwerk-Interfaces. 
  361## @param interface Name eines logischen Netzwerk-Interface (z.B. eth0) 
  362## @details Das Ergebnis ist ein leerer String, falls zu diesem Interface keine Zone existiert 
  363##   oder falls es das Interface nicht gibt. 
  365    trap 
'error_trap get_zone_of_interface "'"$*"'"' EXIT
 
  370    find_all_uci_sections firewall zone | 
while read -r uci_prefix; 
do 
  371        zone=
$(uci_get 
"${uci_prefix}.name")
 
  372        interfaces=
$(get_zone_log_interfaces 
"$zone")
 
  373        is_in_list 
"$interface" "$interfaces" && echo -n 
"$zone" && 
return 0
 
  376    # ein leerer Rueckgabewert gilt als Fehler 
  381# Liefere die sortierte Liste der Opennet-Interfaces. 
  383# 1. dem Netzwerk ist ein Geraet zugeordnet 
  384# 2. Netzwerkname beginnend mit "on_wifi", "on_eth", ...
 
  385# 3. alphabetische Sortierung der Netzwerknamen 
  386get_sorted_opennet_interfaces() {
 
  387    trap 
'error_trap get_sorted_opennet_interfaces "'"$*"'"' EXIT
 
  390    # wir vergeben einfach statische Ordnungsnummern: 
  391    #   10 - konfigurierte Interfaces 
  392    #   20 - nicht konfigurierte Interfaces 
  393    # Offsets basierend auf dem Netzwerknamen: 
  397    for network in 
$(get_zone_log_interfaces 
"$ZONE_MESH"); 
do 
  399        [ -z 
"$(get_subdevices_of_interface "$network
")" ] && order=20
 
  400        if [ 
"${network#on_wifi}" != 
"$network" ]; then
 
  402        elif [ "
${network#on_eth}
" != "$network
" ]; then 
  407        echo "$order $network
" 
  408    done | sort -n | cut -f 2 -d " " 
  412# Liefere alle vorhandenen logischen Netzwerk-Schnittstellen (lan, wan, ...) zurueck. 
  413get_all_network_interfaces() { 
  415    # Die uci-network-Spezifikation sieht keine anonymen uci-Sektionen fuer Netzwerk-Interfaces vor. 
  416    # Somit ist es wohl korrekt, auf die Namen als Teil des uci-Pfads zu vertrauen. 
  417    find_all_uci_sections "network
" "interface
" | cut -f 2 -d . | while read -r interface; do 
  418        # ignoriere loopback-Interfaces und ungueltige 
  419        [ -z "$interface
" ] || [ "$interface
" = "none
" ] || [ "$interface
" = "loopback
" ] && continue 
  420        # alle uebrigen sind reale Interfaces 
  427## @fn delete_firewall_zone() 
  428## @brief Lösche eine Firewall-Zone, sowie alle Regeln, die sich auf diese Zone beziehen. 
  429## @param zone Name der Zone 
  430## @attention Anschließend ist ein "apply_changes firewall
" erforderlich. 
  431delete_firewall_zone() { 
  436    uci_prefix=$(find_first_uci_section firewall zone "name=$zone
") 
  437    uci_delete "$uci_prefix
" 
  438    for section in "forwarding
" "redirect
" "rule
"; do 
  439        for key in "src
" "dest
"; do 
  440            find_all_uci_sections firewall "$section
" "${
key}=$zone
" | while read -r uci_prefix; do 
  441                uci_delete "$uci_prefix
" 
  448## @fn is_interface_up() 
  449## @brief Prüfe ob ein logisches Netzwerk-Interface aktiv ist. 
  450## @param interface Zu prüfendes logisches Netzwerk-Interface 
  451## @details Im Fall eines Bridge-Interface wird sowohl der Status der Bridge (muss aktiv sein), als 
  452##   auch der Status der Bridge-Teilnehmer (mindestens einer muss aktiv sein) geprüft. 
  454    trap 'error_trap is_interface_up "'"$*"'"' EXIT 
  456    # falls es ein uebergeordnetes Bridge-Interface geben sollte, dann muss dies ebenfalls aktiv sein 
  457    if [ "$(uci_get 
"network.${interface}.type")
" = "bridge
" ]; then 
  458        # das Bridge-Interface existiert nicht (d.h. es ist down) 
  459        [ -z "$(ip link show dev 
"br-${interface}" 2>/dev/
null || 
true)
" ] && trap "" EXIT && return 1 
  460        # Bridge ist aus? Damit ist das befragte Interface ebenfalls aus ... 
  461        ip link show dev "br-
${interface}
" | grep -q '[\t ]state DOWN[\ ]' && trap "" EXIT && return 1 
  464    for device in $(get_subdevices_of_interface "$interface
"); do 
  465        ip link show dev "$device
" | grep -q '[\t ]state UP[\ ]' && return 0 
  468    trap "" EXIT && return 1 
  472## @fn get_ipv4_of_mac() 
  473## @brief Ermittle die IPv4-Adresse zu einer MAC-Adresse 
  474## @param mac MAC-Adresse eines Nachbarn 
  477    awk '{ if ($4 == "'"$ip"'") print $1; }' /proc/net/arp | sort | head -1 
  481is_current_wifi_interface_channel_with_dfs() { 
  482    local wifi_interface="$1
" 
  484    channel=$(iwinfo "$wifi_interface
" info | awk '{ if ($3 == "Channel:
") print $4 }') 
  485    echo "$channel
" | grep -q "^[0-9]\+
$" || return 1 
  486    [ "$channel
" -gt 48 ] 
  491    trap 'error_trap run_iwinfo_scan "'"$*"'"' EXIT 
  492    local phy_device="$1
" 
  493    local wifi_interface_uci 
  495    local original_channel 
  496    local needs_auto_channel 
  497    wifi_interface_uci=$(find_first_uci_section "wireless
" "wifi-iface
" "device=$phy_device
") 
  498    [ -z "$wifi_interface_uci
" ] && return 0 
  499    # the field "device
" is not strictly specified - thus fall back to a sane default 
  500    wifi_interface=$(uci_get "$wifi_interface_uci.device
" "wlan0
") 
  501    # try whether it works without any changes and exit early in case of success 
  502    iwinfo "$wifi_interface
" scan 2>/dev/null && return 0 
  503    # Possible reason for failure: "scan
" does not seem to work for a master on a DFS channel: 
  504    #   https://forum.openwrt.org/t/cannot-scan-on-5g-on-tl-wr902ac/42863/2 
  505    # (but sometimes it indeed works - thus we tried it in advance before, anyway) 
  506    # Try hard to prepare a situation where it is possible to scan the channels. 
  507    # Sadly this may break the HTTP connection (e.g. when accessing the wireless scan via the 
  508    # web interface) if the user accessed the IP of the wireless interface. 
  509    if [ "$(uci_get 
"$wifi_interface_uci.mode")
" = "ap
" ] && is_current_wifi_interface_channel_with_dfs "$wifi_interface
"; then 
  510        original_channel=$(uci_get "wireless.$phy_device.channel
") 
  511        needs_auto_channel="true" 
  513        needs_auto_channel="false" 
  515    if [ "$needs_auto_channel
" = "true" ]; then 
  516        # switch to a non-DFS channel 
  517        uci set "wireless.$phy_device.channel=48
" 
  518        uci commit "wireless.$phy_device
" 
  522    iwinfo "$wifi_interface
" scan 
  523    # revert to the original setup 
  524    if [ "$needs_auto_channel
" = "true" ]; then 
  525        uci set "wireless.$phy_device.channel=$original_channel
" 
  526        uci commit "wireless.$phy_device
" 
  532get_potential_opennet_scan_results_for_device() { 
  533    trap 'error_trap get_potential_opennet_scan_results_for_device "'"$*"'"' EXIT 
  534    local phy_device="$1
" 
  535    run_iwinfo_scan "$phy_device
" \ 
  537            if ($1 == "ESSID:
") { if ((name != "") && (encryption == "none
")) print(signal"\t
"channel"\t
"quality"\t
"name); split($0, tokens, /"/); name=tokens[2]; };
 
  538            if ($1 == 
"Signal:") signal=$2;
 
  539            if ($4 == 
"Quality:") quality=substr($5, 0, index($5, 
"/") - 1);
 
  540            if ($3 == 
"Channel:") channel=$4;
 
  541            if ($1 == 
"Encryption:") encryption=$2;
 
  544        | grep -E "(opennet|\bon\b)" \ 
  545        | grep -vF "join.opennet-initiative.de" || true 
  548# Ende der Doku-Gruppe 
msg_debug(message)
Debug-Meldungen ins syslog schreiben.
 
msg_info(message)
Informationen und Fehlermeldungen ins syslog schreiben.
 
get_subdevices_of_interface()
Ermittle die physischen Netzwerk-Geräte (bis auf wifi), die zu einem logischen Netzwerk-Interface geh...
 
_run_system_network_function(func:,...:)
Führe eine der in /lib/functions/network.sh definierten Funktionen aus.
 
get_zone_raw_devices()
Ermittle die physischen Netzwerkinterfaces, die direkt einer Firewall-Zone zugeordnet sind.
 
get_zone_of_device(interface)
Ermittle die Zone eines physischen Netzwerk-Interfaces.
 
get_device_of_interface()
Ermittle das physische Netzwerk-Gerät, das einem logischen Netzwerk entspricht.
 
get_ping_time(target, duration)
Ermittle die Latenz eines Ping-Pakets auf dem Weg zu einem Ziel.
 
has_opennet_dns()
Prüfe, ob *.on-Domains aufgelöst werden.
 
get_zone_of_interface(interface)
Ermittle die Zone eines logischen Netzwerk-Interfaces.
 
del_interface_from_zone()
Entferne ein logisches Interface aus einer Firewall-Zone.
 
get_current_addresses_of_network()
Liefere die IP-Adressen eines logischen Interface inkl. Praefix-Laenge (z.B. 172.16....
 
filter_routable_addresses()
Filtere aus einer Menge von Ziel-IPs diejenigen heraus, für die eine passende Routing-Regel existiert...
 
create_uci_section_if_missing()
Prüfe, ob eine definierte UCI-Sektion existiert und lege sie andernfalls an.
 
uci_delete(uci_path)
Lösche ein UCI-Element.
 
uci_replace_list()
Replace the items in a list. Wanted items are expected via stdin (one per line, uci_path).
 
uci_get_list(uci_path)
Liefere alle einzelnen Elemente einer UCI-Liste zurück.
 
uci_add_list(uci_path, new_item)
Füge einen neuen Wert zu einer UCI-Liste hinzu und achte dabei auf Einmaligkeit.
 
set eu grep root::etc shadow exit if command v chpasswd dev null
 
set eu on function print_services services log for dir in etc on services d var on services volatile d