#!/bin/bash # Function to check if a package is installed is_installed() { pacman -Qi "$1" &> /dev/null } # Function to check if a package is installed is_group_installed() { pacman -Qg "$1" &> /dev/null } # Function to install packages if not already installed install_packages() { local packages=("$@") local to_install=() for pkg in "${packages[@]}"; do if ! is_installed "$pkg" && ! is_group_installed "$pkg"; then to_install+=("$pkg") fi done if [ ${#to_install[@]} -ne 0 ]; then echo "Installing: ${to_install[*]}" yay -S --noconfirm --answerdiff=None --answeredit=None --answerclean=None "${to_install[@]}" fi } # Function to remove pacman/AUR packages if installed remove_packages() { local packages=("$@") local to_remove=() for pkg in "${packages[@]}"; do if is_installed "$pkg"; then to_remove+=("$pkg") fi done if [ ${#to_remove[@]} -ne 0 ]; then echo "Removing: ${to_remove[*]}" sudo pacman -Rns --noconfirm "${to_remove[@]}" fi } # Function to enable systemd services if not already enabled enable_services() { for service in "$@"; do if ! systemctl is-enabled "$service" &> /dev/null; then echo "Enabling $service..." sudo systemctl enable "$service" else echo "$service is already enabled." fi done } # Output of the last update's pacman -Su, written by updates.d/10-pacman.sh PACMAN_UPDATE_LOG="$HOME/.local/state/miniserver/pacman-update.log" # True if the last update rebuilt the initramfs and mkinitcpio reported a # failure. Rebooting then can leave the machine unbootable. initramfs_build_failed() { [ -f "$PACMAN_UPDATE_LOG" ] || return 1 grep -q "Updating linux initcpios" "$PACMAN_UPDATE_LOG" || return 1 grep -q "==> ERROR" "$PACMAN_UPDATE_LOG" || ! grep -q "Image generation successful" "$PACMAN_UPDATE_LOG" } # Install a script atomically (copy to a temp file, then rename over the target) # so a copy of it that is running right now keeps reading the old file install_script() { local tmp 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. setup_cockpit() { if ! systemctl is-enabled --quiet cockpit.socket || ! systemctl is-active --quiet cockpit.socket; then echo "Enabling Cockpit..." sudo systemctl enable --now cockpit.socket fi } # Print the rules a firewall.conf asks for, one per line, as service: or # port:/. Sourced in a subshell so the file can't change # anything else. firewall_entries() { ( FIREWALL_SERVICES=() FIREWALL_PORTS=() source "$1" > /dev/null || exit 1 for e in "${FIREWALL_SERVICES[@]}"; do echo "service:$e"; done for e in "${FIREWALL_PORTS[@]}"; do echo "port:$e"; done ) } # Rules setup_firewall opened, so it knows which ones it may close again FIREWALL_STATE="$HOME/.local/state/miniserver/firewall-opened" # Allow what MiniServer's and each sibling project's firewall.conf ask for # through firewalld's default zone, and make sure firewalld is running. # Closes a rule only if setup_firewall opened it and no firewall.conf asks for # it any more (e.g. after `sibling remove`); rules that were already there or # were added by hand are left alone. Safe to re-run. setup_firewall() { command -v firewall-offline-cmd &> /dev/null || return 0 # firewall-cmd only works while firewalld is running; before that, write the # permanent config directly so SSH is allowed before the firewall comes up local fw running=0 changed=0 complete=1 status=0 if systemctl is-active --quiet firewalld; then running=1 fw=(sudo firewall-cmd --permanent) else fw=(sudo firewall-offline-cmd) fi local wanted=() opened=() kept=() conf out entry kind value port for conf in "$HOME/.local/share/MiniServer/firewall.conf" "$HOME/.local/share/siblings/"*/firewall.conf; do [ -f "$conf" ] || continue if out="$(firewall_entries "$conf")"; then [ -n "$out" ] && mapfile -t -O "${#wanted[@]}" wanted <<< "$out" else echo "Firewall: could not read $conf, so no rules will be closed this time." complete=0 status=1 fi done # Keep SSH reachable if sshd listens on a port other than 22. If its config # can't be read, don't close anything: that port could be the SSH port. if command -v sshd &> /dev/null; then if out="$(sudo sshd -T 2> /dev/null)"; then for port in $(awk '$1 == "port" && $2 != 22 { print $2 }' <<< "$out"); do wanted+=("port:$port/tcp") done else complete=0 fi fi [ -f "$FIREWALL_STATE" ] && mapfile -t opened < "$FIREWALL_STATE" for entry in "${wanted[@]}"; do kind="${entry%%:*}" value="${entry#*:}" if ! { [[ "$kind" == service && "$value" =~ ^[A-Za-z0-9_.-]+$ ]] || [[ "$kind" == port && "$value" =~ ^[0-9]+(-[0-9]+)?/(tcp|udp|sctp|dccp)$ ]]; }; then echo "Firewall: ignoring invalid $kind '$value'" status=1 continue fi if ! "${fw[@]}" --query-"$kind"="$value" &> /dev/null; then echo "Firewall: allowing $value" if "${fw[@]}" --add-"$kind"="$value"; then opened+=("$entry") changed=1 else status=1 fi fi done for entry in "${opened[@]}"; do [ -n "$entry" ] || continue if (( ! complete )) || printf '%s\n' "${wanted[@]}" | grep -qxF -- "$entry"; then kept+=("$entry") continue fi kind="${entry%%:*}" value="${entry#*:}" if "${fw[@]}" --query-"$kind"="$value" &> /dev/null; then echo "Firewall: closing $value (nothing asks for it any more)" if "${fw[@]}" --remove-"$kind"="$value"; then changed=1 else kept+=("$entry") status=1 fi fi done mkdir -p "$(dirname "$FIREWALL_STATE")" printf '%s\n' "${kept[@]}" | sed '/^$/d' | sort -u > "$FIREWALL_STATE" if (( running )); then if (( changed )); then sudo firewall-cmd --reload || status=1 fi else echo "Starting firewalld..." sudo systemctl enable --now firewalld || status=1 # Docker sets up its firewalld zone only when it starts if systemctl is-active --quiet docker; then echo "Docker started before firewalld. Restart it (sudo systemctl restart docker) or reboot so its containers' ports work through the firewall." fi fi return $status }