From 28f13641a3b115199cecb3fb56695dcba37ca5b1 Mon Sep 17 00:00:00 2001 From: randogoth Date: Tue, 29 Sep 2026 12:48:40 +0300 Subject: [PATCH] feat: dial hints on the account-bound operations for self-resolving hosts --- cli/src/main.rs | 8 +++---- core/src/address.rs | 9 +++----- core/src/client.rs | 51 +++++++++++++++++++++++++++++++++------------ 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index a24a7b2..e3f0ee8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -340,7 +340,7 @@ fn restore(key: &PathBuf, store: &Store, address: &str, timeout: u64) -> Result< fn rotate(key: &PathBuf, store: &Store, timeout: u64) -> Result<()> { let account = load_account(key, store)?; - let rotated = client::rotate(store, &account, timeout)?; + let rotated = client::rotate(store, &account, None, timeout)?; let addr = store.account()?.expect("rotate checked for an account"); println!("rotated {}", addr.short()); println!("new key: {}", b32(&rotated.new_pk)); @@ -509,7 +509,7 @@ fn accept(key: &PathBuf, store: &Store, address: &str, timeout: u64) -> Result<( } store.accept(&address, &identity)?; println!("accepted {address}; its token travels in your next message to them"); - match client::push_tokens(store, &account, timeout)? { + match client::push_tokens(store, &account, None, timeout)? { Pushed::NotRegistered => { warn("not registered; the set will be pushed with your first fetch") } @@ -525,7 +525,7 @@ fn block(key: &PathBuf, store: &Store, address: &str, timeout: u64) -> Result<() return Err(anyhow!(format!("{address} was never accepted"))); } println!("blocked {address}; their mail lands in requests from their next message on"); - match client::push_tokens(store, &account, timeout)? { + match client::push_tokens(store, &account, None, timeout)? { Pushed::NotRegistered => { warn("not registered; the set will be pushed with your first fetch") } @@ -711,7 +711,7 @@ fn resolve_id_prefixes(store: &Store, ids: &[String]) -> Result Result<()> { let account = load_account(key, store)?; let full = resolve_id_prefixes(store, ids)?; - let removed = client::delete(store, &account, &full, timeout)?; + let removed = client::delete(store, &account, &full, None, timeout)?; println!("removed {removed} message(s) from the server"); Ok(()) } diff --git a/core/src/address.rs b/core/src/address.rs index cfa9ba5..97aaf04 100644 --- a/core/src/address.rs +++ b/core/src/address.rs @@ -217,6 +217,7 @@ fn valid_username(name: &str) -> Result<(), Error> { #[cfg(test)] mod tests { use super::*; + use crate::crypto::b32; #[test] fn dial_hint_routes_without_renaming() { @@ -227,14 +228,10 @@ mod tests { // proof-of-possession and display all read `host`. assert_eq!(addr.dial.as_deref(), Some("192.0.2.10")); assert_eq!(addr.host, "example.org"); - assert_eq!(addr.short(), "alice@example.org:1961"); + // The default port is elided in the short form, as everywhere. + assert_eq!(addr.short(), "alice@example.org"); assert_eq!(Address::parse("alice@example.org").unwrap().dial, None); } -} - -mod tests { - use super::*; - use crate::crypto::b32; #[test] fn short_form_and_default_port() { diff --git a/core/src/client.rs b/core/src/client.rs index cd312e4..cc2f706 100644 --- a/core/src/client.rs +++ b/core/src/client.rs @@ -56,6 +56,20 @@ impl Session { /// Opens a session, enforcing sec 4's rule about unpinned servers. TCP pins /// the server's Noise static key; on RNS the destination hash is the pin /// (RNS.md sec 13.4). +/// The account address with the host's dial hint applied, when the caller +/// resolved DNS itself: account-bound operations re-read the account, so +/// the hint rides along this way rather than in the stored row. +#[cfg(feature = "store")] +fn account_with_dial(store: &Store, dial: Option<&str>) -> Result { + let addr = store + .account()? + .ok_or(Error::NotRegistered)?; + Ok(match dial { + Some(dial) => addr.with_dial(dial), + None => addr, + }) +} + #[cfg(feature = "store")] pub fn connect( store: &Store, @@ -286,9 +300,16 @@ pub enum Pushed { /// An accept or a block only takes effect once the server holds the changed /// set, so it is pushed now rather than at the next fetch. #[cfg(feature = "store")] -pub fn push_tokens(store: &Store, account: &Account, timeout: u64) -> Result { - let Some(addr) = store.account()? else { - return Ok(Pushed::NotRegistered); +pub fn push_tokens( + store: &Store, + account: &Account, + dial: Option<&str>, + timeout: u64, +) -> Result { + let addr = match account_with_dial(store, dial) { + Ok(addr) => addr, + Err(Error::NotRegistered) => return Ok(Pushed::NotRegistered), + Err(e) => return Err(e), }; let mut session = connect(store, &addr, true, timeout)?; let held = authenticate(session.transport(), &addr.user, account, store)?; @@ -346,10 +367,13 @@ pub struct Rotated { /// is untouched; only the index moves, and the superseded key stays /// derivable from it (sec 2). #[cfg(feature = "store")] -pub fn rotate(store: &Store, account: &Account, timeout: u64) -> Result { - let Some(addr) = store.account()? else { - return Err(Error::NotRegistered); - }; +pub fn rotate( + store: &Store, + account: &Account, + dial: Option<&str>, + timeout: u64, +) -> Result { + let addr = account_with_dial(store, dial)?; if account.index() as usize >= MAX_CHAIN { return Err(Error::ChainLimit { index: account.index() + 1, @@ -564,6 +588,9 @@ pub struct FetchOptions<'a> { pub reset: bool, /// Network timeout in seconds. pub timeout: u64, + /// Dial this address instead of the account host, for hosts that + /// resolve DNS themselves; the host keeps its identity roles. + pub dial: Option<&'a str>, /// Checked between pages and before each envelope; when raised, /// `fetch_with` stops and returns the partial summary with `cancelled` /// set. What was processed is accounted for locally — cursor moved or @@ -579,6 +606,7 @@ impl FetchOptions<'_> { keep, reset, timeout, + dial: None, cancel: None, } } @@ -607,9 +635,7 @@ pub fn fetch_with( account: &Account, opts: FetchOptions<'_>, ) -> Result { - let Some(addr) = store.account()? else { - return Err(Error::NotRegistered); - }; + let addr = account_with_dial(store, opts.dial)?; if opts.reset { store.set_cursor(0, &[0u8; ID_LEN])?; } @@ -761,11 +787,10 @@ pub fn delete( store: &Store, account: &Account, ids: &[[u8; ID_LEN]], + dial: Option<&str>, timeout: u64, ) -> Result { - let Some(addr) = store.account()? else { - return Err(Error::NotRegistered); - }; + let addr = account_with_dial(store, dial)?; let mut session = connect(store, &addr, true, timeout)?; let transport = session.transport(); authenticate(transport, &addr.user, account, store)?;