fix(image): keep the Tor onion reachable on RTC-less boards

An RTC-less Pi boots at 1970; Tor then rejects the consensus and the unlock
onion never publishes. Bake a clock floor (build epoch) into the initramfs and
jump the clock forward to it before Tor starts, and pass a numeric tor_ntp= in
the cmdline so busybox ntpd syncs without DNS. Proven on hardware: the onion
unlock succeeded on a Pi 3 after this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-Birkenbach
2026-07-24 17:55:01 +02:00
parent d65bf17cd3
commit 690ca79812
4 changed files with 23 additions and 8 deletions

View File

@@ -25,6 +25,8 @@ for bb in /bin/busybox /usr/bin/busybox; do
[ -x "$bb" ] && { copy_exec "$bb" /usr/local/bin/busybox; break; } [ -x "$bb" ] && { copy_exec "$bb" /usr/local/bin/busybox; break; }
done done
date -u +%s > "${DESTDIR}/etc/tor-clock-floor"
# Onion keys + torrc, staged on the system by lim. # Onion keys + torrc, staged on the system by lim.
mkdir -p "${DESTDIR}/etc/tor/onion" mkdir -p "${DESTDIR}/etc/tor/onion"
for key in hostname hs_ed25519_public_key hs_ed25519_secret_key; do for key in hostname hs_ed25519_public_key hs_ed25519_secret_key; do

View File

@@ -34,8 +34,12 @@ if [ ! -s /etc/resolv.conf ]; then
done done
fi fi
# RTC-less boards boot at 1970; Tor rejects the consensus otherwise. Bounded so floor=$(cat /etc/tor-clock-floor 2>/dev/null || echo 0)
# a failed sync never blocks boot. if [ "$(date +%s)" -lt "$floor" ]; then
/usr/local/bin/busybox date -s "@$floor" >/dev/null 2>&1 \
|| date -s "@$floor" >/dev/null 2>&1 || true
fi
if [ -x /usr/local/bin/busybox ]; then if [ -x /usr/local/bin/busybox ]; then
/usr/local/bin/busybox timeout 30 \ /usr/local/bin/busybox timeout 30 \
/usr/local/bin/busybox ntpd -n -q -p "${tor_ntp:-pool.ntp.org}" \ /usr/local/bin/busybox ntpd -n -q -p "${tor_ntp:-pool.ntp.org}" \

View File

@@ -20,6 +20,9 @@ from lim.image.initramfs.base import InitramfsBackend
from lim.image.plan import ImagePlan from lim.image.plan import ImagePlan
from lim.image.session import ImageSession, chroot_bash, install_packages from lim.image.session import ImageSession, chroot_bash, install_packages
# Numeric IP, not a hostname: the initramfs has no DNS resolver.
_DEFAULT_NTP = "162.159.200.123"
class InitramfsToolsBackend(InitramfsBackend): class InitramfsToolsBackend(InitramfsBackend):
def luks_package_collection(self) -> str: def luks_package_collection(self) -> str:
@@ -91,12 +94,7 @@ class InitramfsToolsBackend(InitramfsBackend):
"update-initramfs -c -k all 2>/dev/null || update-initramfs -u -k all", "update-initramfs -c -k all 2>/dev/null || update-initramfs -u -k all",
) )
def configure_bootloader( def configure_bootloader(self, plan: ImagePlan, session: ImageSession, root: Path) -> None:
self,
plan: ImagePlan, # noqa: ARG002
session: ImageSession,
root: Path,
) -> None:
hostname = (root / "etc/hostname").read_text().strip() hostname = (root / "etc/hostname").read_text().strip()
boot = session.boot_mount_path boot = session.boot_mount_path
@@ -113,5 +111,7 @@ class InitramfsToolsBackend(InitramfsBackend):
text = f"{text} ip=::::{hostname}:eth0:dhcp" text = f"{text} ip=::::{hostname}:eth0:dhcp"
if "net.ifnames=0" not in text: if "net.ifnames=0" not in text:
text = f"{text} net.ifnames=0" text = f"{text} net.ifnames=0"
if plan.tor_unlock and "tor_ntp=" not in text:
text = f"{text} tor_ntp={_DEFAULT_NTP}"
cmdline_txt.write_text(text + "\n") cmdline_txt.write_text(text + "\n")
ui.info(f"Content of {cmdline_txt}:{cmdline_txt.read_text()}") ui.info(f"Content of {cmdline_txt}:{cmdline_txt.read_text()}")

View File

@@ -174,6 +174,7 @@ class TestInitramfsToolsBackend:
assert "root=/dev/mapper/cryptroot" in cmdline assert "root=/dev/mapper/cryptroot" in cmdline
assert "root=PARTUUID=abcd-02" not in cmdline assert "root=PARTUUID=abcd-02" not in cmdline
assert "ip=::::pi:eth0:dhcp" in cmdline assert "ip=::::pi:eth0:dhcp" in cmdline
assert "tor_ntp=" in cmdline
assert cmdline.count("\n") == 1 # cmdline must stay a single line assert cmdline.count("\n") == 1 # cmdline must stay a single line
assert "auto_initramfs=1" in (session.boot_mount_path / "config.txt").read_text() assert "auto_initramfs=1" in (session.boot_mount_path / "config.txt").read_text()
@@ -209,3 +210,11 @@ class TestInitramfsToolsHookHardening:
premount = self._read("tor_premount") premount = self._read("tor_premount")
assert '. "$conf"' not in premount assert '. "$conf"' not in premount
assert "sed -n 's/^IPV4DNS" in premount assert "sed -n 's/^IPV4DNS" in premount
def test_hook_stamps_a_clock_floor(self):
assert "/etc/tor-clock-floor" in self._read("tor_hook")
def test_premount_raises_clock_to_floor_before_ntp(self):
premount = self._read("tor_premount")
assert "/etc/tor-clock-floor" in premount
assert premount.index("tor-clock-floor") < premount.index("ntpd")