fix(tor): un-alias bitcoin-core's hidden-service name; add regression tests

read_tor_address("bitcoin-core") was resolving through tor_service_name to
the shared "bitcoin" alias, but enrollment (install.rs auto-enroll and the
tor.create-service RPC) always names HiddenServiceDir/tor-hostnames entries
using the raw package_id verbatim — never canonicalized. On a real node
that's hidden_service_bitcoin-core, which the aliased lookup never found,
so the per-app UI Tor badge stayed empty even after the previous commit
made bitcoin-core auto-enrollable.

Give bitcoin-core its own identity-mapped arm instead of folding it into
the legacy bitcoin/bitcoin-knots/bitcoind alias, and pin all three lookup
tables (known_service_port, is_protocol_service, tor_service_name) with
regression tests so this alias-drift class of bug can't recur silently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxfWiFfnBkdSxwKUuV2tNy
This commit is contained in:
2026-09-10 16:26:59 +00:00
co-authored by Claude Sonnet 5
parent 69f3a355c7
commit dc7b598558
2 changed files with 50 additions and 1 deletions
+17
View File
@@ -377,6 +377,23 @@ async fn write_staged_torrc(content: &str, staging: &str) -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(test)]
mod known_service_tests {
use super::{is_protocol_service, known_service_port};
#[test]
fn bitcoin_core_is_a_protocol_service_on_the_p2p_port() {
// Regression: apps/bitcoin-core/manifest.yml uses id "bitcoin-core",
// distinct from the legacy "bitcoin"/"bitcoin-knots" ids. Missing
// here means auto-enrollment silently skips it (known_service_port
// returns 0) and, separately, regenerate_torrc falls back to the
// web-app HiddenServicePort-80 default instead of forwarding 8333
// straight through.
assert_eq!(known_service_port("bitcoin-core"), 8333);
assert!(is_protocol_service("bitcoin-core"));
}
}
#[cfg(test)] #[cfg(test)]
mod torrc_tests { mod torrc_tests {
use super::app_hidden_service_port_line; use super::app_hidden_service_port_line;
@@ -657,10 +657,20 @@ fn apply_dynamic_metadata(app_id: &str, meta: &mut AppMetadata) {
/// Map app_id to Tor hidden service directory name. /// Map app_id to Tor hidden service directory name.
/// "archipelago" is the main web UI (nginx port 80). /// "archipelago" is the main web UI (nginx port 80).
/// Supports container names from deploy (archy-*, btcpay-server, etc.). /// Supports container names from deploy (archy-*, btcpay-server, etc.).
///
/// This must match what enrollment actually names the hidden service dir
/// with — both the install-time auto-enroll (`install.rs`) and the manual
/// `tor.create-service` RPC write `HiddenServiceDir` using the raw
/// `package_id`/`name` verbatim, with no canonicalization. So `bitcoin-core`
/// gets its own identity arm rather than folding into the "bitcoin" alias:
/// aliasing it here without also canonicalizing the write side would point
/// this lookup at `hidden_service_bitcoin`, which never gets created — the
/// on-disk dir is always `hidden_service_bitcoin-core` for this app id.
fn tor_service_name(app_id: &str) -> Option<&'static str> { fn tor_service_name(app_id: &str) -> Option<&'static str> {
match app_id { match app_id {
"archipelago" => Some("archipelago"), "archipelago" => Some("archipelago"),
"bitcoin" | "bitcoin-core" | "bitcoin-knots" | "bitcoind" => Some("bitcoin"), "bitcoin-core" => Some("bitcoin-core"),
"bitcoin" | "bitcoin-knots" | "bitcoind" => Some("bitcoin"),
"electrumx" | "electrs" | "electrum" => Some("electrumx"), "electrumx" | "electrs" | "electrum" => Some("electrumx"),
"lnd" | "lnd-ui" => Some("lnd"), "lnd" | "lnd-ui" => Some("lnd"),
"btcpay" | "btcpay-server" | "btcpayserver" => Some("btcpay"), "btcpay" | "btcpay-server" | "btcpayserver" => Some("btcpay"),
@@ -906,6 +916,28 @@ mod launch_url_port_tests {
} }
} }
#[cfg(test)]
mod tor_service_name_tests {
use super::tor_service_name;
#[test]
fn bitcoin_core_resolves_to_its_own_hidden_service_dir() {
// Regression: enrollment (install.rs, tor.create-service) writes
// HiddenServiceDir/tor-hostnames entries using the raw package_id
// verbatim, never canonicalized. Aliasing "bitcoin-core" to the
// shared "bitcoin" name here would point reads at a directory
// enrollment never creates.
assert_eq!(tor_service_name("bitcoin-core"), Some("bitcoin-core"));
}
#[test]
fn legacy_bitcoin_ids_share_the_bitcoin_alias() {
assert_eq!(tor_service_name("bitcoin"), Some("bitcoin"));
assert_eq!(tor_service_name("bitcoin-knots"), Some("bitcoin"));
assert_eq!(tor_service_name("bitcoind"), Some("bitcoin"));
}
}
#[cfg(test)] #[cfg(test)]
mod extract_lan_address_tests { mod extract_lan_address_tests {
use super::extract_lan_address; use super::extract_lan_address;