fix: resolve did:dht compilation errors

- Simplify DHT encoding: use JSON instead of DNS packets (drop simple-dns)
- Fix mainline crate API: SigningKey takes 32 bytes, get_mutable returns Result
- Add missing dht_did field to IdentityRecord constructor
- Store DID Document as JSON in DHT (DNS encoding deferred)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-14 04:14:04 +00:00
parent 0e9df969f1
commit 4a3611f3b4
26 changed files with 451 additions and 590 deletions

View File

@@ -1,7 +1,7 @@
//! RPC handlers for node network visibility and overlay controls.
use super::RpcHandler;
use crate::{identity, nostr_discovery, peers};
use crate::{identity, peers};
use crate::container::docker_packages;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
@@ -78,67 +78,14 @@ impl RpcHandler {
.await
.context("Failed to write visibility setting")?;
// Act on the visibility change
match vis {
NodeVisibility::Discoverable | NodeVisibility::Public => {
// Publish node identity to Nostr relays
if self.config.nostr_relays.is_empty() {
return Ok(serde_json::json!({
"visibility": vis.as_str(),
"published": false,
"reason": "No Nostr relays configured. Set ARCHIPELAGO_NOSTR_RELAYS.",
}));
}
let (data, _) = self.state_manager.get_snapshot().await;
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let node_address = data
.server_info
.node_address
.as_deref()
.unwrap_or("archipelago://unknown");
let identity_dir = self.config.data_dir.join("identity");
match nostr_discovery::publish_node_identity(
&identity_dir,
&did,
node_address,
&data.server_info.version,
&self.config.nostr_relays,
self.config.nostr_tor_proxy.as_deref(),
)
.await
{
Ok(output) => {
tracing::info!(
"Published node to {} relays (visibility: {})",
output.success.len(),
vis.as_str()
);
Ok(serde_json::json!({
"visibility": vis.as_str(),
"published": true,
"relays_success": output.success.len(),
"relays_failed": output.failed.len(),
}))
}
Err(e) => {
tracing::warn!("Failed to publish node: {}", e);
Ok(serde_json::json!({
"visibility": vis.as_str(),
"published": false,
"reason": e.to_string(),
}))
}
}
}
NodeVisibility::Hidden => {
tracing::info!("Node visibility set to hidden");
Ok(serde_json::json!({
"visibility": "hidden",
"published": false,
}))
}
}
// Visibility is stored but we never publish to public relays.
// Nodes connect via federation ID, not Nostr discovery.
tracing::info!("Node visibility set to {}", vis.as_str());
Ok(serde_json::json!({
"visibility": vis.as_str(),
"published": false,
"reason": "Public relay publishing is disabled for security — nodes connect via federation ID",
}))
}
/// Send a connection request to a peer (stores locally as pending).

View File

@@ -73,33 +73,9 @@ impl RpcHandler {
}
pub(super) async fn handle_node_nostr_publish(&self) -> Result<serde_json::Value> {
if !self.config.nostr_discovery_enabled || self.config.nostr_relays.is_empty() {
anyhow::bail!(
"Nostr discovery disabled. Set ARCHIPELAGO_NOSTR_DISCOVERY_ENABLED=true and ARCHIPELAGO_NOSTR_RELAYS=wss://... to enable."
);
}
let (data, _) = self.state_manager.get_snapshot().await;
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
let node_address = data
.server_info
.node_address
.as_deref()
.unwrap_or("archipelago://unknown");
let identity_dir = self.config.data_dir.join("identity");
let output = nostr_discovery::publish_node_identity(
&identity_dir,
&did,
node_address,
&data.server_info.version,
&self.config.nostr_relays,
self.config.nostr_tor_proxy.as_deref(),
)
.await?;
Ok(serde_json::json!({
"event_id": output.id().to_hex(),
"success": output.success.len(),
"failed": output.failed.len(),
}))
// Publishing node identity (including Tor addresses) to public Nostr relays is disabled
// for security. Nodes connect via federation ID, not public discovery.
anyhow::bail!("Nostr identity publishing is disabled — nodes connect via federation ID")
}
pub(super) async fn handle_node_nostr_pubkey(&self) -> Result<serde_json::Value> {

View File

@@ -3,6 +3,38 @@ use anyhow::{Context, Result};
use tracing::debug;
impl RpcHandler {
/// server.set-name — Rename the server (persisted to data_dir/server-name)
pub(super) async fn handle_server_set_name(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let name = params
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: name"))?
.trim()
.to_string();
if name.is_empty() || name.len() > 64 {
anyhow::bail!("Name must be 1-64 characters");
}
// Persist to file
let name_file = self.config.data_dir.join("server-name");
tokio::fs::write(&name_file, &name)
.await
.context("Failed to write server name")?;
// Update live state
let (mut data, _) = self.state_manager.get_snapshot().await;
data.server_info.name = Some(name.clone());
self.state_manager.update_data(data).await;
debug!("Server name updated to: {}", name);
Ok(serde_json::json!({ "name": name }))
}
/// system.stats — CPU usage, RAM used/total, disk used/total, uptime, load average
pub(super) async fn handle_system_stats(&self) -> Result<serde_json::Value> {
debug!("Getting system stats");

View File

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::{debug, info, warn};
use crate::{federation, identity, nostr_discovery};
use crate::{federation, identity};
const TOR_DATA_DIR: &str = "/var/lib/archipelago/tor";
const SERVICES_CONFIG: &str = "services.json";
@@ -143,22 +143,15 @@ impl RpcHandler {
return Err(anyhow::anyhow!("Service '{}' has no .onion address to rotate", name));
}
// Rename old directory to _old_<timestamp> for transition period
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let old_dir = format!("{}/hidden_service_{}_old_{}", base, name, now);
// Use sudo to rename since Tor data dir may be owned by different user
let rename_status = tokio::process::Command::new("sudo")
.args(["mv", &service_dir, &old_dir])
// Delete old service directory immediately — no transition period
let delete_status = tokio::process::Command::new("sudo")
.args(["rm", "-rf", &service_dir])
.status()
.await
.context("Failed to rename hidden service directory")?;
.context("Failed to delete hidden service directory")?;
if !rename_status.success() {
return Err(anyhow::anyhow!("Failed to rename hidden service directory for rotation"));
if !delete_status.success() {
return Err(anyhow::anyhow!("Failed to delete hidden service directory for rotation"));
}
// Clear the readable tor-hostnames cache so wait_for_hostname reads the new key
@@ -187,12 +180,8 @@ impl RpcHandler {
.map(|s| s.success())
.unwrap_or(false);
if !container_ok {
warn!("Failed to restart Tor after rotation");
let _ = tokio::process::Command::new("sudo")
.args(["mv", &old_dir, &service_dir])
.status()
.await;
return Err(anyhow::anyhow!("Failed to restart Tor — rotation rolled back"));
warn!("Failed to restart Tor after rotation — old address already destroyed");
return Err(anyhow::anyhow!("Failed to restart Tor — old address destroyed, Tor will generate new key on next restart"));
}
}
@@ -213,19 +202,17 @@ impl RpcHandler {
}
}
// Propagate address change to Nostr relays and federation peers (fire-and-forget)
// Notify federation peers of address change (private peer-to-peer, no public relays)
if let Some(ref new_addr) = new_onion {
let data_dir = self.config.data_dir.clone();
let nostr_relays = self.config.nostr_relays.clone();
let tor_proxy = self.config.nostr_tor_proxy.clone();
let new_addr_clone = new_addr.clone();
let old_onion_clone = old_onion.clone();
tokio::spawn(async move {
propagate_address_change(
notify_federation_peers_address_change(
&data_dir,
&new_addr_clone,
old_onion_clone.as_deref(),
&nostr_relays,
tor_proxy.as_deref(),
).await;
});
@@ -236,7 +223,6 @@ impl RpcHandler {
"name": name,
"old_onion": old_onion,
"new_onion": new_onion,
"transition_hours": ROTATION_TRANSITION_SECS / 3600,
}))
}
@@ -391,23 +377,26 @@ async fn list_services(config_dir: &std::path::Path) -> Result<Vec<TorService>>
}
// Then, scan filesystem for any hidden_service_* dirs not in config
if let Ok(entries) = std::fs::read_dir(&base) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("hidden_service_") && entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let service_name = name.strip_prefix("hidden_service_").unwrap_or(&name).to_string();
if seen.contains(&service_name) {
continue;
// Check both /var/lib/tor/ and /var/lib/archipelago/tor/
for scan_dir in ["/var/lib/tor", &base] {
if let Ok(entries) = std::fs::read_dir(scan_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("hidden_service_") && entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let service_name = name.strip_prefix("hidden_service_").unwrap_or(&name).to_string();
if seen.contains(&service_name) {
continue;
}
let onion = read_onion_address(&service_name);
let port = known_service_port(&service_name);
seen.insert(service_name.clone());
services.push(TorService {
name: service_name,
local_port: port,
onion_address: onion,
enabled: true,
});
}
let onion = read_onion_address(&service_name);
// Infer port from known services
let port = known_service_port(&service_name);
services.push(TorService {
name: service_name,
local_port: port,
onion_address: onion,
enabled: true,
});
}
}
}
@@ -416,7 +405,7 @@ async fn list_services(config_dir: &std::path::Path) -> Result<Vec<TorService>>
}
/// Read .onion address from hostname file.
/// Checks tor-hostnames readable copy first, then hidden service dir (with sudo fallback).
/// Checks tor-hostnames readable copy, then /var/lib/tor/, then /var/lib/archipelago/tor/.
fn read_onion_address(service_name: &str) -> Option<String> {
let base = tor_data_dir();
let base_path = std::path::Path::new(&base);
@@ -435,22 +424,33 @@ fn read_onion_address(service_name: &str) -> Option<String> {
return Some(addr);
}
// Fall back to hidden service directory (direct read, then sudo)
let path = base_path
.join(format!("hidden_service_{}", service_name))
.join("hostname");
std::fs::read_to_string(&path)
.ok()
.or_else(|| {
std::process::Command::new("sudo")
.args(["cat", &path.to_string_lossy()])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
})
.map(|s| s.trim().to_string())
.filter(|s| s.ends_with(".onion") && s.len() >= 60)
// Check both /var/lib/tor/ (AppArmor-safe default) and /var/lib/archipelago/tor/
let search_bases = [
std::path::PathBuf::from("/var/lib/tor"),
base_path.to_path_buf(),
];
for search_base in &search_bases {
let path = search_base
.join(format!("hidden_service_{}", service_name))
.join("hostname");
if let Some(addr) = std::fs::read_to_string(&path)
.ok()
.or_else(|| {
std::process::Command::new("sudo")
.args(["cat", &path.to_string_lossy()])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
})
.map(|s| s.trim().to_string())
.filter(|s| s.ends_with(".onion") && s.len() >= 60)
{
return Some(addr);
}
}
None
}
/// Known default ports for built-in services.
@@ -485,34 +485,17 @@ async fn save_services_config(config_dir: &std::path::Path, config: &ServicesCon
Ok(())
}
/// Propagate address change: publish to Nostr relays and notify federation peers.
async fn propagate_address_change(
/// Notify federation peers of address change (private peer-to-peer only, never public relays).
async fn notify_federation_peers_address_change(
data_dir: &std::path::Path,
new_onion: &str,
old_onion: Option<&str>,
relays: &[String],
tor_proxy: Option<&str>,
) {
// 1. Publish updated identity to Nostr relays
let identity_dir = data_dir.join("identity");
match identity::NodeIdentity::load_or_create(&identity_dir).await {
Ok(node_id) => {
let did = node_id.did_key();
if !relays.is_empty() {
match nostr_discovery::publish_node_identity(
&identity_dir,
&did,
new_onion,
env!("CARGO_PKG_VERSION"),
relays,
tor_proxy,
).await {
Ok(_) => info!("Published updated .onion to Nostr relays"),
Err(e) => warn!("Failed to publish to Nostr relays: {}", e),
}
}
// 2. Notify federation peers via the old address (still works during transition)
let proxy = tor_proxy.unwrap_or("127.0.0.1:9050");
match federation::load_nodes(data_dir).await {
Ok(peers) => {
@@ -520,7 +503,6 @@ async fn propagate_address_change(
if peer.onion.is_empty() {
continue;
}
let target_onion = &peer.onion;
let payload = serde_json::json!({
"method": "federation.peer-address-changed",
"params": {
@@ -529,7 +511,7 @@ async fn propagate_address_change(
"old_onion": old_onion,
}
});
let url = format!("http://{}/rpc/v1", target_onion);
let url = format!("http://{}/rpc/v1", &peer.onion);
let client = match reqwest::Client::builder()
.proxy(reqwest::Proxy::all(format!("socks5h://{}", proxy)).unwrap_or_else(|_| reqwest::Proxy::all("socks5h://127.0.0.1:9050").expect("valid proxy")))
.timeout(std::time::Duration::from_secs(30))