572 lines
20 KiB
Bash
Executable File
572 lines
20 KiB
Bash
Executable File
#!/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() { # <name> <field: 2=ports 3=why>
|
|
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 <proto> <port>, 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() { # <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 "<level> <proto> <port>" 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() { # <zone> <level> <service>
|
|
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
|
|
# "<level> <proto> <port>" 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 <proto> <port> in: full, some (from some addresses) or ""
|
|
zone_allows() { # <zone> <proto> <port>
|
|
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 <proto> <port> in from the network of <kind> (lan,
|
|
# tailscale or containers): full, some or ""
|
|
firewall_allows() { # <proto> <port> <kind>
|
|
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 "<proto> <port> <project>" 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 <proto> <port>, comma-separated
|
|
conf_owners() { # <proto> <port>
|
|
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 <unit>", "user <unit>",
|
|
# "app <name>" or "none <name>"
|
|
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() { # <port>
|
|
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() { # <proto> <port> <kinds> <owner>
|
|
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() { # <proto> <port> <owner>
|
|
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() { # <scope> <arguments>
|
|
if [ "$1" = user ]; then systemctl --user "${@:2}"; else sudo systemctl "${@:2}"; fi
|
|
}
|
|
|
|
is_active() { # <scope> <unit>
|
|
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() { # <scope> <unit>
|
|
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() { # <unit>
|
|
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
|
|
}
|