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:
@@ -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
|
||||||
|
|||||||
@@ -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}" \
|
||||||
|
|||||||
@@ -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()}")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user