From ddd823ea1571696defe80600fd2f22a834fccdf3 Mon Sep 17 00:00:00 2001 From: ljm42 Date: Mon, 1 Dec 2025 17:36:25 -0700 Subject: [PATCH] feat: Reliable hosts file management --- etc/rc.d/rc.S.cont | 9 ++- etc/rc.d/rc.avahidaemon | 18 +++-- sbin/rebuild_hosts | 170 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 sbin/rebuild_hosts diff --git a/etc/rc.d/rc.S.cont b/etc/rc.d/rc.S.cont index 2e00799cb..0c550e58e 100755 --- a/etc/rc.d/rc.S.cont +++ b/etc/rc.d/rc.S.cont @@ -226,8 +226,13 @@ if [[ -r /boot/config/ident.cfg ]]; then NAME=${NAME//[^a-zA-Z\-\.0-9]/\-} fi /bin/echo "$NAME" >/etc/HOSTNAME -/bin/echo "# Generated" >/etc/hosts -/bin/echo "127.0.0.1 $NAME localhost" >>/etc/hosts +/bin/mkdir -p /etc/hosts.d +{ + /bin/echo "# Do not edit, generated by rc.S.cont" + /bin/echo "127.0.0.1 ${NAME} localhost" + /bin/echo "::1 localhost" +} >/etc/hosts.d/00-base +/usr/local/sbin/rebuild_hosts # LimeTech - restore the configured timezone if [[ $timeZone == custom ]]; then diff --git a/etc/rc.d/rc.avahidaemon b/etc/rc.d/rc.avahidaemon index acdd434f1..dfbd1f9ee 100755 --- a/etc/rc.d/rc.avahidaemon +++ b/etc/rc.d/rc.avahidaemon @@ -30,7 +30,7 @@ DAEMON="Avahi mDNS/DNS-SD daemon" CALLER="avahi" AVAHI="/usr/sbin/avahi-daemon" CONF="/etc/avahi/avahi-daemon.conf" -HOSTS="/etc/hosts" +HOSTS="/etc/hosts.d/05-avahi" NAME=$( "$tmp" + chmod 644 "$tmp" + mv "$tmp" "$HOSTS" + /usr/local/sbin/rebuild_hosts return 0 } # when stopping avahidaemon, remove name.local from the hosts file remove_local_from_hosts(){ - local OLD="^127\.0\.0\.1.*" - local NEW="127.0.0.1 $NAME localhost" - sed -i "s/$OLD/$NEW/gm;t" $HOSTS + rm -f "$HOSTS" + /usr/local/sbin/rebuild_hosts return 0 } diff --git a/sbin/rebuild_hosts b/sbin/rebuild_hosts new file mode 100644 index 000000000..d40e4ac67 --- /dev/null +++ b/sbin/rebuild_hosts @@ -0,0 +1,170 @@ +#!/bin/bash +# +# rebuild_hosts: generate /etc/hosts from /etc/hosts.d fragments. +# +# Each file in /etc/hosts.d/ should contain lines in standard hosts(5) format: +# IPADDR hostname [alias...] +# +# This script: +# - Reads all regular files in /etc/hosts.d, in sorted order. +# - Merges entries with the same IP onto a single line. +# - Deduplicates hostnames per IP. +# - Tracks which fragments contributed to each IP and annotates with '# from ...'. +# - Writes the result atomically to /etc/hosts with perms root:root 0644. +# +# Do NOT edit /etc/hosts directly; edit /etc/hosts.d/* instead. + +set -euo pipefail + +# run & log functions +. /etc/rc.d/rc.runlog + +trap 'log "rebuild_hosts FAILED with status $? (see previous log lines for context)" || true' ERR + +HOSTS_DIR=/etc/hosts.d +HOSTS_FILE=/etc/hosts + +# Create temp file in same FS as HOSTS_FILE so mv is atomic +tmpfile=$(mktemp "${HOSTS_FILE}.XXXXXX") + +# Header +{ + echo "# $HOSTS_FILE - automatically generated by rebuild_hosts" + echo "# Do not edit this file directly; use ${HOSTS_DIR}/* instead." + echo "#" + echo "# fragments:" + shopt -s nullglob + for f in "$HOSTS_DIR"/*; do + [ -f "$f" ] || continue + printf "# %s\n" "$(basename "$f")" + done + shopt -u nullglob + echo +} > "$tmpfile" + +# IP -> "host1 host2 ..." +declare -A IP_TO_HOSTS + +# IP -> "fragment1 fragment2 ..." +declare -A IP_TO_SOURCES + +# Preserve IP order (first time we see an IP, record it here) +IP_ORDER=() + +# Name of the fragment currently being processed (basename of file) +CURRENT_SOURCE="" + +add_hosts_line() { + # $1 = IP address, remaining args = hostnames/aliases + local ip="$1"; shift + local host + + # If this IP hasn't been seen yet, initialize it and record order + if [[ -z "${IP_TO_HOSTS[$ip]+x}" ]]; then + IP_TO_HOSTS["$ip"]="" + IP_ORDER+=("$ip") + fi + + # Append any new hostnames for this IP, skipping duplicates. + # We store hostnames as a space-separated string. + local current_hosts="${IP_TO_HOSTS[$ip]}" + + # Iterate over all hostnames passed to this function + for host in "$@"; do + # Skip empty tokens (paranoia / defensive programming) + [[ -z "$host" ]] && continue + + # Check if this hostname is already present for this IP. + # We wrap the string and hostname with spaces so we can do + # a simple substring match without false positives: + # " foo bar " contains " bar " but not " ar ". + case " $current_hosts " in + *" $host "*) ;; # already present, do nothing + *) current_hosts="$current_hosts $host" ;; # append new hostname + esac + done + + # Strip the leading space if we added anything + IP_TO_HOSTS["$ip"]="${current_hosts# }" + + # Track which fragment files contributed to this IP. + # This is just for the trailing "# from 00-base 10-avahi" comment. + if [[ -n "$CURRENT_SOURCE" ]]; then + local current_sources="${IP_TO_SOURCES[$ip]:-}" + + case " $current_sources " in + *" $CURRENT_SOURCE "*) ;; # already recorded + *) current_sources="$current_sources $CURRENT_SOURCE" ;; + esac + + # Again, strip leading space + IP_TO_SOURCES["$ip"]="${current_sources# }" + fi +} + +process_file() { + local file="$1" + local line ip + # Remember which fragment we are processing for source tracking + CURRENT_SOURCE="$(basename "$file")" + + while IFS= read -r line || [[ -n "$line" ]]; do + # Strip comments (anything after '#') + line="${line%%#*}" + + # Trim leading whitespace + line="${line#"${line%%[![:space:]]*}"}" + # Trim trailing whitespace + line="${line%"${line##*[![:space:]]}"}" + + # Skip empty lines + [[ -z "$line" ]] && continue + + # Split line into fields: + # $1 = IP, remaining = hostnames/aliases. + # shellcheck disable=SC2086 + set -- $line + ip="$1"; shift || true + + # If IP is empty or no hostnames, skip the line + [[ -z "$ip" ]] && continue + [[ $# -eq 0 ]] && continue + + add_hosts_line "$ip" "$@" + done < "$file" +} + +log "Rebuilding $HOSTS_FILE..." + +shopt -s nullglob +fragment_files=("$HOSTS_DIR"/*) +shopt -u nullglob + +for f in "${fragment_files[@]}"; do + [[ -f "$f" ]] || continue + process_file "$f" +done + +# If no fragments defined any IPs, provide a minimal fallback +if [[ ${#IP_ORDER[@]} -eq 0 ]]; then + CURRENT_SOURCE="fallback" + add_hosts_line "127.0.0.1" "localhost" +fi + +# Emit merged lines in original IP discovery order +for ip in "${IP_ORDER[@]}"; do + hosts="${IP_TO_HOSTS[$ip]}" + sources="${IP_TO_SOURCES[$ip]:-}" + + if [[ -n "$sources" ]]; then + printf "%-15s %s # from %s\n" "$ip" "$hosts" "$sources" >> "$tmpfile" + else + printf "%-15s %s\n" "$ip" "$hosts" >> "$tmpfile" + fi +done + +# Set perms and atomic replace +chmod 0644 "$tmpfile" 2>/dev/null || true +chown root:root "$tmpfile" 2>/dev/null || true +mv "$tmpfile" "$HOSTS_FILE" +