diff --git a/install.sh b/install.sh index 4d0bd5c..5631f84 100755 --- a/install.sh +++ b/install.sh @@ -1,7 +1,7 @@ #!/bin/bash set -Eeuo pipefail -INSTALL_VERSION=4 +INSTALL_VERSION=5 ACTION=install case "${1:-}" in '') ;; @@ -174,7 +174,7 @@ deploy_files() { local php_version log "Deploying Pivilion files" install -d -m 0755 /usr/local/lib/pivilion /usr/local/bin /etc/systemd/system /etc/tor/torrc.d /etc/profile.d /var/lib/pivilion - install -m 0644 "$SOURCE_DIR/usr/local/lib/pivilion/common.sh" /usr/local/lib/pivilion/common.sh + install -m 0644 "$SOURCE_DIR/usr/local/lib/pivilion/common.sh" "$SOURCE_DIR/usr/local/lib/pivilion/hostname.sh" /usr/local/lib/pivilion/ install -m 0755 "$SOURCE_DIR/usr/local/lib/pivilion/mode.sh" "$SOURCE_DIR/usr/local/lib/pivilion/config-sync.sh" "$SOURCE_DIR/usr/local/lib/pivilion/diagnose.sh" \ "$SOURCE_DIR/usr/local/lib/pivilion/issue-banner.sh" /usr/local/lib/pivilion/ install -m 0755 "$SOURCE_DIR"/usr/local/bin/* /usr/local/bin/ @@ -319,7 +319,7 @@ check_install() { local failed=0 file for file in /etc/pivilion.defaults /var/lib/pivilion/install-state /etc/issue.d/90-pivilion.issue \ /usr/local/lib/pivilion/common.sh /usr/local/lib/pivilion/mode.sh /usr/local/lib/pivilion/diagnose.sh \ - /usr/local/lib/pivilion/config-sync.sh \ + /usr/local/lib/pivilion/config-sync.sh /usr/local/lib/pivilion/hostname.sh \ /usr/local/lib/pivilion/issue-banner.sh /etc/systemd/system/pivilion-mode.service \ /etc/systemd/system/pivilion-config.service /etc/systemd/system/pivilion-login-banner.service; do if [ ! -e "$file" ]; then echo "Missing: $file" >&2; failed=1; fi diff --git a/tests/run.sh b/tests/run.sh index e0409c7..d3bcaac 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -62,6 +62,8 @@ boot_config_contract() { grep -q 'pivilion_boot_config_path' "$ROOT/usr/local/lib/pivilion/config-sync.sh" && grep -q 'pivilion.conf.last-good' "$ROOT/usr/local/lib/pivilion/config-sync.sh" && grep -q 'derive_psk' "$ROOT/usr/local/lib/pivilion/config-sync.sh" && + grep -q 'pivilion_apply_hostname' "$ROOT/usr/local/lib/pivilion/config-sync.sh" && + ! grep -R -q --include='*.sh' --include='*.service' 'hostnamectl' "$ROOT/install.sh" "$ROOT/usr" "$ROOT/etc" && grep -q 'Before=.*NetworkManager' "$ROOT/etc/systemd/system/pivilion-config.service" && grep -q 'config-sync.sh set PIVILION_MODE' "$ROOT/usr/local/bin/hotspot" && grep -q 'config-sync.sh set-wifi' "$ROOT/usr/local/bin/pikey" @@ -83,6 +85,7 @@ shellcheck_scripts() { } run "configuration and state fixtures" "$ROOT/tests/test-common.sh" +run "early-boot hostname application" "$ROOT/tests/test-hostname.sh" run "shell syntax" shell_syntax run "PHP syntax" php_syntax run "systemd unit syntax" systemd_syntax diff --git a/tests/test-hostname.sh b/tests/test-hostname.sh new file mode 100755 index 0000000..72397cc --- /dev/null +++ b/tests/test-hostname.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -euo pipefail + +ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +export PIVILION_HOSTNAME_FILE="$TMP/hostname" +export PIVILION_HOSTS_FILE="$TMP/hosts" +export PIVILION_HOSTNAME_COMMAND="$TMP/hostname-command" +export MOCK_HOSTNAME_STATE="$TMP/kernel-hostname" +export MOCK_HOSTNAME_CALLS="$TMP/hostname-calls" + +cat > "$PIVILION_HOSTS_FILE" <<'EOF' +127.0.0.1 localhost +127.0.1.1 old-name old-alias +192.0.2.1 example.test +127.0.1.1 stale-duplicate +EOF +printf 'old-name\n' > "$PIVILION_HOSTNAME_FILE" +printf 'old-name\n' > "$MOCK_HOSTNAME_STATE" + +cat > "$PIVILION_HOSTNAME_COMMAND" <<'EOF' +#!/bin/sh +if [ "${1:-}" = -s ]; then cat "$MOCK_HOSTNAME_STATE"; exit 0; fi +printf '%s\n' "$1" > "$MOCK_HOSTNAME_STATE" +printf '%s\n' "$1" >> "$MOCK_HOSTNAME_CALLS" +EOF +chmod +x "$PIVILION_HOSTNAME_COMMAND" + +source "$ROOT/usr/local/lib/pivilion/hostname.sh" + +for mode in hotspot onion; do + name="pivilion-$mode" + pivilion_apply_hostname "$name" + [ "$(cat "$PIVILION_HOSTNAME_FILE")" = "$name" ] + [ "$(awk '$1 == "127.0.1.1" { print $2 }' "$PIVILION_HOSTS_FILE")" = "$name" ] + [ "$(awk '$1 == "127.0.1.1" { count++ } END { print count+0 }' "$PIVILION_HOSTS_FILE")" -eq 1 ] + [ "$("$PIVILION_HOSTNAME_COMMAND" -s)" = "$name" ] + + before=$(sha256sum "$PIVILION_HOSTNAME_FILE" "$PIVILION_HOSTS_FILE") + inodes=$(stat -c '%i' "$PIVILION_HOSTNAME_FILE" "$PIVILION_HOSTS_FILE") + calls=$(wc -l < "$MOCK_HOSTNAME_CALLS") + pivilion_apply_hostname "$name" + [ "$before" = "$(sha256sum "$PIVILION_HOSTNAME_FILE" "$PIVILION_HOSTS_FILE")" ] + [ "$inodes" = "$(stat -c '%i' "$PIVILION_HOSTNAME_FILE" "$PIVILION_HOSTS_FILE")" ] + [ "$calls" -eq "$(wc -l < "$MOCK_HOSTNAME_CALLS")" ] +done + +grep -Fqx '192.0.2.1 example.test' "$PIVILION_HOSTS_FILE" + +PIVILION_HOSTNAME_FILE="$TMP/missing/hostname" +export PIVILION_HOSTNAME_FILE +if pivilion_apply_hostname broken >"$TMP/failure.out" 2>&1; then exit 1; fi +grep -q 'Could not create a temporary file' "$TMP/failure.out" diff --git a/usr/local/lib/pivilion/config-sync.sh b/usr/local/lib/pivilion/config-sync.sh index 0487043..fabf56d 100644 --- a/usr/local/lib/pivilion/config-sync.sh +++ b/usr/local/lib/pivilion/config-sync.sh @@ -2,6 +2,7 @@ set -Eeuo pipefail source "${PIVILION_COMMON_PATH:-/usr/local/lib/pivilion/common.sh}" +source "${PIVILION_HOSTNAME_LIB_PATH:-$(dirname -- "${BASH_SOURCE[0]}")/hostname.sh}" pivilion_require_root BOOT_ROOT=$(pivilion_boot_root) @@ -189,7 +190,10 @@ migrate_paths() { apply_runtime() { local php_version post_mb escaped_ssid migrate_paths - if [ "$(hostname -s)" != "$PIVILION_HOSTNAME" ] && command -v hostnamectl >/dev/null 2>&1; then hostnamectl set-hostname "$PIVILION_HOSTNAME"; fi + if ! pivilion_apply_hostname "$PIVILION_HOSTNAME"; then + fail_boot "Failed to apply hostname '$PIVILION_HOSTNAME' without D-Bus" + return "$PIVILION_E_CONFIG" + fi command -v iw >/dev/null 2>&1 && iw reg set "$PIVILION_WIFI_COUNTRY" 2>/dev/null || true if [ -n "$PIVILION_WIFI_SSID" ] && [ -n "$PIVILION_WIFI_PSK" ]; then if command -v nmcli >/dev/null 2>&1 && { systemctl is-enabled --quiet NetworkManager 2>/dev/null || systemctl is-active --quiet NetworkManager 2>/dev/null; }; then diff --git a/usr/local/lib/pivilion/hostname.sh b/usr/local/lib/pivilion/hostname.sh new file mode 100644 index 0000000..cda681f --- /dev/null +++ b/usr/local/lib/pivilion/hostname.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Apply a hostname without systemd-hostnamed. This file is also sourced by the +# test suite, so paths and the hostname command may be redirected there. +pivilion_apply_hostname() { + local hostname=$1 + local hostname_file=${PIVILION_HOSTNAME_FILE:-/etc/hostname} + local hosts_file=${PIVILION_HOSTS_FILE:-/etc/hosts} + local hostname_command=${PIVILION_HOSTNAME_COMMAND:-/bin/hostname} + local tmp current + + tmp=$(mktemp "$(dirname "$hostname_file")/.pivilion-hostname.XXXXXX") || { + echo "Could not create a temporary file for $hostname_file" >&2 + return 1 + } + printf '%s\n' "$hostname" > "$tmp" && chmod 0644 "$tmp" || { + rm -f "$tmp" + echo "Could not prepare $hostname_file for hostname '$hostname'" >&2 + return 1 + } + if [ -r "$hostname_file" ] && cmp -s "$tmp" "$hostname_file"; then + rm -f "$tmp" + elif ! mv -f "$tmp" "$hostname_file"; then + rm -f "$tmp" + echo "Could not atomically write hostname '$hostname' to $hostname_file" >&2 + return 1 + fi + + tmp=$(mktemp "$(dirname "$hosts_file")/.pivilion-hosts.XXXXXX") || { + echo "Could not create a temporary file for $hosts_file" >&2 + return 1 + } + awk -v hostname="$hostname" ' + $1 == "127.0.1.1" { if (!written++) print "127.0.1.1\t" hostname; next } + { print } + END { if (!written) print "127.0.1.1\t" hostname } + ' "$hosts_file" > "$tmp" 2>/dev/null || { + rm -f "$tmp" + echo "Could not update the 127.0.1.1 entry in $hosts_file" >&2 + return 1 + } + chmod 0644 "$tmp" || { + rm -f "$tmp" + echo "Could not prepare the updated $hosts_file" >&2 + return 1 + } + if [ -r "$hosts_file" ] && cmp -s "$tmp" "$hosts_file"; then + rm -f "$tmp" + elif ! mv -f "$tmp" "$hosts_file"; then + rm -f "$tmp" + echo "Could not atomically update the 127.0.1.1 entry in $hosts_file" >&2 + return 1 + fi + + [ -x "$hostname_command" ] || { + echo "Cannot set the running hostname: $hostname_command is not executable" >&2 + return 1 + } + current=$($hostname_command -s 2>/dev/null || true) + if [ "$current" != "$hostname" ] && ! "$hostname_command" "$hostname"; then + echo "Could not set the running kernel hostname to '$hostname' with $hostname_command" >&2 + return 1 + fi +}