feat: Reliable hosts file management

This commit is contained in:
ljm42
2025-12-01 17:36:25 -07:00
parent 606db963f2
commit ddd823ea15
3 changed files with 188 additions and 9 deletions

View File

@@ -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

View File

@@ -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=$(</etc/HOSTNAME)
# run & log functions
@@ -53,17 +53,21 @@ disable(){
# when starting avahidaemon, add name.local to the hosts file
add_local_to_hosts(){
local OLD="^127\.0\.0\.1.*"
local NEW="127.0.0.1 $NAME $NAME.local localhost"
sed -i "s/$OLD/$NEW/gm;t" $HOSTS
tmp=$(mktemp "${HOSTS}.XXXXXX")
{
echo "# Do not edit, generated by rc.avahidaemon"
echo "127.0.0.1 ${NAME}.local"
} > "$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
}

170
sbin/rebuild_hosts Normal file
View File

@@ -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"