From 5559fcce31f3412e326bfbaa2a0e7ad4acd75a6a Mon Sep 17 00:00:00 2001 From: randogoth Date: Tue, 29 Sep 2026 12:46:58 +0300 Subject: [PATCH] feat: dial hint on Address for hosts that resolve DNS themselves --- core/src/address.rs | 35 +++++++++++++++++++++++++++++++++++ core/src/client.rs | 10 +++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/core/src/address.rs b/core/src/address.rs index f073f5a..cfa9ba5 100644 --- a/core/src/address.rs +++ b/core/src/address.rs @@ -35,6 +35,13 @@ pub struct Address { pub port: u16, pub scheme: Scheme, pub identity: Option<[u8; KEY_LEN]>, + /// Where to dial instead of resolving `host` again: an embedder that + /// resolves DNS itself (an Android app, whose native getaddrinfo can be + /// dead while the platform resolver works) sets the address's IP here. + /// The hostname keeps every identity role — pins, PoP, display — so + /// `dial` is purely a routing hint, and `None` (the default) means + /// fumi resolves `host` itself. + pub dial: Option, } impl Address { @@ -53,6 +60,15 @@ impl Address { format!("{}{}/{}", self.scheme.prefix(), self.short(), b32(identity)) } + /// Routing hint: dial this address instead of resolving `host`. The + /// hostname is untouched — pins, proof-of-possession and display all + /// keep using it, so a dial override changes where the packets go and + /// nothing about who the server is. + pub fn with_dial(mut self, dial: &str) -> Address { + self.dial = Some(dial.to_string()); + self + } + /// The Reticulum destination hash, for the `rns` carrier only. pub fn destination(&self) -> Result<[u8; 16], Error> { if self.scheme != Scheme::Rns { @@ -109,6 +125,7 @@ impl Address { port, scheme, identity, + dial: None, } } Scheme::Rns => { @@ -131,6 +148,7 @@ impl Address { port: 0, scheme, identity, + dial: None, } } }) @@ -197,6 +215,23 @@ fn valid_username(name: &str) -> Result<(), Error> { } #[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dial_hint_routes_without_renaming() { + let addr = Address::parse("alice@example.org:1961") + .unwrap() + .with_dial("192.0.2.10"); + // The hint changes where packets go, never who the server is: pins, + // 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"); + assert_eq!(Address::parse("alice@example.org").unwrap().dial, None); + } +} + mod tests { use super::*; use crate::crypto::b32; diff --git a/core/src/client.rs b/core/src/client.rs index 7931026..cd312e4 100644 --- a/core/src/client.rs +++ b/core/src/client.rs @@ -71,7 +71,15 @@ pub fn connect( host: addr.host.clone(), }); } - let transport = TcpTransport::connect(&addr.host, addr.port, pinned, timeout)?; + // The dial hint lets a host that resolves DNS itself (an + // Android app) reach the server by IP while `host` keeps its + // identity roles. + let transport = TcpTransport::connect( + addr.dial.as_deref().unwrap_or(&addr.host), + addr.port, + pinned, + timeout, + )?; let unpinned_static = if pinned.is_none() { Some(*transport.server_static()) } else {