From 842225fd40ebbd98be41f21899e19b6ecd245815 Mon Sep 17 00:00:00 2001 From: David Young Date: Wed, 28 Oct 2026 15:57:54 -0600 Subject: [PATCH] add audit --- README.md | 8 + audit | 571 ++++++++++++++++++++++++++++++++++++++++++++++ fix-networking.sh | 7 +- install.sh | 5 +- update | 1 + utils.sh | 32 ++- 6 files changed, 614 insertions(+), 10 deletions(-) create mode 100755 audit diff --git a/README.md b/README.md index 444c74b..00d7238 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,11 @@ To make your own repository work as a sibling project, see [skills/sibling-proje ## Firewall firewalld blocks incoming connections except for the services and ports listed in [firewall.conf](firewall.conf) and in each sibling project's own `firewall.conf`. Each `update` opens anything newly listed, and closes ports MiniServer opened that nothing lists any more, for example after `sibling remove`. Rules you add yourself with `firewall-cmd` are left alone. + +To see what's actually open: + +``` +audit +``` + +It lists every port something on the server is listening on, whatever installed it, along with who can reach it (this machine only, the LAN, Tailscale or containers) and the service behind it. Each port the LAN can reach is marked with the `firewall.conf` that asks for it, or `[in no firewall.conf]` if it was opened some other way. Ports Docker containers publish are shown as reachable whatever firewalld says, because Docker opens them itself. You can stop or start the services from the list; one you stop stays off, through `update` and `refresh` too, until you start it again in `audit`. `audit --list` only shows the ports. diff --git a/audit b/audit new file mode 100755 index 0000000..e93722b --- /dev/null +++ b/audit @@ -0,0 +1,571 @@ +#!/bin/bash +# +# Purpose : Show the network ports open on this machine: who can reach each one +# through the firewall, which service opened it and why, and let you +# stop or start those services. +# +# A service you stop here is also kept from starting at boot, and update leaves +# it off (see enable_services in utils.sh) until you start it here again. + +# Wrapped in { ...; exit; } so bash reads the whole script up front; update +# replaces this file while it runs. +{ + +MINISERVER="$HOME/.local/share/MiniServer" +# Provides firewall_entries and SERVICES_OFF +source "$MINISERVER/utils.sh" + +# Why a service or app opens ports, by systemd unit or process name. Services +# with ports here are also listed while they're stopped, so they can be started. +# Format: name|ports|why +KNOWN=( + "sshd.service|22/tcp|SSH: log in to this machine from other machines" + "systemd-resolved.service||Looks up domain names (DNS); 5355 is LLMNR, which answers name lookups from the LAN" + "avahi-daemon.service|5353/udp|mDNS: lets machines on your LAN reach this one as $(uname -n).local" + "cockpit.socket|9090/tcp|Cockpit: manage this server from a web browser" + "cockpit.service|9090/tcp|Cockpit: manage this server from a web browser" + "tailscaled.service|41641/udp|Tailscale: your Core's private network" + "docker.service||Docker" + "pm2-$USER.service||pm2: keeps your Node apps running; each app opens its own ports" + "hermes-gateway.service||The sandbox's proxy for Hermes Agent's gateway (srt); only this machine can reach it" +) + +# Stopping these would break the server's network, its firewall or the +# graphical login, so audit doesn't offer to +PROTECTED="systemd-resolved.service NetworkManager.service systemd-networkd.service firewalld.service sddm.service dbus.service dbus-broker.service" + +usage() { + cat << EOF +Usage: audit [--list] + + audit Show the open ports and choose a service to stop or start + audit --list Only show the open ports + +Run it with sudo rights: without them, the services run by root can't be named. +EOF +} + +known_field() { # + local entry + for entry in "${KNOWN[@]}"; do + [ "${entry%%|*}" = "$1" ] && { cut -d'|' -f"$2" <<< "$entry"; return 0; } + done + return 1 +} + +# The running known service that uses , for a socket whose +# process only root can see +known_port_owner() { + local entry unit + for entry in "${KNOWN[@]}"; do + unit="${entry%%|*}" + [[ ",$(cut -d'|' -f2 <<< "$entry")," == *",$2/$1,"* ]] || continue + systemctl is-active --quiet "$unit" 2> /dev/null && { echo "system $unit"; return 0; } + done + return 1 +} + +# firewall-cmd, which only answers while firewalld runs +fw() { + if [ -n "$NO_SUDO" ]; then firewall-cmd "$@"; else sudo firewall-cmd "$@"; fi +} + +# The ports a firewalld service opens, as "proto port" lines in +# SERVICE_PORTS[service], with those of the services it includes +declare -A SERVICE_PORTS +cache_service() { # + [ -n "${SERVICE_PORTS[$1]+set}" ] && return + # Set first, so services that include each other don't loop + SERVICE_PORTS[$1]="" + local info entry inc ports="" + info="$(fw --info-service="$1" 2> /dev/null)" + for entry in $(sed -n 's/^ *ports: //p' <<< "$info"); do + ports+="${entry#*/} ${entry%/*}"$'\n' + done + for inc in $(sed -n 's/^ *includes: //p' <<< "$info"); do + cache_service "$inc" + ports+="${SERVICE_PORTS[$inc]}" + done + SERVICE_PORTS[$1]="$ports" +} + +# Add " " lines to ZONE_RULES[zone] for a service, where +# level is full, or some when only some source addresses are let in +add_service_rules() { # + local proto port + cache_service "$3" + while read -r proto port; do + [ -n "$proto" ] && ZONE_RULES[$1]+="$2 $proto $port"$'\n' + done <<< "${SERVICE_PORTS[$3]}" +} + +# What firewalld lets in, from every zone: ZONE_RULES[zone] holds +# " " lines ("any" for any protocol or port; a port can be +# a range), ZONE_OF_IFACE the zone each interface was put in (others are in +# DEFAULT_ZONE), and SOURCE_ZONES the zones that apply to some source +# addresses on every interface +load_firewall() { + FIREWALL_ON=0 FIREWALL_UNREAD=0 DEFAULT_ZONE="" + declare -gA ZONE_RULES ZONE_OF_IFACE + ZONE_RULES=() ZONE_OF_IFACE=() SOURCE_ZONES=() + systemctl is-active --quiet firewalld && FIREWALL_ON=1 + [ "$FIREWALL_ON" = 1 ] || return 0 + + local all line zone key value level item + if ! all="$(fw --list-all-zones 2> /dev/null)"; then + FIREWALL_UNREAD=1 + return 0 + fi + while IFS= read -r line; do + # A zone starts unindented, e.g. "public (default, active)" + if [[ "$line" =~ ^([^[:space:]]+)( \((.*)\))?$ ]]; then + zone="${BASH_REMATCH[1]}" + [[ "${BASH_REMATCH[3]}" == *default* ]] && DEFAULT_ZONE="$zone" + continue + fi + [ -n "$zone" ] || continue + + # Rich rules, one per line after "rich rules:". Only those that accept + # connections count; one limited to a source lets in only some addresses. + if [[ "$line" =~ ^[[:space:]]+rule[[:space:]] ]]; then + [[ "$line" =~ [[:space:]]accept([[:space:]]|$) ]] || continue + level=full + [[ "$line" == *"source address="* || "$line" == *"source ipset="* || "$line" == *"source mac="* ]] && level=some + if [[ "$line" =~ [[:space:]]port\ port=\"([0-9-]+)\"\ protocol=\"([a-z]+)\" ]]; then + ZONE_RULES[$zone]+="$level ${BASH_REMATCH[2]} ${BASH_REMATCH[1]}"$'\n' + elif [[ "$line" =~ service\ name=\"([^\"]+)\" ]]; then + add_service_rules "$zone" "$level" "${BASH_REMATCH[1]}" + else + ZONE_RULES[$zone]+="$level any any"$'\n' + fi + continue + fi + + [[ "$line" =~ ^[[:space:]]+([a-z -]+):[[:space:]]?(.*)$ ]] || continue + key="${BASH_REMATCH[1]}" value="${BASH_REMATCH[2]}" + case "$key" in + target) + [ "$value" = ACCEPT ] && ZONE_RULES[$zone]+="full any any"$'\n' ;; + interfaces) + for item in $value; do ZONE_OF_IFACE[$item]="$zone"; done ;; + sources) + [ -n "$value" ] && SOURCE_ZONES+=("$zone") ;; + services) + for item in $value; do add_service_rules "$zone" full "$item"; done ;; + ports) + for item in $value; do ZONE_RULES[$zone]+="full ${item#*/} ${item%/*}"$'\n'; done ;; + esac + done <<< "$all" +} + +# How a zone lets in: full, some (from some addresses) or "" +zone_allows() { # + local level proto port best="" + while read -r level proto port; do + [ -n "$level" ] || continue + [[ "$proto" == any || "$proto" == "$2" ]] || continue + [[ "$port" == any ]] || (( $3 >= ${port%-*} && $3 <= ${port#*-} )) || continue + [ "$level" = full ] && { echo full; return; } + best=some + done <<< "${ZONE_RULES[$1]}" + echo "$best" +} + +# How the firewall lets in from the network of (lan, +# tailscale or containers): full, some or "" +firewall_allows() { # + local iface level best="" zone + if [ "$FIREWALL_ON" = 0 ] || [ "$FIREWALL_UNREAD" = 1 ]; then + [ -n "${IFACES_OF_KIND[$3]}" ] && echo full + return + fi + for iface in ${IFACES_OF_KIND[$3]}; do + level="$(zone_allows "${ZONE_OF_IFACE[$iface]:-$DEFAULT_ZONE}" "$1" "$2")" + [ "$level" = full ] && { echo full; return; } + [ -n "$level" ] && best=some + done + # A zone bound to source addresses lets them in on any interface + if [ -n "${IFACES_OF_KIND[$3]}" ]; then + for zone in "${SOURCE_ZONES[@]}"; do + [ -n "$(zone_allows "$zone" "$1" "$2")" ] && best=some + done + fi + echo "$best" +} + +# The ports each firewall.conf asks for, as " " in +# CONF_PORTS, with services expanded to their ports +load_firewall_confs() { + local conf name entry proto port + CONF_PORTS=() + # Only shown alongside the firewall's rules + [ "$FIREWALL_ON" = 1 ] && [ "$FIREWALL_UNREAD" = 0 ] || return 0 + for conf in "$MINISERVER/firewall.conf" "$HOME/.local/share/siblings/"*/firewall.conf; do + [ -f "$conf" ] || continue + name=MiniServer + [[ "$conf" == */siblings/* ]] && name="$(basename "$(dirname "$conf")")" + while IFS= read -r entry; do + case "$entry" in + port:*) + entry="${entry#port:}" + CONF_PORTS+=("${entry#*/} ${entry%/*} $name") ;; + service:*) + entry="${entry#service:}" + cache_service "$entry" + while read -r proto port; do + [ -n "$proto" ] && CONF_PORTS+=("$proto $port $name") + done <<< "${SERVICE_PORTS[$entry]}" ;; + esac + done < <(firewall_entries "$conf" 2> /dev/null) + done +} + +# The projects whose firewall.conf asks for , comma-separated +conf_owners() { # + local entry proto port name names="" + [[ "$2" =~ ^[0-9]+$ ]] || return 0 + for entry in "${CONF_PORTS[@]}"; do + read -r proto port name <<< "$entry" + [ "$proto" = "$1" ] || continue + (( $2 >= ${port%-*} && $2 <= ${port#*-} )) || continue + [[ ", $names, " == *", $name, "* ]] || names+="${names:+, }$name" + done + echo "$names" +} + +# Which network an interface is on: local, lan, tailscale or containers +iface_kind() { + case "$1" in + lo) echo local ;; + tailscale*) echo tailscale ;; + docker*|br-*|veth*|virbr*) echo containers ;; + *) echo lan ;; + esac +} + +# The systemd unit a process runs in, as "system ", "user ", +# "app " or "none " +process_owner() { + local pid="$1" comm="$2" cgroup unit + cgroup="$(cut -d: -f3- "/proc/$pid/cgroup" 2> /dev/null | head -n 1)" + unit="${cgroup##*/}" + case "$cgroup" in + */app-flatpak-*.scope) + unit="${unit#app-flatpak-}" + echo "app ${unit%-*.scope}" ;; + *.scope) echo "app $comm" ;; + /system.slice/*.service) echo "system $unit" ;; + /user.slice/*/user@*.service/*.service) echo "user $unit" ;; + *) echo "none $comm" ;; + esac +} + +# A port systemd itself listens on, for a service it starts on demand +socket_unit() { # + systemctl list-sockets --all --no-legend 2> /dev/null \ + | awk -v port="$1" '$1 ~ ":" port "$" { print $2; exit }' +} + +# Collect the listening sockets into ROWS, one per service and port, with the +# networks each listens on; and the interfaces on each network into +# IFACES_OF_KIND +load_ports() { + local -A iface_of seen + local netid local_addr process addr port iface kind pid comm owner key + ROW_KEYS=() + declare -gA ROW_PROTO ROW_PORT ROW_OWNER ROW_KINDS IFACES_OF_KIND + ROW_PROTO=() ROW_PORT=() ROW_OWNER=() ROW_KINDS=() IFACES_OF_KIND=() + + while read -r _ iface _ addr _; do + iface_of["${addr%/*}"]="$iface" + kind="$(iface_kind "$iface")" + [[ " ${IFACES_OF_KIND[$kind]} " == *" $iface "* ]] || IFACES_OF_KIND[$kind]+=" $iface" + done < <(ip -o addr show 2> /dev/null) + + while read -r netid _ _ _ local_addr _ process; do + port="${local_addr##*:}" + addr="${local_addr%:*}" + addr="${addr#[}" addr="${addr%]}" + iface="" + [[ "$addr" == *%* ]] && iface="${addr#*%}" addr="${addr%%%*}" + case "$addr" in + 0.0.0.0|::|\*) kind=all ;; + 127.*|::1) kind=local ;; + *) kind="$(iface_kind "${iface:-${iface_of[$addr]:-lan}}")" ;; + esac + + pid="$(grep -o 'pid=[0-9]*' <<< "$process" | head -n 1 | cut -d= -f2)" + comm="$(sed -nE 's/^users:\(\("([^"]*)".*/\1/p' <<< "$process")" + if [ -z "$pid" ]; then + owner="$(known_port_owner "$netid" "$port")" || owner="none ?" + elif [ "$comm" = systemd ] && unit="$(socket_unit "$port")" && [ -n "$unit" ]; then + owner="system $unit" + elif [ "$comm" = docker-proxy ]; then + owner="none docker-proxy" + else + owner="$(process_owner "$pid" "$comm")" + fi + + # Random high ports bound to one address are a service's replies and + # discovery, not something to connect to: one row for all of them + [[ "$kind" != all && "$port" -ge 32768 ]] && port=random + key="$netid $port $owner" + if [ -z "${seen[$key]}" ]; then + seen[$key]=1 + ROW_KEYS+=("$key") + ROW_PROTO[$key]="$netid" ROW_PORT[$key]="$port" ROW_OWNER[$key]="$owner" + fi + [[ " ${ROW_KINDS[$key]} " == *" $kind "* ]] || ROW_KINDS[$key]+=" $kind" + done < <($SS -tulpnH 2> /dev/null) +} + +# Who can connect to a row's port, given where it listens and the firewall +reach_label() { # + local proto="$1" port="$2" kinds="$3" lan=0 ts=0 ct=0 blocked=0 parts=() + [ "$port" = random ] && port=0 + for kind in $kinds; do + case "$kind" in + all) lan=1 ts=1 ;; + lan) lan=1 ;; + tailscale) ts=1 ;; + containers) ct=1 ;; + esac + done + if [ "$lan" = 1 ]; then + # Docker opens the ports containers publish itself, ahead of firewalld + if [ "$4" = "none docker-proxy" ]; then + parts+=("\e[31mLAN\e[0m (past the firewall)") + else + case "$(firewall_allows "$proto" "$port" lan)" in + full) parts+=("\e[31mLAN\e[0m") ;; + some) parts+=("\e[31mLAN\e[0m (some addresses)") ;; + *) blocked=1 ;; + esac + fi + fi + if [ "$ts" = 1 ] && [ -n "$(firewall_allows "$proto" "$port" tailscale)" ]; then + parts+=("\e[33mTailscale\e[0m") + fi + if [ "$ct" = 1 ] && [ -n "$(firewall_allows "$proto" "$port" containers)" ]; then + parts+=("\e[33mcontainers\e[0m") + fi + [ ${#parts[@]} -eq 0 ] && parts=("\e[32mthis machine\e[0m") + local label="${parts[0]}" part + for part in "${parts[@]:1}"; do label+=", $part"; done + [ "$blocked" = 1 ] && label+=" (firewall blocks LAN)" + echo -n "$label" +} + +owner_label() { + local scope="${1%% *}" name="${1#* }" + case "$scope" in + system) echo "${name%.service}" ;; + user) echo "${name%.service} (yours)" ;; + app) echo "$name (app)" ;; + *) [ "$name" = docker-proxy ] && echo "Docker container" || echo "$name" ;; + esac +} + +why() { + local scope="${1%% *}" name="${1#* }" text + text="$(known_field "$name" 3)" && { echo "$text"; return; } + case "$scope" in + system) systemctl show -p Description --value "$name" 2> /dev/null ;; + user) systemctl --user show -p Description --value "$name" 2> /dev/null ;; + none) + case "$name" in + "?") echo "Run audit with sudo rights to see which service this is" ;; + docker-proxy) echo "A port a Docker container publishes; see: docker ps" ;; + esac ;; + esac +} + +# For a port other machines on the LAN can reach: which firewall.conf asks for +# it, or a warning that none does. Containers' ports needn't be listed. +conf_note() { # + local owners + [ "$3" = "none docker-proxy" ] && return + owners="$(conf_owners "$1" "$2")" + if [ -n "$owners" ]; then + echo -n " [firewall.conf: $owners]" + else + echo -n " \e[31m[in no firewall.conf]\e[0m" + fi +} + +systemctl_for() { # + if [ "$1" = user ]; then systemctl --user "${@:2}"; else sudo systemctl "${@:2}"; fi +} + +is_active() { # + if [ "$1" = user ]; then systemctl --user is-active --quiet "$2"; else systemctl is-active --quiet "$2"; fi +} + +# Services that can be stopped or started here, as "scope unit": those with +# open ports, then the known ones that are installed +load_services() { + local key owner entry unit + SERVICES_LIST=() + for key in "${ROW_KEYS[@]}"; do + owner="${ROW_OWNER[$key]}" + [[ "$owner" == system\ * || "$owner" == user\ * ]] || continue + [[ " $PROTECTED " == *" ${owner#* } "* ]] && continue + [[ " ${SERVICES_LIST[*]} " == *" $owner "* ]] || SERVICES_LIST+=("$owner") + done + for entry in "${KNOWN[@]}"; do + unit="${entry%%|*}" + [[ "$unit" == *.service || "$unit" == *.socket ]] || continue + [ -n "$(known_field "$unit" 2)" ] || continue + [[ " ${SERVICES_LIST[*]} " == *" system $unit "* ]] && continue + # Listed even while running without its port open yet, as right after + # starting, so it doesn't seem to vanish + systemctl cat "$unit" &> /dev/null || continue + # cockpit.socket and cockpit.service are one service to the user + [[ "$unit" == *.socket && " ${SERVICES_LIST[*]} " == *" system ${unit%.socket}.service "* ]] && continue + [[ "$unit" == *.service && " ${SERVICES_LIST[*]} " == *" system ${unit%.service}.socket "* ]] && continue + SERVICES_LIST+=("system $unit") + done +} + +show_ports() { + local key reach note text + echo + if [ "$FIREWALL_ON" = 0 ]; then + echo -e "\e[31mFirewall (firewalld) off: every port listening beyond this machine can be reached.\e[0m" + elif [ "$FIREWALL_UNREAD" = 1 ]; then + echo -e "\e[33mFirewall (firewalld) on, but its rules couldn't be read: ports are shown as if it let everything in.\e[0m" + else + echo "Firewall (firewalld) on: it blocks connections from other machines except to the ports its zones allow." + echo "Ports the LAN can reach show which firewall.conf asks for them (MiniServer's or a sibling project's)." + fi + [ -n "$NO_SUDO" ] && echo "Without sudo rights, the services run by root show as \"?\"." + echo + printf "\e[1m%-7s %-5s %-34s %-26s %s\e[0m\n" PORT PROTO "OPEN TO" SERVICE WHY + local sorted visible + # By port, with the random ports last + mapfile -t sorted < <(for key in "${ROW_KEYS[@]}"; do + printf '%s\t%s\n' "${ROW_PORT[$key]/random/99999}" "$key"; done | sort -n -k1,1 | cut -f2-) + for key in "${sorted[@]}"; do + reach="$(reach_label "${ROW_PROTO[$key]}" "${ROW_PORT[$key]}" "${ROW_KINDS[$key]}" "${ROW_OWNER[$key]}")" + # printf counts the color codes as width, so pad by the visible length + visible="$(sed 's/\\e\[[0-9;]*m//g' <<< "$reach")" + note="" + if [[ "$visible" == *LAN* && "$visible" != *"blocks LAN"* && "$FIREWALL_ON" = 1 && "$FIREWALL_UNREAD" = 0 ]]; then + note="$(conf_note "${ROW_PROTO[$key]}" "${ROW_PORT[$key]}" "${ROW_OWNER[$key]}")" + fi + text="$(why "${ROW_OWNER[$key]}")" + [ -z "$text" ] && note="${note# }" + printf "%-7s %-5s %b%*s %-26s %s%b\n" "${ROW_PORT[$key]}" "${ROW_PROTO[$key]}" \ + "$reach" $((34 - ${#visible})) "" "$(owner_label "${ROW_OWNER[$key]}")" "$text" "$note" + done +} + +show_services() { + local i scope unit state + echo + echo "Services you can stop or start:" + echo + for i in "${!SERVICES_LIST[@]}"; do + scope="${SERVICES_LIST[$i]%% *}" unit="${SERVICES_LIST[$i]#* }" + if is_active "$scope" "$unit"; then state="\e[32mrunning\e[0m"; else state="\e[33mstopped\e[0m"; fi + printf " %2d) %-30b %b\n" $((i + 1)) "$(owner_label "${SERVICES_LIST[$i]}")" "$state" + done +} + +toggle() { # + local scope="$1" unit="$2" units=("$2") pair + if [[ " $PROTECTED " == *" $unit "* ]]; then + echo "$unit keeps the server or its network working, so audit won't stop it." + return 1 + fi + if [ "$unit" = sshd.service ] && [ -n "$SSH_CONNECTION" ] && is_active "$scope" "$unit"; then + echo "You're logged in over SSH: stopping sshd would lock you out." + return 1 + fi + # A service started on demand has a socket unit too; both go together + case "$unit" in + *.socket) pair="${unit%.socket}.service" ;; + *.service) pair="${unit%.service}.socket" ;; + esac + systemctl_for "$scope" cat "$pair" &> /dev/null && units+=("$pair") + + mkdir -p "$(dirname "$SERVICES_OFF")" + if is_active "$scope" "$unit"; then + confirm "Stop ${units[*]} and keep it from starting at boot?" || return 0 + systemctl_for "$scope" disable --now "${units[@]}" || return 1 + for unit in "${units[@]}"; do + grep -qsx "$scope $unit" "$SERVICES_OFF" || echo "$scope $unit" >> "$SERVICES_OFF" + done + else + confirm "Start ${units[*]} and start it at boot?" || return 0 + systemctl_for "$scope" enable --now "${units[@]}" || return 1 + for unit in "${units[@]}"; do + sed -i "\|^$scope $unit\$|d" "$SERVICES_OFF" 2> /dev/null + done + wait_for_ports "$2" + fi +} + +# A service takes a moment to open its ports once started: wait up to 10 +# seconds for its known ones, so the table shows them +wait_for_ports() { # + local ports port proto + ports="$(known_field "$1" 2)" || return 0 + for port in ${ports//,/ }; do + proto="${port#*/}" port="${port%/*}" + echo "Waiting for $1 to open port $port..." + for _ in $(seq 40); do + [ -n "$(ss -lnH "--$proto" "sport = :$port" 2> /dev/null)" ] && break + sleep 0.25 + done + done +} + +confirm() { + local answer + read -rp "$1 [y/N] " answer + [[ "$answer" == [yY]* ]] +} + +case "$1" in + ""|-l|--list) ;; + -h|--help) usage; exit 0 ;; + *) usage; exit 2 ;; +esac + +# Root's sockets name their process only to root +SS="sudo ss" +NO_SUDO="" +if ! sudo -v; then + SS="ss" + NO_SUDO=1 +fi + +while true; do + load_firewall + load_firewall_confs + load_ports + show_ports + [ -n "$1" ] && exit 0 + + load_services + show_services + echo + echo " q) Quit" + echo + while true; do + read -rp "Choose a service to stop or start: " choice || exit 0 + case "$choice" in + q|Q|"") exit 0 ;; + *[!0-9]*) ;; + *) + if (( choice >= 1 && choice <= ${#SERVICES_LIST[@]} )); then + entry="${SERVICES_LIST[$((choice - 1))]}" + toggle "${entry%% *}" "${entry#* }" + break + fi ;; + esac + echo "Choose a number from the list, or q." + done +done +} diff --git a/fix-networking.sh b/fix-networking.sh index 1f000ba..7bb992f 100644 --- a/fix-networking.sh +++ b/fix-networking.sh @@ -4,11 +4,14 @@ set -e +# Provides enable_services +source "$HOME/.local/share/MiniServer/utils.sh" + echo "Installing Avahi and nss-mdns..." sudo pacman -S --noconfirm avahi nss-mdns -echo "Enabling Avahi daemon..." -sudo systemctl enable avahi-daemon.service +# Left off if you turned it off with audit +enable_services avahi-daemon.service echo "Updating /etc/nsswitch.conf..." sudo sed -i 's/^hosts:.*/hosts: mymachines mdns_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] files myhostname dns/' /etc/nsswitch.conf diff --git a/install.sh b/install.sh index dd001a6..c73074b 100755 --- a/install.sh +++ b/install.sh @@ -76,13 +76,14 @@ cd ~/.local/share/MiniServer # Source utility functions (provides install_packages, install_script and other helpers) source utils.sh -# Install the refresh, update, sibling, version and configure scripts to the user's bin +# Install the refresh, update, sibling, version, configure and audit scripts to the user's bin mkdir -p "$HOME/.local/bin" install_script "$HOME/.local/share/MiniServer/refresh" "$HOME/.local/bin/refresh" install_script "$HOME/.local/share/MiniServer/update" "$HOME/.local/bin/update" install_script "$HOME/.local/share/MiniServer/sibling" "$HOME/.local/bin/sibling" install_script "$HOME/.local/share/MiniServer/version" "$HOME/.local/bin/version" install_script "$HOME/.local/share/MiniServer/configure" "$HOME/.local/bin/configure" +install_script "$HOME/.local/share/MiniServer/audit" "$HOME/.local/bin/audit" # Replaced by refresh and update rm -f "$HOME/.local/bin/miniserverrefresh" "$HOME/.local/bin/miniserverupdate" @@ -142,7 +143,7 @@ cd ~/.local/share/MiniServer # Set up Cockpit (installed with the utilities above) setup_cockpit -echo "Cockpit is at https://$(uname -n).local:9090" +service_turned_off cockpit.socket || echo "Cockpit is at https://$(uname -n).local:9090" # Set up firewalld (installed with the utilities above) setup_firewall || echo "Some firewall rules could not be applied, see above. Run 'update' to retry." diff --git a/update b/update index e21a20e..87cc0c1 100755 --- a/update +++ b/update @@ -51,6 +51,7 @@ install_script "$MINISERVER/refresh" ~/.local/bin/refresh install_script "$MINISERVER/sibling" ~/.local/bin/sibling install_script "$MINISERVER/version" ~/.local/bin/version install_script "$MINISERVER/configure" ~/.local/bin/configure +install_script "$MINISERVER/audit" ~/.local/bin/audit if [[ -z "$UPDATE_REEXECED" ]]; then export UPDATE_REEXECED=1 diff --git a/utils.sh b/utils.sh index 47b07e3..6fe60a0 100755 --- a/utils.sh +++ b/utils.sh @@ -44,12 +44,29 @@ remove_packages() { fi } -# Function to enable systemd services if not already enabled +# Services you turned off with audit, as " " lines +SERVICES_OFF="$HOME/.config/miniserver/services-off" + +# True if you turned off the system service with audit +service_turned_off() { + grep -qsx "system $1" "$SERVICES_OFF" +} + +# Function to enable systemd services if not already enabled. With --now, also +# start them. Services you turned off with audit are left off. enable_services() { + local now=() + if [ "$1" = "--now" ]; then + now=(--now) + shift + sudo systemctl daemon-reload + fi for service in "$@"; do - if ! systemctl is-enabled "$service" &> /dev/null; then + if service_turned_off "$service"; then + echo "$service is turned off (see audit)." + elif ! systemctl is-enabled "$service" &> /dev/null; then echo "Enabling $service..." - sudo systemctl enable "$service" + sudo systemctl enable "${now[@]}" "$service" else echo "$service is already enabled." fi @@ -97,10 +114,13 @@ install_script() { tmp="$(mktemp "$2.XXXXXX")" && cp "$1" "$tmp" && chmod 755 "$tmp" && mv -f "$tmp" "$2" } -# Start Cockpit. Safe to re-run: does nothing once set up. The cockpit package -# itself comes from packages.conf; its firewall port is in firewall.conf. +# Start Cockpit, unless you turned it off with audit. Safe to re-run: does +# nothing once set up. The cockpit package itself comes from packages.conf; its +# firewall port is in firewall.conf. setup_cockpit() { - if ! systemctl is-enabled --quiet cockpit.socket || ! systemctl is-active --quiet cockpit.socket; then + if service_turned_off cockpit.socket; then + echo "Cockpit is turned off (see audit)." + elif ! systemctl is-enabled --quiet cockpit.socket || ! systemctl is-active --quiet cockpit.socket; then echo "Enabling Cockpit..." sudo systemctl enable --now cockpit.socket fi